66 Commits

Author SHA1 Message Date
您的名字 514368bc07 feat: 升级到v3.1.0
- 实现定时按钮倒计时显示功能
- 适配pixel主题化图标展示
- 优化TimerDialog按钮宽度设计
2025-10-28 19:40:13 +08:00
您的名字 794e1a32fe feat: 优化播放进度条交互体验
- 修复拖拽时圆球消失问题
- 添加动态轨道高度变化效果(按住时4dp,松开时2dp)
- 优化圆球大小设置(固定14dp)
- 添加ProGuard规则保护DefaultTimeBar反射字段
- 改进触摸事件处理逻辑
- 增强拖拽体验的流畅性

修复内容:
- CustomSeekView: 重构触摸事件处理和动态高度调整
- 布局文件: 统一设置圆球大小为14dp
- ProGuard: 保护Media3 DefaultTimeBar字段不被混淆
2025-10-24 16:53:19 +08:00
您的名字 df0333d26e fix: 修复更新跳转链接,跳转到具体版本页面
- 修改Updater.java中的confirm方法
- 从硬编码的/releases/latest改为动态跳转到/releases/tag/v{version}
- 添加latestVersion字段存储检测到的最新版本号
- 确保点击更新后跳转到正确的版本页面
- 同时修复mobile和leanback版本
2025-10-24 14:42:58 +08:00
您的名字 0fd0e245d4 docs: 更新README.md到v3.0.9版本
- 更新版本号徽章到3.0.9
- 更新下载链接指向v3.0.9版本
- 添加v3.0.9版本历史记录
- 新增v3.0.9更新日志,包含新功能和UI优化
- 更新APK文件大小信息
2025-10-24 14:30:48 +08:00
您的名字 e95ffad576 chore: 更新Release仓库json文件到v3.0.9
- 更新mobile.json版本信息到3.0.9
- 更新leanback.json版本信息到3.0.9
- 添加新功能描述:直播开关控制、实时倍速显示
- 添加UI优化描述:滑杆优化、播放进度条改进
- 更新下载链接指向v3.0.9版本
2025-10-24 14:04:12 +08:00
您的名字 78570eed7c feat: 升级到v3.0.9版本
- 新增直播开关控制功能,可隐藏/显示直播tab
- 优化滑杆圆球大小至20dp直径,提升操作体验
- 改进滑杆刻度显示,非激活轨道显示刻度
- 增强播放进度条动态大小调整功能
- 新增实时倍速显示功能
- 优化源管理模块间距动态调整
- 修复播放进度条圆球跳回问题
- 完善直播开关逻辑和UI交互
- 更新版本号至3.0.9
2025-10-24 14:00:14 +08:00
您的名字 dcc751c691 feat: 更新检测机制优化
- 修改更新检测逻辑,直接使用GitHub Releases API检测最新版本
- 点击更新按钮跳转到GitHub Releases页面而不是直接下载APK
- 优化版本号比较算法,支持更准确的版本检测
- 修改关于页面按钮文字颜色为白色
- 移除对本地JSON文件的依赖,提高更新检测的可靠性

相关文件:
- app/src/mobile/java/com/fongmi/android/tv/Updater.java
- app/src/leanback/java/com/fongmi/android/tv/Updater.java
- app/src/mobile/res/layout/dialog_about.xml
2025-10-14 19:10:23 +08:00
您的名字 7a9dc27835 发布v3.0.8正式版 - 4个架构包
 新功能:
• 更新流程优化:点击更新跳转到GitHub Releases页面
• UI优化:关于页面按钮文字颜色改为白色
• 点击效果优化:使用更柔和的半透明白色
• 选中状态优化:使用半透明黄色背景

🔧 修复:
• 修复Hook类安装权限问题
• 解决'解析软件包时出现问题'错误

📱 包含版本:
• 手机版 ARM64-v8a (34MB)
• 手机版 ARMv7a (30MB)
• TV版 ARM64-v8a (34MB)
• TV版 ARMv7a (30MB)
2025-10-14 18:46:10 +08:00
您的名字 3407f1f955 更新v3.0.8手机版APK
- 修复Hook类安装权限问题
- 优化点击效果颜色,使用更柔和的半透明白色
- 调整选中状态背景色为半透明黄色
- 更新版本号到3.0.8
2025-10-14 18:00:14 +08:00
您的名字 54280b68eb 修复点击效果和安装权限问题
- 修复Hook类中canRequestPackageInstalls()返回false导致的安装失败问题
- 优化点击效果颜色,从刺眼的亮黄色改为柔和的半透明白色
- 调整选中状态背景色,使用更柔和的半透明黄色
- 清理项目中的临时文件和重复文件
- 更新版本号到3.0.8
2025-10-14 17:50:42 +08:00
您的名字 cf56f091f3 docs: 更新到v3.0.8版本,优化UI交互体验 2025-10-14 13:40:13 +08:00
您的名字 9d6d531ffe docs: 更新README文档,反映新的文件结构和下载方式
📝 更新内容:
• 更新下载链接指向XMBOX-Release仓库
• 添加版本历史链接,支持历史版本下载
• 更新v3.0.8更新日志,突出UI交互体验优化
• 添加下载说明章节,解释新的文件结构
• 更新APK文件大小信息
• 保持文档与新的版本化文件结构同步

🔗 新的下载方式:
• 最新版本:直接下载链接
• 历史版本:按版本号组织的文件夹
• 版本信息:每个版本包含完整的JSON配置
2025-10-14 12:55:23 +08:00
您的名字 0c60ddf63d chore: 升级版本号至3.0.8
- 更新app/build.gradle中的versionCode和versionName
- 更新README.md中的版本信息和更新日志
- 更新XMBOX-Release目录中的版本配置
- 更新create_release.sh脚本中的版本号
- 添加v3.0.8的更新说明,重点突出UI交互体验优化
2025-10-14 12:37:30 +08:00
您的名字 928a0e9807 feat: 优化UI交互体验和视觉效果
- 修复按钮点击效果过于明显的问题
- 统一使用自定义背景替代系统selectableItemBackgroundBorderless
- 移除Control.Action样式中的文字阴影效果
- 优化直播页面选择按钮颜色为主题黄色
- 调整许可协议页面按钮区域上间距为8dp
- 修复跨类和换源按钮的文字重叠问题
- 提升整体UI视觉一致性和用户体验
2025-10-14 12:35:12 +08:00
您的名字 93d8c5703b feat: v3.0.7 UI优化和功能改进
 UI优化:
- 新增CustomSwitch自定义开关组件(黄色/黑色Material Design)
- 优化电量百分比显示(16sp字号,距离闪电图标2dp)
- 隐藏壁纸功能,精简设置页面

🔒 安全增强:
- 启用v1/v2/v3/v4多重签名保护
- 提升应用安全性和兼容性

🔧 改进优化:
- 修复设置页面开关组件问题
- 优化内存使用
- 提升播放稳定性
2025-10-13 22:50:42 +08:00
您的名字 597261ff57 feat: 添加自定义开关按钮样式和UI优化
- 新增CustomSwitch自定义开关组件(黄色/黑色Material Design风格)
- 优化电量百分比显示(16sp字号,距离闪电图标2dp)
- 更新所有设置页面使用新的开关样式
- 移除旧的开关颜色设置代码
- 构建v8a正式版APK
2025-10-13 20:22:15 +08:00
您的名字 d4d30d39c1 feat: 优化播放页面电池显示和搜索页面布局
 新增功能
- 播放页面添加电池电量显示功能
- 充电时显示闪电图标
- 时间、闪电图标、电量百分比分离显示

🎨 界面优化
- 优化搜索页面左侧视频源列表间距和字体大小
- 改进布局紧凑性,提升视觉体验

🐛 问题修复
- 修复全屏播放模式下电池百分比无法显示的问题
- 修复普通布局缺少电量显示控件的问题

🔧 其他改进
- 删除隐私协议页面顶部应用图标
- 优化通知权限请求时机(改为用户同意隐私协议后请求)
2025-10-13 17:43:07 +08:00
您的名字 f49f1cd0b0 fix: 修复静默检查的严重问题
🚨 关键修复:
- 移除多余的 getAutoUpdateCheck() 判断
- 完全恢复 FongMi/TV 的更新检查逻辑
- 所有控制都通过 Setting.getUpdate() 进行

🔄 正确的更新流程:
1. 应用启动时总是执行更新检查
2. 但是否弹窗由 Setting.getUpdate() 控制
3. 用户拒绝更新后不再弹窗,直到手动检查

⚠️ 问题说明:
之前添加的 getAutoUpdateCheck() 检查是错误的,
这会导致即使有新版本也不弹窗的问题。
FongMi/TV 没有这个额外检查,所有逻辑都在 need() 方法中。
2025-10-13 17:43:07 +08:00
您的名字 8c6275ffe8 fix: 优化更新检查用户体验
🔇 静默更新检查:
- 自动检查时不显示任何提示信息
- 只有发现新版本时才弹出更新对话框
- 手动检查时显示完整的状态反馈

👤 用户友好:
- 避免应用启动时的无关弹窗
- 网络错误时静默处理,不干扰用户
- 完全模仿 FongMi/TV 的更新体验

🎛️ 控制机制:
- forceCheck 标记区分自动/手动检查
- 手机版和TV版行为完全一致
- 用户可以通过设置禁用自动检查
2025-10-13 17:43:07 +08:00
您的名字 e094f38423 fix: 修复应用更新检查机制
🔧 更新检查优化:
- 复刻 FongMi/TV 的更新检查机制
- 使用独立 Release 仓库托管更新信息
- 避免 GitHub API 频率限制问题
- 默认启用自动更新检查功能

📂 文件变更:
- 重写手机版 Updater.java,统一更新检查逻辑
- 修改 Github.java,指向新的 Release 仓库
- 启用 getAutoUpdateCheck() 默认值为 true

 功能改进:
- 手机版和TV版使用相同的更新检查机制
- 支持国内外网络环境自动切换
- 错误处理和用户提示优化
2025-10-13 17:43:07 +08:00
Tochen 0734ffc630 Update README.md 2025-09-26 17:16:53 +08:00
您的名字 a8700a8c66 docs: 更新 README.md 到 v3.0.7
- 更新版本号到 v3.0.7
- 更新下载链接和文件大小
- 添加详细的 v3.0.7 更新日志
- 记录所有核心修复和UI优化内容
2025-09-26 13:19:14 +08:00
您的名字 ca95128ee9 feat: 全面优化应用稳定性和用户体验
🐛 核心修复:
- 修复 VodConfig/LiveConfig 空指针崩溃问题
- 添加构造函数初始化列表,防止 clear() 方法空指针异常
- 增强 Setting 类隐私协议状态管理

🎨 UI/UX 改进:
- 新增隐私协议页面 (PrivacyAgreementActivity)
- 修复按钮文字显示不完整问题(调整文字大小和按钮高度)
- 空状态动画位置优化(向上移动40dp)
- TV版选集按钮选中状态文字改为黄色显示

🌟 空状态优化:
- 恢复完整的 Lottie 空状态动画 (54KB)
- 新增多个空状态布局:搜索、收藏、通用
- 更新空状态文案为川渝方言风格:'这里撒子内容都没得~'

📺 TV版本优化:
- 新增专用颜色选择器 episode_text.xml
- 选集按钮选中状态文字颜色改为黄色 (#FFEB3B)
- 仅影响视频详情页,不干扰其他页面

🔧 技术改进:
- 优化生命周期管理和错误处理
- 增强任务栈管理,防止用户返回协议页面
- 添加空值安全检查,提升应用稳定性

版本:v3.0.7 - 包含所有修复和优化的稳定版本
2025-09-26 13:19:13 +08:00
您的名字 dde56eeedb 修复leanback版本更新提示和视频源选中状态问题 2025-09-26 13:19:09 +08:00
Tochen f530ee6407 Update README.md 2025-09-24 19:08:47 +08:00
Tochen f9ec0334e1 Update README.md 2025-09-24 18:56:52 +08:00
Tochen 389d548d08 Update README.md 2025-09-24 18:50:13 +08:00
Tochen fb948dc8c0 更新 README.md 2025-09-24 18:23:38 +08:00
Tochen ce2f46cf5b Update README.md 2025-09-24 18:04:58 +08:00
Tochen db63949a31 更新 README.md 2025-09-17 16:09:38 +08:00
Tochen f2127ab3a6 更新 README.md 2025-09-17 16:05:54 +08:00
Tochen f525a88668 Update README.md 2025-08-28 15:26:23 +08:00
Tochen 91a20c8aae Update README.md 2025-08-28 15:22:21 +08:00
Tochen 59a8c4fd01 Update README.md 2025-08-28 15:16:28 +08:00
Tochen 3f63cc2416 Update README.md 2025-08-24 01:11:51 +08:00
您的名字 fcdef561ec Update version to 3.0.6 in README.md 2025-08-22 17:42:57 +08:00
您的名字 8d0ae1d5b4 Update strings and notifications 2025-08-22 17:39:24 +08:00
您的名字 afd2d4667d Update settings page icons: Replace icons with Material Design 3 style icons 2025-08-22 17:36:29 +08:00
您的名字 ceadb06a64 docs: 同步构建说明文档 2025-08-22 13:36:22 +08:00
您的名字 f276fad550 docs: 更新版本至 v3.0.5 2025-08-22 13:34:01 +08:00
您的名字 b20cf45850 完善版本检查反馈:添加'已是最新版'提示
- 修复点击版本号检查更新时无反馈的问题
- 添加'已是最新版本'的友好提示
- 优化用户体验,确保每次点击都有明确反馈
- 支持移动端和TV端
2025-08-06 16:07:38 +08:00
您的名字 593f1e7444 📱 发布XMBOX v3.0.6 正式版APK
🚀 新特性:
- 修复自动更新功能,支持点击版本号弹窗检查更新
- 智能镜像选择:GitHub API限流时自动切换到Gitee
- 友好错误提示:显示具体的更新失败原因

📦 发布文件:
- mobile-arm64_v8a.apk (36MB) - 适用于大部分现代手机
- mobile-armeabi_v7a.apk (32MB) - 适用于较老设备

🛠️ 技术改进:
- 优化网络请求逻辑,提高成功率
- 配置Gitee镜像加速,提升国内用户体验
- 增强异常处理,避免静默失败
2025-08-06 15:22:16 +08:00
您的名字 a4d671b394 修复自动更新功能并优化Gitee镜像配置
- 修复版本号点击无弹窗问题,添加友好的错误提示
- 更新Gitee镜像配置,使用正确的用户名 ochenoktochen/XMBOX
- 优化更新检测逻辑,支持智能镜像选择和错误处理
- 增强用户体验,显示具体的更新失败原因
- 支持GitHub API限流时自动切换到Gitee镜像源
2025-08-06 14:35:54 +08:00
您的名字 836e363f94 feat: 发布 XMBOX v3.0.5 - 重大稳定性提升
🔥 主要更新:
- 升级到最新依赖版本 (AGP 8.12.0, Gradle 8.13, Java 17)
- 完全重构 Context 管理系统,解决闪退问题
- 采用原项目 FongMi/TV 的稳定策略
- 启用 EventBus 编译时优化,提升性能
- 修复关于页面 GitHub 链接,永远指向最新版本

🐛 修复问题:
- 修复 Context 初始化时序问题导致的崩溃
- 修复 ApplicationContext 被垃圾回收的问题
- 修复 Updater 构造函数的过早调用问题
- 解决 Android 15 设备兼容性问题

 性能优化:
- EventBus 性能提升 (编译时索引生成)
- APK 体积优化 (release 版本 36MB vs 49MB debug)
- 代码混淆和资源压缩启用
- 内存使用优化

📦 构建改进:
- 支持 arm64-v8a 和 armeabi-v7a 双架构
- 完整的 release 和 debug 版本构建
- 新增 chaquo Python 模块支持
- 同步所有依赖到最新稳定版本

🛡️ 稳定性:
- 采用经过 6.3k+ 星标验证的原项目策略
- 完善的异常处理机制
- 防护性编程实践
- 支持 Android 5.0+ 到 Android 15
2025-08-06 11:55:38 +08:00
您的名字 dab1425dea 🐛 Fix source switching crash & enhance stability (v3.0.4)
### 🐛 Bug Fixes
- Fix random crashes when switching video sources in settings management
- Enhanced VodConfig.setHome() null pointer exception handling
- Improved Fragment lifecycle checks to prevent crashes
- Optimized HistoryDialog source switching safety
- Enhanced thread safety for concurrent loading

###  Performance Improvements
- Added automatic cache cleaning functionality
- Improved memory usage optimization
- Enhanced network request stability

### 🆕 New Features
- Added comprehensive error handling mechanisms
- Enhanced crash protection functionality
- Improved Fragment state validation

### 📱 Build Improvements
- Updated README with professional documentation
- Enhanced build configuration for ARM64-V8A and ARM V7A
- Improved APK packaging and signing process
2025-07-30 21:25:10 +08:00
Tosencen 0d7b25710c 删除冲突的APK文件 2025-07-08 17:00:25 +08:00
Tosencen a1a45aeacd 改进UI: 更新设置页面图标,添加关于弹窗,优化镜像更新功能 2025-07-08 16:59:04 +08:00
Tosencen 6eb7f9139d chore: add v7a release APK for 3.0.3 2025-07-08 12:39:51 +08:00
Tosencen eafc53e8b2 chore: update release APK to 3.0.3 2025-07-08 12:36:14 +08:00
Tosencen 9c7a0fd40e chore: bump version to 3.0.3 2025-07-08 12:31:32 +08:00
Tosencen b847ff23dd 更新设置图标:使用更简洁的线性风格设计 2025-07-07 21:43:15 +08:00
Tosencen 8579905949 feat: 1. 修改版本号为3.0.2 2. 优化历史记录复制功能 3. 调整Toast显示时间为1秒 2025-07-03 20:00:03 +08:00
Tosencen 6d6239e602 release: 发布v3.0.1版本 2025-07-03 11:25:41 +08:00
Tosencen 81832a055e release: 发布v3.0.1版本 2025-07-03 11:25:39 +08:00
Tochen 4ec3c434b2 Update README.md 2025-07-03 11:22:16 +08:00
Tosencen d47f8d5cd4 docs: zlive - 直播相关功能 2025-07-03 10:59:09 +08:00
Tosencen 5f7df956c3 docs: tvbus - TV 总线功能 2025-07-03 10:59:09 +08:00
Tosencen beaf3b30f6 docs: thunder - 迅雷下载相关功能 2025-07-03 10:59:09 +08:00
Tosencen 730c6cc7a7 docs: quickjs - JavaScript 引擎 2025-07-03 10:59:09 +08:00
Tosencen 7c1744b366 docs: jianpian - 剪片相关功能 2025-07-03 10:59:09 +08:00
Tosencen 1b61870cd4 docs: hook - 钩子功能 2025-07-03 10:59:09 +08:00
Tosencen 0dcc0e6da1 docs: forcetech - 强制技术相关功能 2025-07-03 10:59:08 +08:00
Tosencen 592bcff438 docs: catvod - 视频点播相关功能 2025-07-03 10:59:08 +08:00
Tosencen 6af8908670 docs: app - 主要的应用程序代码 2025-07-03 10:59:08 +08:00
Tosencen 993ef78d4d docs: 更新模块说明 2025-07-03 10:57:15 +08:00
Tosencen 53ef17c01d docs: 更新文件夹描述 2025-07-03 10:57:15 +08:00
307 changed files with 8722 additions and 1195 deletions
+36 -7
View File
@@ -1,8 +1,37 @@
.idea
.gradle
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Log/OS Files
*.log
# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.apk
*.aab
output.json
# IntelliJ
*.iml
.idea/
misc.xml
deploymentTargetDropDown.xml
render.experimental.xml
# Keystore files
*.jks
lib-*.aar
*build
/media*
/Release
/local.properties
*.keystore
# Google Services (e.g. APIs or Firebase)
google-services.json
# Android Profiling
*.hprof
# APK files
apk/release/*.apk
+3
View File
@@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "automatic"
}
+264 -219
View File
@@ -1,281 +1,326 @@
# XMBOX
<h1 align="center"> 📱 XMBOX - Android资源播放器
</h1>
<div align="center">
一个简单的视频播放器应用,支持以下功能:
![Version](https://img.shields.io/badge/version-3.0.9-blue.svg)
![Android](https://img.shields.io/badge/platform-Android-green.svg)
![License](https://img.shields.io/badge/license-GPL--3.0-orange.svg)
![Build](https://img.shields.io/badge/build-passing-brightgreen.svg)
## 主要功能
- 视频播放:支持多种格式视频播放
- 直播观看:支持直播源播放
- 收藏管理:可收藏喜欢的视频和直播源
- 设置中心:自定义应用配置
一个操作方便、界面简洁的Android视频播放器盒子,需自行添源,支持TV和手机双平台。
## 技术特点
- 基于 Android 原生开发
- 使用 ExoPlayer 作为播放内核
- 支持 TV 和手机双平台
- Material Design 界面设计
[下载APK](https://github.com/Tosencen/XMBOX-Release/tree/main/apk/release) • [功能特性](#-功能特性) • [构建指南](#-构建指南) • [API文档](#-api文档)
## 应用截图
- 视频列表
- 播放界面
- 设置中心
</div>
## 开发说明
本项目仅用于学习 Android 开发,代码改自 [FongMi/TV](https://github.com/FongMi/TV)。
## 🎯 功能特性
## 免责声明
1. 本项目仅供学习交流使用,不得用于商业用途
2. 项目中的内容均来自网络,如有侵权请联系删除
3. 使用本项目产生的一切后果由使用者自行承担
### 📺 多平台支持
- **Android TV版本** - 针对电视、盒子优化的遥控器界面
- **手机版本** - 触屏友好的移动端界面
- **多架构支持** - ARM64-V8A 和 ARM V7A 双架构
## 许可证
GPL-3.0 license
### 🎬 强大的播放功能
- 🎵 **多格式支持** - 支持主流视频格式播放
- 📡 **直播观看** - 支持各种直播源协议
- 🔍 **智能搜索** - 全局搜索和换源功能
- 📚 **收藏管理** - 视频收藏和历史记录
- 🎨 **自定义界面** - 丰富的主题和布局选项
# 影視
### ⚡ 技术特色
- 🚀 **高性能播放** - 基于ExoPlayer播放内核
- 🔧 **模块化架构** - 清晰的模块分层设计
- 🛡️ **稳定可靠** - 完善的错误处理和崩溃防护
- 🌐 **网络优化** - 智能代理和DNS解析
- 📱 **Material Design** - 现代化UI设计
### 基於 CatVod 項目
## 📥 下载安装
https://github.com/CatVodTVOfficial/CatVodTVJarLoader
### 最新版本: v3.0.9
### 點播欄位
| 平台 | ARM64-V8A | ARM V7A |
|------|-----------|---------|
| **📱 手机版** | [下载 (35.8MB)](https://github.com/Tosencen/XMBOX-Release/raw/main/apk/release/v3.0.9/mobile-arm64_v8a-v3.0.9.apk) | [下载 (31.6MB)](https://github.com/Tosencen/XMBOX-Release/raw/main/apk/release/v3.0.9/mobile-armeabi_v7a-v3.0.9.apk) |
| **📺 TV版** | [下载 (35.9MB)](https://github.com/Tosencen/XMBOX-Release/raw/main/apk/release/v3.0.9/leanback-arm64_v8a-v3.0.9.apk) | [下载 (31.7MB)](https://github.com/Tosencen/XMBOX-Release/raw/main/apk/release/v3.0.9/leanback-armeabi_v7a-v3.0.9.apk) |
| 欄位名稱 | 預設值 | 說明 | 其他 |
|------------|------|------|------------|
| searchable | 1 | 是否搜索 | 0:關閉;1:啟用 |
| changeable | 1 | 是否換源 | 0:關閉;1:啟用 |
| quickserch | 1 | 是否快搜 | 0:關閉;1:啟用 |
| indexs | 0 | 是否聚搜 | 0:關閉;1:啟用 |
| hide | 0 | 是否隱藏 | 0:顯示;1:隱藏 |
| timeout | 15 | 播放超時 | 單位:秒 |
| header | none | 請求標頭 | 格式:json |
| click | none | 點擊js | javascript |
### 📁 版本历史
- **v3.0.9**: [查看v3.0.9版本](https://github.com/Tosencen/XMBOX-Release/tree/main/apk/release/v3.0.9) - 新增直播开关控制和UI交互优化
- **v3.0.8**: [查看v3.0.8版本](https://github.com/Tosencen/XMBOX-Release/tree/main/apk/release/v3.0.8) - UI交互体验全面优化
- **v3.0.7**: [查看v3.0.7版本](https://github.com/Tosencen/XMBOX-Release/tree/main/apk/release/v3.0.7) - 全面优化稳定性和用户体验
### 直播欄位
### 📦 下载说明
- **最新版本**: 根目录的 `mobile.json``leanback.json` 包含最新版本信息
- **历史版本**: 每个版本都有独立的文件夹,包含完整的APK文件和版本信息
- **文件结构**: 按版本号组织,便于管理和下载
- **签名保护**: 所有APK均使用v1/v2/v3/v4多重签名保护
| 欄位名稱 | 預設值 | 說明 | 其他 |
|----------|-------|-------|------------|
| ua | none | 用戶代理 | |
| origin | none | 來源 | |
| referer | none | 參照地址 | |
| epg | none | 節目地址 | |
| logo | none | 台標地址 | |
| pass | false | 是否免密碼 | |
| boot | false | 是否自啟動 | |
| timeout | 15 | 播放超時 | 單位:秒 |
| header | none | 請求標頭 | 格式:json |
| click | none | 點擊js | javascript |
| catchup | none | 回看參數 | |
| timeZone | none | 時區 | |
TV版基于 [FongMi/TV](https://github.com/FongMi/TV) 原项目就改了些配色,想要嘿稳定的可去原项目体验
### 📋 系统要求
- Android 5.0 (API 21) 及以上
- ARM64-V8A: 推荐新设备使用,性能更优
- ARM V7A: 兼容老设备,适配性更强
### 樣式
## 🏗️ 构建指南
| 欄位名稱 | 值 | 說明 |
|-------|------|-----|
| type | rect | 矩形 |
| | oval | 橢圓 |
| | list | 列表 |
| ratio | 0.75 | 34 |
| | 1.33 | 43 |
### 📋 环境要求
- Android Studio Arctic Fox 或更高版本
- JDK 11 或更高版本
- Android SDK API 35
- Gradle 8.10.2
直式
### 🔧 快速开始
```json
{
"style": {
"type": "rect"
}
}
1. **克隆项目**
```bash
git clone https://github.com/yourusername/XMBOX.git
cd XMBOX
```
橫式
```json
{
"style": {
"type": "rect",
"ratio": 1.33
}
}
2. **配置签名** (可选)
```bash
# 将你的签名文件放到 keystore/ 目录
# 或修改 app/build.gradle 中的签名配置
```
正方
3. **构建项目**
```bash
# 构建所有版本
./gradlew assembleRelease
```json
{
"style": {
"type": "rect",
"ratio": 1
}
}
# 构建特定版本
./gradlew assembleMobileArm64_v8aRelease # 手机版 ARM64
./gradlew assembleLeanbackArm64_v8aRelease # TV版 ARM64
./gradlew assembleMobileArmeabi_v7aRelease # 手机版 ARM V7A
./gradlew assembleLeanbackArmeabi_v7aRelease # TV版 ARM V7A
```
正圓
```json
{
"style": {
"type": "oval"
}
}
4. **生成的APK位置**
```
app/build/outputs/apk/
├── mobileArm64_v8a/release/mobile-arm64_v8a.apk
├── leanbackArm64_v8a/release/leanback-arm64_v8a.apk
├── mobileArmeabi_v7a/release/mobile-armeabi_v7a.apk
└── leanbackArmeabi_v7a/release/leanback-armeabi_v7a.apk
```
橢圓
## 🏛️ 项目架构
```json
{
"style": {
"type": "oval",
"ratio": 1.1
}
}
### 📂 模块说明
```
XMBOX/
├── app/ # 主应用模块
│ ├── src/main/ # 通用代码
│ ├── src/mobile/ # 手机版特定代码
│ └── src/leanback/ # TV版特定代码
├── catvod/ # 视频点播核心
├── quickjs/ # JavaScript引擎
├── forcetech/ # 强制技术模块
├── thunder/ # 迅雷下载模块
├── hook/ # 钩子功能
├── jianpian/ # 视频剪辑模块
├── tvbus/ # TV总线功能
└── zlive/ # 直播功能模块
```
### API
### 🔧 技术栈
- **开发语言**: Java
- **UI框架**: Android Views + Material Components
- **播放器**: ExoPlayer
- **网络库**: OkHttp
- **JSON解析**: Gson
- **异步处理**: EventBus
- **数据库**: Room
刷新詳情
## 📝 更新日志
```
http://127.0.0.1:9978/action?do=refresh&type=detail
### v3.0.9 (2025-10-24)
#### ✨ 新功能
* **直播开关控制** - 新增直播tab显示/隐藏开关,用户可根据需要控制直播功能
* **实时倍速显示** - 播放控制对话框新增实时倍速数值显示,提升用户体验
* **源管理优化** - 优化源管理模块间距动态调整,界面更加协调
#### 🎨 UI优化
* **滑杆交互优化** - 滑杆圆球大小优化至20dp直径,提升操作体验
* **刻度显示改进** - 改进滑杆刻度显示,非激活轨道显示刻度,激活轨道保持干净
* **播放进度条增强** - 增强播放进度条动态大小调整功能,修复圆球跳回问题
* **直播开关逻辑** - 完善直播开关逻辑和UI交互,确保功能一致性
#### 🔧 技术改进
* **优化内存使用** - 进一步优化内存管理机制
* **提升播放稳定性** - 增强播放器稳定性
* **增强UI交互体验** - 改进用户界面交互响应
### v3.0.8 (2025-10-14)
#### 🎨 UI交互体验全面优化
* **修复按钮点击效果** - 解决按钮点击效果过于明显的问题
* **统一自定义背景** - 使用自定义背景替代系统selectableItemBackgroundBorderless
* **移除文字阴影** - 清理Control.Action样式中的文字阴影效果
* **优化直播页面** - 选择按钮颜色统一为主题黄色
* **调整页面布局** - 许可协议页面按钮区域上间距调整为8dp
* **修复文字重叠** - 解决跨类和换源按钮的文字重叠问题
* **提升视觉一致性** - 整体UI视觉一致性和用户体验优化
#### 🔧 技术改进
* **优化内存使用** - 改进内存管理机制
* **提升播放稳定性** - 增强播放器稳定性
* **文件结构重组** - 按版本号重新组织发布文件结构
### v3.0.5 (2025-08-20)
#### 🎨 界面优化
- 优化导航栏历史记录图标,采用 Material Design 3 规范的列表图标
- 改进设置页面的图标显示效果
- 优化用户界面视觉体验
### v3.0.4 (2025-07-30)
#### 🐛 修复
- 修复设置页面源管理模块中切换视频源时的随机闪退问题
- 增强VodConfig.setHome()方法的空指针异常处理
- 改进Fragment生命周期检查以防止崩溃
- 优化HistoryDialog中源切换的安全性
- 增强并发加载的线程安全性
#### ⚡ 优化
- 提升应用启动速度
- 优化内存使用
- 增强网络请求稳定性
#### 🆕 新增
- 新增自动缓存清理功能
- 添加更完善的错误处理机制
- 增强崩溃保护功能
### v3.0.3 及更早版本
查看 [完整更新日志](CHANGELOG.md)
## 🔌 API 文档
### 刷新操作
```http
#
GET http://127.0.0.1:9978/action?do=refresh&type=detail
#
GET http://127.0.0.1:9978/action?do=refresh&type=player
#
GET http://127.0.0.1:9978/action?do=refresh&type=live
```
刷新播放
### 推送功能
```http
#
GET http://127.0.0.1:9978/action?do=refresh&type=subtitle&path=http://xxx
```
http://127.0.0.1:9978/action?do=refresh&type=player
#
GET http://127.0.0.1:9978/action?do=refresh&type=danmaku&path=http://xxx
```
刷新直播
### 缓存管理
```http
#
GET http://127.0.0.1:9978/cache?do=set&key=xxx&value=xxx
```
http://127.0.0.1:9978/action?do=refresh&type=live
#
GET http://127.0.0.1:9978/cache?do=get&key=xxx
#
GET http://127.0.0.1:9978/cache?do=del&key=xxx
```
推送字幕
更多API文档请查看 [API参考手册](docs/API.md)
```
http://127.0.0.1:9978/action?do=refresh&type=subtitle&path=http://xxx
```
## 📖 配置说明
推送彈幕
### 点播字段配置
| 字段名 | 默认值 | 说明 | 备注 |
|--------|--------|------|------|
| searchable | 1 | 是否支持搜索 | 0:关闭 1:启用 |
| changeable | 1 | 是否支持换源 | 0:关闭 1:启用 |
| quickSearch | 1 | 是否快速搜索 | 0:关闭 1:启用 |
| timeout | 15 | 播放超时时间 | 单位:秒 |
```
http://127.0.0.1:9978/action?do=refresh&type=danmaku&path=http://xxx
```
### 直播字段配置
| 字段名 | 默认值 | 说明 | 备注 |
|--------|--------|------|------|
| ua | none | 用户代理 | |
| origin | none | 来源 | |
| referer | none | 引用地址 | |
| timeout | 15 | 播放超时 | 单位:秒 |
新增緩存字串
详细配置说明请查看 [配置文档](docs/CONFIG.md)
```
http://127.0.0.1:9978/cache?do=set&key=xxx&value=xxx
```
## 🤝 贡献指南
取得緩存字串
欢迎提交 Issue 和 Pull Request
```
http://127.0.0.1:9978/cache?do=get&key=xxx
```
### 🔄 提交规范
- feat: 新功能
- fix: 修复bug
- docs: 文档更新
- style: 代码格式调整
- refactor: 代码重构
- test: 测试相关
- chore: 构建配置等
刪除緩存字串
### 🧪 开发流程
1. Fork 本项目
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 创建 Pull Request
```
http://127.0.0.1:9978/cache?do=del&key=xxx
```
### ⚖️ 许可协议
XMBOX软件许可协议:
- 以下是对[GPL-3.0](LICENSE.md)开源协议的补充,如有冲突,以以下协议为准。
- 词语约定: 本协议中的“本软件”指“XMBOX软件”,“用户”指签署本协议的使用者,“版权数据”指包括但不限于视频、图像、音频、名字等在内的他人拥有所属版权的数据。
1. 本软件仅为技术性多媒体播放器外壳(“空壳播放器”),核心功能限于基础媒体文件解析与播放。
2. 本软件自身不包含、不预装、不内置、不集成、不主动推荐、不直接或间接提供任何音视频、直播、图文等媒体资源内容。软件播放的任何资源均非由本软件或其开发者提供。
3. 用户通过本软件播放的任何内容均完全来源于用户自行配置、输入、添加、获取或选择的第三方来源(如网络地址、本地文件、用户安装的插件/扩展/配置源等)。本软件仅作为访问用户自行指定内容的技术工具。
4. 本软件无法控制、筛选、审查或保证用户访问的任何第三方内容的合法性、版权状态、准确性、安全性或适宜性。用户对其播放的内容负全部责任。
5. 关于用户责任与风险承担:
* 用户必须确保其通过本软件配置、访问或播放的所有内容均已获相关权利人合法授权,或属于法律允许的自由使用范畴。
* 用户理解并同意,使用本软件访问第三方资源可能涉及侵犯版权、传播非法信息、隐私泄露、网络安全等风险。因用户使用本软件访问、播放或传播内容产生的一切法律责任、纠纷、损失及后果(包括法律诉讼、行政处罚、民事赔偿等),均由用户自行承担,与本软件及其开发者无涉。
* 开发者不认可、不支持任何利用本软件规避技术保护措施(如DRM)的行为,此类行为导致的侵权责任由用户全权承担。
6. 用户承诺并保证不利用本软件从事任何侵犯他人知识产权或其他合法权益的活动,或进行任何违反法律法规的行为。严禁使用本软件播放、传播盗版、色情、暴力、赌博、诈骗、危害国家安全、危害社会稳定等非法或侵权内容。
7. 在任何情况下,本软件开发者均不就因用户使用或无法使用本软件、用户配置或访问的第三方资源、用户违反本协议或法律法规的行为导致的任何直接、间接、偶然、特殊、惩罚性或结果性损害(包括利润损失、数据丢失、业务中断、声誉损害等)承担任何责任(无论基于合同、侵权、严格责任或其他法律理论)。
8. 本软件运行可能依赖第三方库、服务或技术。开发者不对这些第三方组件的可用性、准确性、功能或合法性负责。
9. 用户理解并同意,使用本软件(包括下载、安装、运行)存在固有技术风险(如软件缺陷、兼容性问题、系统不稳定等),用户应自行承担此风险。
10. 本软件仅用于对技术可行性的探索及研究,不接受任何商业(包括但不限于广告等)合作及捐赠。
11. 本软件内使用的部分包括但不限于字体、图片等资源来源于互联网。如果出现侵权可联系开发者移除。
12. 使用本软件的过程中可能会产生版权数据。对于这些版权数据,本软件不拥有它们的所有权。为了避免侵权,用户务必在 24 小时内 清除使用本项目的过程中所产生的版权数据。
13. 本协议受中华人民共和国法律管辖并据其解释。若用户所在地法律强制规定特定责任条款,应以当地法律要求为准,但其他条款仍保持有效。任何由本协议或使用本软件引起的争议,应首先通过友好协商解决。
14. 若你使用了本软件,即代表你接受本协议。
### Proxy
## ⚖️ 免责声明
scheme 支持 http, https, socks4, socks5
1. **学习用途**: 本项目仅供学习和技术交流使用,不得用于商业用途
2. **内容来源**: 项目中的内容来源于网络,如有侵权请联系删除
3. **使用责任**: 使用本项目产生的一切后果由使用者自行承担
4. **法律合规**: 请确保在当地法律法规允许的范围内使用本软件
```
scheme://username:password@host:port
```
## 📄 开源协议
配置新增 proxy 判斷域名是否走代理
全局只需要加上一條規則 ".*."
本项目基于 [GPL-3.0](LICENSE.md) 协议开源
```json
{
"spider": "",
"proxy": [
"raw.githubusercontent.com",
"googlevideo.com"
]
}
```
## 🙏 致谢
### Hosts
- 基于 [FongMi/TV](https://github.com/FongMi/TV) 项目开发
- 感谢 [CatVodTVOfficial](https://github.com/CatVodTVOfficial) 提供的核心技术
- 感谢所有为项目做出贡献的开发者
```json
{
"spider": "",
"hosts": [
"cache.ott.*.itv.cmvideo.cn=base-v4-free-mghy.e.cdn.chinamobile.com"
]
}
```
## 📞 联系方式
### Headers
- GitHub Issues: [提交问题](../../issues)
- 讨论区: [Discussions](../../discussions)
```json
{
"spider": "",
"headers": [
{
"host": "gslbserv.itv.cmvideo.cn",
"header": {
"User-Agent": "okhttp/3.12.13",
"Referer": "test"
}
}
]
}
```
---
### 爬蟲本地代理
<div align="center">
Java
**⭐ 如果这个项目对你有帮助,请给我们一个 Star!**
```
proxy://
```
Made with ❤️ by XMBOX Team
```
Proxy.getUrl(boolean local)
```
Python
```
proxy://do=py
```
```
getProxyUrl(boolean local)
```
JS
```
proxy://do=js
```
```
getProxy(boolean local)
```
### 配置範例
[點播-線上](other/sample/vod/online.json)
[點播-本地](other/sample/vod/offline.json)
[直播-線上](other/sample/live/online.json)
[直播-本地](other/sample/live/offline.json)
### 飛機群
[討論群組](https://t.me/+qTlg0qAVzP9kMmM1)
[發布頻道](https://t.me/fongmi_release)
### 贊助
![photo_2024-01-10_11-39-12](https://github.com/FongMi/TV/assets/3471963/fdc12771-386c-4d5d-9a4d-d0bec0276fa7)
### Star
[![Star History Chart](https://api.star-history.com/svg?repos=FongMi/TV&type=Date)](https://www.star-history.com/#FongMi/TV&Date)
</div>
+62
View File
@@ -0,0 +1,62 @@
# XMBOX Release Files
## 📁 文件结构
```
apk/release/
├── mobile.json # 最新版本信息 (手机版)
├── leanback.json # 最新版本信息 (TV版)
├── v3.0.7/ # v3.0.7版本文件
│ ├── mobile.json # v3.0.7版本信息
│ ├── leanback.json # v3.0.7版本信息
│ ├── mobile-arm64_v8a.apk
│ ├── mobile-armeabi_v7a.apk
│ ├── leanback-arm64_v8a.apk
│ └── leanback-armeabi_v7a.apk
└── v3.0.8/ # v3.0.8版本文件
├── mobile.json # v3.0.8版本信息
├── leanback.json # v3.0.8版本信息
├── mobile-arm64_v8a-v3.0.8.apk
├── mobile-armeabi_v7a-v3.0.8.apk
├── leanback-arm64_v8a-v3.0.8.apk
└── leanback-armeabi_v7a-v3.0.8.apk
```
## 📱 版本说明
### v3.0.8 (最新版本)
- **发布时间**: 2025-10-14
- **版本代码**: 308
- **主要更新**: UI交互体验全面优化
### v3.0.7
- **发布时间**: 2025-09-26
- **版本代码**: 307
- **主要更新**: 全面优化稳定性和用户体验
## 🔗 下载链接
### 最新版本 (v3.0.8)
- **手机版 ARM64**: [mobile-arm64_v8a-v3.0.8.apk](v3.0.8/mobile-arm64_v8a-v3.0.8.apk)
- **手机版 ARMv7**: [mobile-armeabi_v7a-v3.0.8.apk](v3.0.8/mobile-armeabi_v7a-v3.0.8.apk)
- **TV版 ARM64**: [leanback-arm64_v8a-v3.0.8.apk](v3.0.8/leanback-arm64_v8a-v3.0.8.apk)
- **TV版 ARMv7**: [leanback-armeabi_v7a-v3.0.8.apk](v3.0.8/leanback-armeabi_v7a-v3.0.8.apk)
### 历史版本
- **v3.0.7**: [查看v3.0.7版本文件](v3.0.7/)
## 📋 版本信息
每个版本目录都包含对应的JSON配置文件,包含:
- `name`: 版本号
- `desc`: 版本描述和更新内容
- `code`: 版本代码
- `downloads`: 下载链接映射 (仅根目录文件)
## 🔐 签名信息
所有APK文件均使用多重签名保护:
- ✅ v1 (JAR签名) - 最佳兼容性
- ✅ v2 (APK签名方案v2) - 全文件签名
- ✅ v3 (APK签名方案v3) - 支持密钥轮换
- ✅ v4 (APK签名方案v4) - 增量签名
+9
View File
@@ -0,0 +1,9 @@
{
"name": "3.0.8",
"desc": "XMBOX TV版 v3.0.8 (Android TV/机顶盒专用)\n\n✨ UI优化:\n• 修复按钮点击效果过于明显的问题\n• 统一使用自定义背景替代系统selectableItemBackgroundBorderless\n• 移除Control.Action样式中的文字阴影效果\n• 优化直播页面选择按钮颜色为主题黄色\n• 调整许可协议页面按钮区域上间距为8dp\n• 修复跨类和换源按钮的文字重叠问题\n• 提升整体UI视觉一致性和用户体验\n\n🔧 改进优化:\n• 优化大屏界面体验\n• 提升播放稳定性\n\n📺 专为电视优化:遥控器导航 | 10-foot UI | ARM64/ARMv7",
"code": 308,
"downloads": {
"arm64_v8a": "v3.0.8/leanback-arm64_v8a-v3.0.8.apk",
"armeabi_v7a": "v3.0.8/leanback-armeabi_v7a-v3.0.8.apk"
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"name": "3.0.8",
"desc": "XMBOX 手机版 v3.0.8\n\n✨ UI优化:\n• 修复按钮点击效果过于明显的问题\n• 统一使用自定义背景替代系统selectableItemBackgroundBorderless\n• 移除Control.Action样式中的文字阴影效果\n• 优化直播页面选择按钮颜色为主题黄色\n• 调整许可协议页面按钮区域上间距为8dp\n• 修复跨类和换源按钮的文字重叠问题\n• 提升整体UI视觉一致性和用户体验\n\n🔧 改进优化:\n• 优化内存使用\n• 提升播放稳定性\n\n📱 支持架构:ARM64-v8a | ARMv7a",
"code": 308,
"downloads": {
"arm64_v8a": "v3.0.8/mobile-arm64_v8a-v3.0.8.apk",
"armeabi_v7a": "v3.0.8/mobile-armeabi_v7a-v3.0.8.apk"
}
}
@@ -0,0 +1,5 @@
{
"name": "3.0.7",
"desc": "XMBOX TV版 v3.0.7 (Android TV/机顶盒专用)\n\n✨ UI优化:\n• 全新自定义开关按钮(黄色/黑色Material Design风格)\n• 优化电量百分比显示(16sp字号,2dp间距)\n• 精简设置页面,隐藏壁纸功能\n\n🔒 安全增强:\n• 启用v1/v2/v3/v4多重签名保护\n• 提升应用安全性和兼容性\n\n🔧 改进优化:\n• 修复设置页面崩溃问题\n• 优化大屏界面体验\n• 提升播放稳定性\n\n📺 专为电视优化:遥控器导航 | 10-foot UI | ARM64/ARMv7",
"code": 307
}
@@ -0,0 +1,5 @@
{
"name": "3.0.7",
"desc": "XMBOX 手机版 v3.0.7\n\n✨ UI优化:\n• 全新自定义开关按钮(黄色/黑色Material Design风格)\n• 优化电量百分比显示(16sp字号,2dp间距)\n• 精简设置页面,隐藏壁纸功能\n\n🔒 安全增强:\n• 启用v1/v2/v3/v4多重签名保护\n• 提升应用安全性和兼容性\n\n🔧 改进优化:\n• 修复设置页面崩溃问题\n• 优化内存使用\n• 提升播放稳定性\n\n📱 支持架构:ARM64-v8a | ARMv7a",
"code": 307
}
+62
View File
@@ -0,0 +1,62 @@
# XMBOX Release Files
## 📁 文件结构
```
apk/release/
├── mobile.json # 最新版本信息 (手机版)
├── leanback.json # 最新版本信息 (TV版)
├── v3.0.7/ # v3.0.7版本文件
│ ├── mobile.json # v3.0.7版本信息
│ ├── leanback.json # v3.0.7版本信息
│ ├── mobile-arm64_v8a.apk
│ ├── mobile-armeabi_v7a.apk
│ ├── leanback-arm64_v8a.apk
│ └── leanback-armeabi_v7a.apk
└── v3.0.8/ # v3.0.8版本文件
├── mobile.json # v3.0.8版本信息
├── leanback.json # v3.0.8版本信息
├── mobile-arm64_v8a-v3.0.8.apk
├── mobile-armeabi_v7a-v3.0.8.apk
├── leanback-arm64_v8a-v3.0.8.apk
└── leanback-armeabi_v7a-v3.0.8.apk
```
## 📱 版本说明
### v3.0.8 (最新版本)
- **发布时间**: 2025-10-14
- **版本代码**: 308
- **主要更新**: UI交互体验全面优化
### v3.0.7
- **发布时间**: 2025-09-26
- **版本代码**: 307
- **主要更新**: 全面优化稳定性和用户体验
## 🔗 下载链接
### 最新版本 (v3.0.8)
- **手机版 ARM64**: [mobile-arm64_v8a-v3.0.8.apk](v3.0.8/mobile-arm64_v8a-v3.0.8.apk)
- **手机版 ARMv7**: [mobile-armeabi_v7a-v3.0.8.apk](v3.0.8/mobile-armeabi_v7a-v3.0.8.apk)
- **TV版 ARM64**: [leanback-arm64_v8a-v3.0.8.apk](v3.0.8/leanback-arm64_v8a-v3.0.8.apk)
- **TV版 ARMv7**: [leanback-armeabi_v7a-v3.0.8.apk](v3.0.8/leanback-armeabi_v7a-v3.0.8.apk)
### 历史版本
- **v3.0.7**: [查看v3.0.7版本文件](v3.0.7/)
## 📋 版本信息
每个版本目录都包含对应的JSON配置文件,包含:
- `name`: 版本号
- `desc`: 版本描述和更新内容
- `code`: 版本代码
- `downloads`: 下载链接映射 (仅根目录文件)
## 🔐 签名信息
所有APK文件均使用多重签名保护:
- ✅ v1 (JAR签名) - 最佳兼容性
- ✅ v2 (APK签名方案v2) - 全文件签名
- ✅ v3 (APK签名方案v3) - 支持密钥轮换
- ✅ v4 (APK签名方案v4) - 增量签名
+9
View File
@@ -0,0 +1,9 @@
{
"name": "3.0.9",
"desc": "XMBOX TV版 v3.0.9 (Android TV/机顶盒专用)\n\n✨ 新功能:\n• 新增直播开关控制功能,可隐藏/显示直播tab\n• 新增实时倍速显示功能,播放控制对话框显示当前倍速\n• 优化源管理模块间距动态调整\n\n🎨 UI优化:\n• 滑杆圆球大小优化至20dp直径,提升操作体验\n• 改进滑杆刻度显示,非激活轨道显示刻度\n• 增强播放进度条动态大小调整功能\n• 修复播放进度条圆球跳回问题\n• 完善直播开关逻辑和UI交互\n\n🔧 改进优化:\n• 优化大屏界面体验\n• 提升播放稳定性\n• 增强UI交互体验\n\n📺 专为电视优化:遥控器导航 | 10-foot UI | ARM64/ARMv7",
"code": 309,
"downloads": {
"arm64_v8a": "v3.0.9/leanback-arm64_v8a-v3.0.9.apk",
"armeabi_v7a": "v3.0.9/leanback-armeabi_v7a-v3.0.9.apk"
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"name": "3.0.9",
"desc": "XMBOX 手机版 v3.0.9\n\n✨ 新功能:\n• 新增直播开关控制功能,可隐藏/显示直播tab\n• 新增实时倍速显示功能,播放控制对话框显示当前倍速\n• 优化源管理模块间距动态调整\n\n🎨 UI优化:\n• 滑杆圆球大小优化至20dp直径,提升操作体验\n• 改进滑杆刻度显示,非激活轨道显示刻度\n• 增强播放进度条动态大小调整功能\n• 修复播放进度条圆球跳回问题\n• 完善直播开关逻辑和UI交互\n\n🔧 改进优化:\n• 优化内存使用\n• 提升播放稳定性\n• 增强UI交互体验\n\n📱 支持架构:ARM64-v8a | ARMv7a",
"code": 309,
"downloads": {
"arm64_v8a": "v3.0.9/mobile-arm64_v8a-v3.0.9.apk",
"armeabi_v7a": "v3.0.9/mobile-armeabi_v7a-v3.0.9.apk"
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"name": "3.0.7",
"desc": "XMBOX TV版 v3.0.7 (Android TV/机顶盒专用)\n\n✨ UI优化:\n• 全新自定义开关按钮(黄色/黑色Material Design风格)\n• 优化电量百分比显示(16sp字号,2dp间距)\n• 精简设置页面,隐藏壁纸功能\n\n🔒 安全增强:\n• 启用v1/v2/v3/v4多重签名保护\n• 提升应用安全性和兼容性\n\n🔧 改进优化:\n• 修复设置页面崩溃问题\n• 优化大屏界面体验\n• 提升播放稳定性\n\n📺 专为电视优化:遥控器导航 | 10-foot UI | ARM64/ARMv7",
"code": 307
}
+5
View File
@@ -0,0 +1,5 @@
{
"name": "3.0.7",
"desc": "XMBOX 手机版 v3.0.7\n\n✨ UI优化:\n• 全新自定义开关按钮(黄色/黑色Material Design风格)\n• 优化电量百分比显示(16sp字号,2dp间距)\n• 精简设置页面,隐藏壁纸功能\n\n🔒 安全增强:\n• 启用v1/v2/v3/v4多重签名保护\n• 提升应用安全性和兼容性\n\n🔧 改进优化:\n• 修复设置页面崩溃问题\n• 优化内存使用\n• 提升播放稳定性\n\n📱 支持架构:ARM64-v8a | ARMv7a",
"code": 307
}
+34 -16
View File
@@ -5,7 +5,7 @@ plugins {
android {
namespace 'com.fongmi.android.tv'
compileSdk 35
compileSdk 36
flavorDimensions = ["mode", "abi"]
signingConfigs {
@@ -14,16 +14,21 @@ android {
storePassword "xmbox123"
keyAlias "xmbox"
keyPassword "xmbox123"
// 同时启用v1、v2、v3、v4签名以确保最佳兼容性
enableV1Signing true
enableV2Signing true
enableV3Signing true
enableV4Signing true
}
}
defaultConfig {
applicationId "com.fongmi.android.tv"
minSdk 21
minSdk 24
//noinspection ExpiredTargetSdkVersion
targetSdk 28
versionCode 300
versionName "3.0.0"
versionCode 310
versionName "3.1.0"
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString(), "eventBusIndex": "com.fongmi.android.tv.event.EventIndex"]
@@ -67,6 +72,17 @@ android {
exclude 'META-INF/beans.xml'
exclude 'META-INF/versions/9/OSGI-INF/MANIFEST.MF'
}
jniLibs {
// 解决重复的JNI库问题,只保留第一个找到的
pickFirsts += [
'**/libavcodec.so',
'**/libavutil.so',
'**/libmedia3ext.so',
'**/libquickjs-android-wrapper.so',
'**/libswresample.so',
'**/libswscale.so'
]
}
}
android.applicationVariants.configureEach { variant ->
@@ -87,15 +103,15 @@ android {
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.aar"])
implementation project(':catvod')
//implementation project(':chaquo')
// implementation project(':chaquo') // 移除Python支持减少8-10MB体积
implementation project(':quickjs')
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.media:media:1.7.0'
@@ -114,9 +130,9 @@ dependencies {
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:' + media3Version
implementation 'androidx.media3:media3-extractor:' + media3Version
implementation 'androidx.media3:media3-ui:' + media3Version
implementation 'androidx.room:room-runtime:2.7.1'
implementation 'androidx.room:room-runtime:2.6.1'
implementation 'cat.ereza:customactivityoncrash:2.4.0'
implementation('com.github.anilbeesetti.nextlib:nextlib-media3ext:0.8.4') { exclude group: 'androidx.media3' }
implementation 'io.github.anilbeesetti:nextlib-media3ext:1.8.0-0.9.0'
implementation 'com.github.bassaer:materialdesigncolors:1.0.0'
implementation 'com.github.bumptech.glide:glide:4.16.0'
implementation 'com.github.bumptech.glide:annotations:4.16.0'
@@ -124,12 +140,12 @@ dependencies {
implementation 'com.github.bumptech.glide:okhttp3-integration:4.16.0'
implementation 'com.github.jahirfiquitiva:TextDrawable:1.0.3'
implementation 'com.github.thegrizzlylabs:sardine-android:0.9'
implementation 'com.github.teamnewpipe:NewPipeExtractor:v0.24.6'
implementation 'com.github.teamnewpipe:NewPipeExtractor:v0.24.8'
implementation 'com.google.android.material:material:1.12.0'
implementation 'com.google.zxing:core:3.5.3'
implementation 'com.guolindev.permissionx:permissionx:1.8.0'
implementation 'com.hierynomus:smbj:0.14.0'
implementation 'io.antmedia:rtmp-client:3.2.0'
implementation 'com.guolindev.permissionx:permissionx:1.7.1'
implementation 'com.hierynomus:smbj:0.13.0'
implementation 'io.antmedia:rtmp-client:3.1.0'
implementation 'javax.servlet:javax.servlet-api:3.1.0'
implementation 'org.aomedia.avif.android:avif:1.1.1.14d8e3c4'
implementation 'org.eclipse.jetty:jetty-client:8.1.21.v20160908'
@@ -146,8 +162,10 @@ dependencies {
mobileImplementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
mobileImplementation 'com.google.android.flexbox:flexbox:3.0.0'
mobileImplementation('com.journeyapps:zxing-android-embedded:4.3.0') { transitive = false }
annotationProcessor 'androidx.room:room-compiler:2.7.1'
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
annotationProcessor 'androidx.room:room-compiler:2.6.1'
// annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.3.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.4'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.5'
implementation 'io.noties.markwon:core:4.6.2'
implementation 'com.airbnb.android:lottie:5.2.0'
}
+12 -3
View File
@@ -59,12 +59,14 @@
-keep class fi.iki.elonen.** { *; }
# NewPipeExtractor
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
-keep class javax.script.** { *; }
-keep class jdk.dynalink.** { *; }
-keep class org.mozilla.javascript.* { *; }
-keep class org.mozilla.javascript.** { *; }
-keep class org.mozilla.javascript.engine.** { *; }
-keep class javax.script.** { *; }
-keep class jdk.dynalink.** { *; }
-keep class org.mozilla.classfile.ClassFileWriter
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
-keep class org.schabi.newpipe.extractor.services.youtube.protos.** { *; }
-dontwarn org.mozilla.javascript.JavaToJSONConverters
-dontwarn org.mozilla.javascript.tools.**
-dontwarn javax.script.**
@@ -90,5 +92,12 @@
-keep class com.sun.jna.** { *; }
-keep class com.east.android.zlive.** { *; }
# Media3 DefaultTimeBar - 保护反射访问的字段
-keep class androidx.media3.ui.DefaultTimeBar {
int barHeight;
int scrubberEnabledSize;
int scrubberDisabledSize;
}
# Zxing
-keep class com.google.zxing.** { *; }
@@ -1,11 +1,14 @@
package com.fongmi.android.tv;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import androidx.appcompat.app.AlertDialog;
import com.fongmi.android.tv.App;
import com.fongmi.android.tv.databinding.DialogUpdateBinding;
import com.fongmi.android.tv.utils.Download;
import com.fongmi.android.tv.utils.FileUtil;
@@ -13,6 +16,7 @@ import com.fongmi.android.tv.utils.Notify;
import com.fongmi.android.tv.utils.ResUtil;
import com.github.catvod.net.OkHttp;
import com.github.catvod.utils.Github;
import com.github.catvod.utils.Logger;
import com.github.catvod.utils.Path;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@@ -27,9 +31,11 @@ public class Updater implements Download.Callback {
private final Download download;
private AlertDialog dialog;
private boolean dev;
private boolean forceCheck; // 是否为手动检查
private String latestVersion; // 存储检测到的最新版本
private File getFile() {
return Path.cache("update.apk");
return Path.root("Download", "XMBOX-update.apk");
}
private String getJson() {
@@ -37,6 +43,26 @@ public class Updater implements Download.Callback {
}
private String getApk() {
// 使用JSON中指定的具体下载路径
try {
String response = OkHttp.string(getJson());
JSONObject object = new JSONObject(response);
JSONObject downloads = object.optJSONObject("downloads");
if (downloads != null) {
String abi = BuildConfig.FLAVOR_abi;
String downloadPath = downloads.optString(abi);
if (!downloadPath.isEmpty()) {
// 直接构建完整URL,不通过Github.getApk()避免重复添加路径
String baseUrl = Github.useCnMirror() ?
"https://gitee.com/ochenoktochen/XMBOX-Release/raw/main" :
"https://raw.githubusercontent.com/Tosencen/XMBOX-Release/main";
return baseUrl + "/apk/" + (dev ? "dev" : "release") + "/" + downloadPath;
}
}
} catch (Exception e) {
Logger.e("Failed to get download path from JSON: " + e.getMessage());
}
// 回退到原来的方式
return Github.getApk(dev, BuildConfig.FLAVOR_mode + "-" + BuildConfig.FLAVOR_abi);
}
@@ -46,11 +72,13 @@ public class Updater implements Download.Callback {
public Updater() {
this.download = Download.create(getApk(), getFile(), this);
this.forceCheck = false;
}
public Updater force() {
Notify.show(R.string.update_check);
Setting.putUpdate(true);
this.forceCheck = true; // 标记为手动检查
return this;
}
@@ -79,13 +107,83 @@ public class Updater implements Download.Callback {
private void doInBackground(Activity activity) {
try {
JSONObject object = new JSONObject(OkHttp.string(getJson()));
String name = object.optString("name");
String desc = object.optString("desc");
int code = object.optInt("code");
if (need(code, name)) App.post(() -> show(activity, name, desc));
// 直接使用GitHub Releases API检测最新版本
String releasesUrl = "https://api.github.com/repos/Tosencen/XMBOX/releases/latest";
String response = OkHttp.string(releasesUrl);
// 检查响应是否包含错误信息
if (response.contains("rate limit exceeded")) {
if (forceCheck) {
App.post(() -> Notify.show("检查更新失败:API请求过于频繁,请稍后重试"));
}
return;
}
if (response.contains("Not Found") || response.contains("404")) {
if (forceCheck) {
App.post(() -> Notify.show("检查更新失败:更新服务暂时不可用"));
}
return;
}
JSONObject release = new JSONObject(response);
String tagName = release.optString("tag_name");
String body = release.optString("body");
// 提取版本号(去掉v前缀)
String version = tagName.startsWith("v") ? tagName.substring(1) : tagName;
if (needUpdate(version)) {
this.latestVersion = version; // 保存最新版本号
App.post(() -> show(activity, version, body));
} else {
if (forceCheck) {
App.post(() -> Notify.show("已是最新版本 " + version));
}
}
} catch (Exception e) {
e.printStackTrace();
if (forceCheck) {
App.post(() -> {
String errorMsg = "检查更新失败";
if (e.getMessage() != null && e.getMessage().contains("rate limit")) {
errorMsg = "检查更新失败:请求过于频繁,请稍后重试";
} else if (e.getMessage() != null && e.getMessage().contains("Not Found")) {
errorMsg = "检查更新失败:更新服务暂时不可用";
} else {
errorMsg = "检查更新失败,请稍后重试";
}
Notify.show(errorMsg);
});
}
}
}
private boolean needUpdate(String remoteVersion) {
if (!Setting.getUpdate()) return false;
try {
// 简单的版本号比较,假设版本格式为 x.y.z
String[] remoteParts = remoteVersion.split("\\.");
String[] localParts = BuildConfig.VERSION_NAME.split("\\.");
// 确保两个版本号都有足够的段
int maxLength = Math.max(remoteParts.length, localParts.length);
for (int i = 0; i < maxLength; i++) {
int remotePart = i < remoteParts.length ? Integer.parseInt(remoteParts[i]) : 0;
int localPart = i < localParts.length ? Integer.parseInt(localParts[i]) : 0;
if (remotePart > localPart) {
return true;
} else if (remotePart < localPart) {
return false;
}
}
return false; // 版本相同
} catch (Exception e) {
Logger.e("Updater: Version comparison error: " + e.getMessage());
return false;
}
}
@@ -109,8 +207,31 @@ public class Updater implements Download.Callback {
}
private void confirm(View view) {
binding.confirm.setEnabled(false);
download.start();
// 跳转到具体版本的GitHub Releases页面
try {
String url = "https://github.com/Tosencen/XMBOX/releases/tag/v" + latestVersion;
Logger.d("Updater: Attempting to open URL: " + url);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// 检查是否有应用可以处理这个Intent
if (intent.resolveActivity(App.get().getPackageManager()) != null) {
App.get().startActivity(intent);
Logger.d("Updater: Successfully started browser intent");
dismiss();
} else {
Logger.e("Updater: No app can handle the URL");
Notify.show("没有找到可以打开链接的应用,请手动访问GitHub下载");
dismiss();
}
} catch (Exception e) {
Logger.e("Updater: Failed to open GitHub releases page: " + e.getMessage());
e.printStackTrace();
Notify.show("无法打开更新页面,请手动访问GitHub下载");
dismiss();
}
}
private void dismiss() {
@@ -1,5 +1,9 @@
package com.fongmi.android.tv.ui.activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.viewbinding.ViewBinding;
@@ -14,23 +18,38 @@ import cat.ereza.customactivityoncrash.CustomActivityOnCrash;
public class CrashActivity extends BaseActivity {
private ActivityCrashBinding mBinding;
private String errorDetails;
@Override
protected ViewBinding getBinding() {
return mBinding = ActivityCrashBinding.inflate(getLayoutInflater());
}
@Override
protected void initView() {
errorDetails = CustomActivityOnCrash.getAllErrorDetailsFromIntent(this, getIntent());
mBinding.error.setText(errorDetails);
}
@Override
protected void initEvent() {
mBinding.copy.setOnClickListener(v -> showError());
mBinding.copy.setOnClickListener(v -> copyErrorToClipboard());
mBinding.restart.setOnClickListener(v -> CustomActivityOnCrash.restartApplication(this, Objects.requireNonNull(CustomActivityOnCrash.getConfigFromIntent(getIntent()))));
}
private void copyErrorToClipboard() {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(getString(R.string.crash_details_title), errorDetails);
clipboard.setPrimaryClip(clip);
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show();
showError();
}
private void showError() {
new AlertDialog.Builder(this)
.setTitle(R.string.crash_details_title)
.setMessage(CustomActivityOnCrash.getAllErrorDetailsFromIntent(this, getIntent()))
.setMessage(errorDetails)
.setPositiveButton(R.string.crash_details_close, null)
.show();
}
}
}
@@ -47,6 +47,7 @@ import com.fongmi.android.tv.ui.base.BaseActivity;
import com.fongmi.android.tv.ui.custom.CustomRowPresenter;
import com.fongmi.android.tv.ui.custom.CustomSelector;
import com.fongmi.android.tv.ui.custom.CustomTitleView;
import com.fongmi.android.tv.ui.dialog.LastWatchToast;
import com.fongmi.android.tv.ui.dialog.SiteDialog;
import com.fongmi.android.tv.ui.presenter.FuncPresenter;
import com.fongmi.android.tv.ui.presenter.HeaderPresenter;
@@ -252,6 +253,16 @@ public class HomeActivity extends BaseActivity implements CustomTitleView.Listen
if ((items.isEmpty() && exist) || (renew && exist)) mAdapter.removeItems(historyIndex, 1);
if ((!items.isEmpty() && !exist) || (renew && exist)) mAdapter.add(historyIndex, new ListRow(mHistoryAdapter));
mHistoryAdapter.setItems(items, null);
// 显示上次播放弹窗
checkLastWatchDialog(items);
}
private void checkLastWatchDialog(List<History> items) {
if (!items.isEmpty() && App.isAppJustLaunched()) {
App.setAppLaunched();
LastWatchToast.create(this, items.get(0)).show();
}
}
private void setHistoryDelete(boolean delete) {
@@ -99,8 +99,17 @@ public class SettingActivity extends BaseActivity implements ConfigCallback, Sit
mBinding.dohText.setText(getDohList()[getDohIndex()]);
mBinding.proxyText.setText(getProxy(Setting.getProxy()));
mBinding.incognitoText.setText(getSwitch(Setting.isIncognito()));
mBinding.liveTabVisibleText.setText(getSwitch(Setting.isLiveTabVisible()));
mBinding.sizeText.setText((size = ResUtil.getStringArray(R.array.select_size))[Setting.getSize()]);
mBinding.qualityText.setText((quality = ResUtil.getStringArray(R.array.select_quality))[Setting.getQuality()]);
setLiveSettingsVisibility();
}
private void setLiveSettingsVisibility() {
boolean isLiveTabVisible = !Setting.isLiveTabVisible(); // 注意:这里取反,因为开关是"隐藏直播"
mBinding.live.setVisibility(isLiveTabVisible ? View.VISIBLE : View.GONE);
mBinding.liveHome.setVisibility(isLiveTabVisible ? View.VISIBLE : View.GONE);
mBinding.liveHistory.setVisibility(isLiveTabVisible ? View.VISIBLE : View.GONE);
}
private void setCacheText() {
@@ -134,6 +143,7 @@ public class SettingActivity extends BaseActivity implements ConfigCallback, Sit
mBinding.wallDefault.setOnClickListener(this::setWallDefault);
mBinding.wallRefresh.setOnClickListener(this::setWallRefresh);
mBinding.incognito.setOnClickListener(this::setIncognito);
mBinding.liveTabVisible.setOnClickListener(this::setLiveTabVisible);
mBinding.quality.setOnClickListener(this::setQuality);
mBinding.size.setOnClickListener(this::setSize);
mBinding.doh.setOnClickListener(this::setDoh);
@@ -304,6 +314,15 @@ public class SettingActivity extends BaseActivity implements ConfigCallback, Sit
mBinding.incognitoText.setText(getSwitch(Setting.isIncognito()));
}
private void setLiveTabVisible(View view) {
Setting.putLiveTabVisible(!Setting.isLiveTabVisible());
mBinding.liveTabVisibleText.setText(getSwitch(Setting.isLiveTabVisible()));
// 发送刷新事件,通知主界面更新导航栏
RefreshEvent.config();
// 更新直播设置项的可见性
setLiveSettingsVisibility();
}
private void setQuality(View view) {
int index = Setting.getQuality();
Setting.putQuality(index = index == quality.length - 1 ? 0 : ++index);
@@ -20,6 +20,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.ItemBridgeAdapter;
@@ -427,7 +428,10 @@ public class VideoActivity extends BaseActivity implements CustomKeyDownVod.List
private void setDetail(Result result) {
if (result.getList().isEmpty()) setEmpty(result.hasMsg());
else setDetail(result.getList().get(0));
Notify.show(result.getMsg());
// 只在有错误或重要消息时显示提示
if (result.hasMsg() && result.getList().isEmpty()) {
Notify.show(result.getMsg());
}
}
private void setEmpty(boolean finish) {
@@ -481,7 +485,7 @@ public class VideoActivity extends BaseActivity implements CustomKeyDownVod.List
private void setText(TextView view, int resId, String text) {
view.setText(getSpan(resId, text), TextView.BufferType.SPANNABLE);
view.setVisibility(text.isEmpty() ? View.GONE : View.VISIBLE);
view.setLinkTextColor(MDColor.YELLOW_500);
view.setLinkTextColor(ContextCompat.getColor(view.getContext(), R.color.primary));
CustomMovement.bind(view);
view.setTag(text);
}
@@ -1224,7 +1228,10 @@ public class VideoActivity extends BaseActivity implements CustomKeyDownVod.List
private void nextSite() {
if (mQuickAdapter.size() == 0) return;
Vod item = (Vod) mQuickAdapter.get(0);
Notify.show(getString(R.string.play_switch_site, item.getSiteName()));
// 只在真正需要切换时显示提示(即当前站点已经失败的情况下)
if (mBroken.contains(getId())) {
Notify.show(getString(R.string.play_switch_site, item.getSiteName()));
}
mQuickAdapter.removeItems(0, 1);
mBroken.add(getId());
setInitAuto(false);
@@ -5,7 +5,9 @@ import android.view.LayoutInflater;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import com.fongmi.android.tv.R;
import com.fongmi.android.tv.databinding.DialogDescBinding;
import com.fongmi.android.tv.ui.custom.CustomMovement;
import com.github.bassaer.library.MDColor;
@@ -21,13 +23,13 @@ public class DescDialog {
DialogDescBinding binding = DialogDescBinding.inflate(LayoutInflater.from(activity));
AlertDialog dialog = new MaterialAlertDialogBuilder(activity).setView(binding.getRoot()).create();
dialog.getWindow().setDimAmount(0);
initView(binding.text, desc);
initView(binding.text, desc, activity);
dialog.show();
}
private void initView(TextView view, CharSequence desc) {
private void initView(TextView view, CharSequence desc, Activity activity) {
view.setText(desc, TextView.BufferType.SPANNABLE);
view.setLinkTextColor(MDColor.BLUE_500);
view.setLinkTextColor(ContextCompat.getColor(activity, R.color.primary));
CustomMovement.bind(view);
}
}
@@ -55,6 +55,7 @@ public class SiteDialog implements SiteAdapter.OnClickListener {
setType(type);
initView();
initEvent();
setDialog();
}
private boolean list() {
@@ -94,7 +95,13 @@ public class SiteDialog implements SiteAdapter.OnClickListener {
if (decoration != null) binding.recycler.removeItemDecoration(decoration);
binding.recycler.addItemDecoration(decoration = new SpaceItemDecoration(getCount(), 16));
binding.recycler.setLayoutManager(new GridLayoutManager(dialog.getContext(), getCount()));
if (!binding.mode.hasFocus()) binding.recycler.post(() -> binding.recycler.scrollToPosition(VodConfig.getHomeIndex()));
if (!binding.mode.hasFocus()) {
binding.recycler.post(() -> {
binding.recycler.scrollToPosition(VodConfig.getHomeIndex());
// 请求焦点,确保选中项保持高亮状态
binding.recycler.post(() -> binding.recycler.requestFocus());
});
}
}
private void setDialog() {
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/black" android:state_focused="true" />
<item android:color="@color/black" android:state_activated="true" />
<item android:color="@color/black" android:state_selected="true" />
<item android:color="@color/black" android:state_pressed="true" />
<item android:color="@color/white" />
</selector>
+2 -2
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/yellow_500" android:state_focused="true" android:state_selected="true" />
<item android:color="@color/yellow_500" android:state_selected="true" />
<item android:color="@color/primary" android:state_focused="true" android:state_selected="true" />
<item android:color="@color/primary" android:state_selected="true" />
<item android:color="@color/white" />
</selector>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/primary" android:state_activated="true" />
<item android:color="@color/white" />
</selector>
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/white" android:state_focused="true" android:state_selected="true" />
<item android:color="@color/green_400" android:state_selected="true" />
<item android:color="@color/primary" android:state_selected="true" />
<item android:color="@color/white" />
</selector>
@@ -2,7 +2,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/white_90" />
<solid android:color="@color/black_90" />
<corners
android:topLeftRadius="12dp"
@@ -2,7 +2,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/white_90" />
<solid android:color="@color/black_80" />
<corners
android:topLeftRadius="8dp"
@@ -2,6 +2,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/green_400" />
<solid android:color="@color/primary" />
</shape>
@@ -4,7 +4,7 @@
<solid android:color="@color/black_20" />
<corners android:radius="4dp" />
<corners android:radius="12dp" />
<padding
android:bottom="8dp"
@@ -2,7 +2,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/grey_600" />
<solid android:color="@color/primary" />
<corners android:radius="4dp" />
@@ -2,7 +2,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/grey_800" />
<solid android:color="@color/primary" />
<corners android:radius="4dp" />
@@ -2,7 +2,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/grey_600" />
<solid android:color="@color/black_60" />
<corners android:radius="4dp" />
@@ -2,7 +2,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/black_40" />
<solid android:color="@color/black_90" />
<corners android:radius="8dp" />
@@ -265,6 +265,36 @@
</LinearLayout>
<LinearLayout
android:id="@+id/liveTabVisible"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/selector_item"
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusDown="@id/quality"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/setting_live_tab_visible"
android:textColor="@color/white"
android:textSize="18sp" />
<TextView
android:id="@+id/liveTabVisibleText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:textColor="@color/white"
android:textSize="18sp"
tools:text="On" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -16,7 +16,7 @@
android:focusableInTouchMode="true"
android:gravity="center"
android:singleLine="true"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="18sp"
tools:text="https://fongmi.github.io/cat.json" />
@@ -9,6 +9,6 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:singleLine="true"
android:textColor="@color/text"
android:textColor="@color/button_text"
android:textSize="14sp"
tools:text="愛奇異彈幕" />
+1 -1
View File
@@ -9,6 +9,6 @@
android:focusableInTouchMode="true"
android:gravity="center"
android:singleLine="true"
android:textColor="@color/text"
android:textColor="@color/button_text"
android:textSize="18sp"
tools:text="Google" />
@@ -12,6 +12,6 @@
android:nextFocusUp="@id/flag"
android:nextFocusDown="@id/array"
android:singleLine="true"
android:textColor="@color/text"
android:textColor="@color/episode_text"
android:textSize="16sp"
tools:text="20" />
+1 -1
View File
@@ -16,7 +16,7 @@
android:focusableInTouchMode="true"
android:gravity="center"
android:singleLine="true"
android:textColor="@color/text"
android:textColor="@color/button_text"
android:textSize="18sp"
tools:text="https://fongmi.github.io/live.json" />
@@ -8,6 +8,6 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"
android:textColor="@color/text"
android:textColor="@color/button_text"
android:textSize="14sp"
tools:text="解析" />
@@ -24,7 +24,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:singleLine="true"
android:textColor="@color/green_a_400"
android:textColor="@color/primary"
android:textSize="14sp"
tools:text="泥巴"
tools:visibility="visible" />
@@ -16,7 +16,7 @@
android:focusableInTouchMode="true"
android:gravity="center"
android:singleLine="true"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="18sp"
tools:text="tv-2024-12-26.bk.gz" />
+2 -1
View File
@@ -17,8 +17,9 @@
android:ellipsize="marquee"
android:gravity="center"
android:singleLine="true"
android:textColor="@color/text"
android:textColor="@color/button_text"
android:textSize="18sp"
android:duplicateParentState="true"
tools:text="泥巴" />
<com.google.android.material.checkbox.MaterialCheckBox
@@ -9,6 +9,6 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:singleLine="true"
android:textColor="@color/text"
android:textColor="@color/button_text"
android:textSize="14sp"
tools:text="中文、哥斯拉.srt" />
@@ -3,6 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/black_90"
android:padding="48dp">
<com.google.android.material.slider.Slider
@@ -12,9 +13,10 @@
android:stepSize="1"
android:valueFrom="1"
android:valueTo="10"
app:thumbColor="@color/yellow_500"
app:thumbColor="@color/primary"
app:thumbRadius="9dp"
app:tickVisible="false"
app:trackColorActive="@color/yellow_500"
app:trackColorInactive="@color/yellow_50" />
app:trackColorActive="@color/primary"
app:trackColorInactive="@color/white_30" />
</FrameLayout>
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/black_90"
android:padding="16dp">
<ImageView
@@ -23,7 +24,7 @@
android:maxLines="3"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:textColor="@color/grey_700"
android:textColor="@color/white"
android:textSize="18sp"
tools:text="@string/push_info" />
@@ -41,11 +42,13 @@
android:layout_alignStart="@+id/info"
android:layout_marginBottom="10dp"
android:hint="@string/dialog_config_hint"
android:textColorHint="@color/white_50"
android:imeOptions="actionDone"
android:importantForAutofill="no"
android:inputType="text"
android:nextFocusDown="@id/positive"
android:singleLine="true"
android:textColor="@color/white"
android:textSize="18sp" />
<LinearLayout
@@ -68,7 +71,7 @@
android:gravity="center"
android:singleLine="true"
android:text="@string/setting_choose"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp" />
<TextView
@@ -83,7 +86,7 @@
android:gravity="center"
android:singleLine="true"
android:text="@string/dialog_positive"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp" />
<TextView
@@ -97,7 +100,7 @@
android:gravity="center"
android:singleLine="true"
android:text="@string/dialog_negative"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp" />
</LinearLayout>
@@ -4,6 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/black_90"
android:orientation="vertical">
<LinearLayout
@@ -19,7 +20,7 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/danmaku_select"
android:textColor="@color/grey_900"
android:textColor="@color/white"
android:textSize="16sp" />
<ImageView
+2 -1
View File
@@ -2,6 +2,7 @@
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black_90"
android:fillViewport="true">
<TextView
@@ -11,7 +12,7 @@
android:letterSpacing="0.05"
android:lineSpacingExtra="8dp"
android:padding="16dp"
android:textColor="@color/grey_800"
android:textColor="@color/white"
android:textSize="16sp" />
</androidx.core.widget.NestedScrollView>
@@ -5,6 +5,7 @@
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black_90"
android:padding="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:maxHeight="296dp"
@@ -4,6 +4,7 @@
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/black_90"
android:padding="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:maxHeight="352dp" />
@@ -5,6 +5,7 @@
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black_90"
android:padding="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:maxHeight="296dp"
@@ -2,6 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/black_90"
android:gravity="center"
android:orientation="horizontal"
android:padding="16dp">
+5 -2
View File
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/black_90"
android:padding="16dp">
<ImageView
@@ -35,11 +36,13 @@
android:layout_alignStart="@+id/info"
android:layout_marginBottom="10dp"
android:hint="socks5://127.0.0.1:9978"
android:textColorHint="@color/white_50"
android:imeOptions="actionDone"
android:importantForAutofill="no"
android:inputType="text"
android:nextFocusDown="@id/positive"
android:singleLine="true"
android:textColor="@color/white"
android:textSize="18sp" />
<LinearLayout
@@ -62,7 +65,7 @@
android:gravity="center"
android:singleLine="true"
android:text="@string/dialog_positive"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp" />
<TextView
@@ -76,7 +79,7 @@
android:gravity="center"
android:singleLine="true"
android:text="@string/dialog_negative"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp" />
</LinearLayout>
@@ -4,6 +4,7 @@
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/black_90"
android:padding="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:maxHeight="352dp" />
@@ -4,6 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/black_90"
android:orientation="horizontal"
android:padding="16dp">
+8 -4
View File
@@ -3,6 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/black_90"
android:padding="48dp">
<com.google.android.material.slider.Slider
@@ -12,9 +13,12 @@
android:stepSize="0.5"
android:valueFrom="2"
android:valueTo="5"
app:thumbColor="@color/yellow_500"
app:tickVisible="false"
app:trackColorActive="@color/yellow_500"
app:trackColorInactive="@color/yellow_50" />
app:thumbColor="@color/primary"
app:thumbRadius="10dp"
app:tickVisible="true"
app:tickColor="@color/black_50"
app:trackColorActive="@color/primary"
app:trackColorInactive="@color/white_20"
app:trackHeight="4dp" />
</FrameLayout>
@@ -2,6 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/black_90"
android:orientation="horizontal"
android:padding="16dp">
+2 -1
View File
@@ -4,6 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/black_90"
android:orientation="vertical">
<LinearLayout
@@ -18,7 +19,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="@color/grey_900"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="選擇字幕" />
+5 -2
View File
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/black_90"
android:padding="16dp">
<ImageView
@@ -35,11 +36,13 @@
android:layout_alignStart="@+id/info"
android:layout_marginBottom="10dp"
android:hint="@string/player_ua"
android:textColorHint="@color/white_50"
android:imeOptions="actionDone"
android:importantForAutofill="no"
android:inputType="text"
android:nextFocusDown="@id/positive"
android:singleLine="true"
android:textColor="@color/white"
android:textSize="18sp" />
<LinearLayout
@@ -62,7 +65,7 @@
android:gravity="center"
android:singleLine="true"
android:text="@string/dialog_positive"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp" />
<TextView
@@ -76,7 +79,7 @@
android:gravity="center"
android:singleLine="true"
android:text="@string/dialog_negative"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp" />
</LinearLayout>
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/black_90"
android:orientation="vertical"
android:padding="24dp">
@@ -11,7 +12,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:letterSpacing="0.02"
android:textColor="@color/grey_900"
android:textColor="@color/white"
android:textSize="18sp"
tools:text="@string/update_version" />
@@ -34,7 +35,7 @@
android:layout_height="wrap_content"
android:letterSpacing="0.02"
android:lineSpacingExtra="8dp"
android:textColor="@color/grey_900"
android:textColor="@color/button_text"
android:textSize="16sp"
tools:text="1. 新增 ffmpeg 音頻軟解\n2. 詳情頁新增分詞快搜\n3. 修復搜尋閃退問題\n4. 設定支援渲染切換" />
@@ -56,7 +57,7 @@
android:focusableInTouchMode="true"
android:gravity="center"
android:text="@string/update_confirm"
android:textColor="@color/white" />
android:textColor="@color/button_text" />
<TextView
android:id="@+id/cancel"
@@ -68,7 +69,7 @@
android:focusableInTouchMode="true"
android:gravity="center"
android:text="@string/dialog_negative"
android:textColor="@color/white" />
android:textColor="@color/button_text" />
</LinearLayout>
</LinearLayout>
@@ -34,7 +34,7 @@
android:focusableInTouchMode="true"
android:nextFocusLeft="@id/video"
android:nextFocusDown="@id/timeBar"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
tools:text="刷新" />
@@ -48,7 +48,7 @@
android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar"
android:text="@string/play_exo"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp" />
<TextView
@@ -60,7 +60,7 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
tools:text="硬解" />
@@ -73,7 +73,7 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
tools:text="1.00" />
@@ -86,7 +86,7 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
tools:text="預設" />
@@ -101,7 +101,7 @@
android:nextFocusDown="@id/timeBar"
android:tag="3"
android:text="@string/play_track_text"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
android:visibility="gone"
tools:visibility="visible" />
@@ -117,7 +117,7 @@
android:nextFocusDown="@id/timeBar"
android:tag="1"
android:text="@string/play_track_audio"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
android:visibility="gone"
tools:visibility="visible" />
@@ -134,7 +134,7 @@
android:nextFocusDown="@id/timeBar"
android:tag="2"
android:text="@string/play_track_video"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
android:visibility="gone"
tools:visibility="visible" />
@@ -26,7 +26,7 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusLeft="@id/change"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
tools:text="首頁" />
@@ -39,7 +39,7 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:text="@string/play"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp" />
<TextView
@@ -51,7 +51,7 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:text="@string/play_exo"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp" />
<TextView
@@ -62,7 +62,7 @@
android:background="@drawable/selector_text"
android:focusable="true"
android:focusableInTouchMode="true"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
tools:text="硬解" />
@@ -74,7 +74,7 @@
android:background="@drawable/selector_text"
android:focusable="true"
android:focusableInTouchMode="true"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
android:visibility="gone"
tools:text="1.00"
@@ -88,7 +88,7 @@
android:background="@drawable/selector_text"
android:focusable="true"
android:focusableInTouchMode="true"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
tools:text="預設" />
@@ -100,7 +100,7 @@
android:background="@drawable/selector_text"
android:focusable="true"
android:focusableInTouchMode="true"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
android:visibility="gone"
tools:text="來源 1"
@@ -116,7 +116,7 @@
android:focusableInTouchMode="true"
android:tag="3"
android:text="@string/play_track_text"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
android:visibility="gone"
tools:visibility="visible" />
@@ -131,7 +131,7 @@
android:focusableInTouchMode="true"
android:tag="1"
android:text="@string/play_track_audio"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
android:visibility="gone"
tools:visibility="visible" />
@@ -146,7 +146,7 @@
android:focusableInTouchMode="true"
android:tag="2"
android:text="@string/play_track_video"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
android:visibility="gone"
tools:visibility="visible" />
@@ -29,6 +29,8 @@
android:nextFocusUp="@id/next"
android:nextFocusDown="@id/timeBar"
app:bar_height="2dp"
app:scrubber_enabled_size="14dp"
app:scrubber_disabled_size="14dp"
app:played_color="#FFEB3B"
app:scrubber_color="#FFEB3B"
app:buffered_color="#80FFEB3B"
@@ -45,7 +45,7 @@
android:nextFocusLeft="@id/loop"
android:nextFocusDown="@id/timeBar"
android:text="@string/play_next"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp" />
<TextView
@@ -58,7 +58,7 @@
android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar"
android:text="@string/play_prev"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp" />
<TextView
@@ -70,7 +70,7 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
tools:text="刷新" />
@@ -84,7 +84,7 @@
android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar"
android:text="@string/play_change"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
tools:text="換源" />
@@ -98,7 +98,7 @@
android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar"
android:text="@string/play_exo"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp" />
<TextView
@@ -110,7 +110,7 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
tools:text="硬解" />
@@ -123,7 +123,7 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
tools:text="1.00" />
@@ -136,7 +136,7 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
tools:text="預設" />
@@ -151,7 +151,7 @@
android:nextFocusDown="@id/timeBar"
android:tag="3"
android:text="@string/play_track_text"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
android:visibility="gone"
tools:visibility="visible" />
@@ -167,7 +167,7 @@
android:nextFocusDown="@id/timeBar"
android:tag="1"
android:text="@string/play_track_audio"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
android:visibility="gone"
tools:visibility="visible" />
@@ -183,7 +183,7 @@
android:nextFocusDown="@id/timeBar"
android:tag="2"
android:text="@string/play_track_video"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
android:visibility="gone"
tools:visibility="visible" />
@@ -197,7 +197,7 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
tools:text="00:00" />
@@ -210,7 +210,7 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar"
android:textColor="@color/white"
android:textColor="@color/button_text"
android:textSize="14sp"
tools:text="00:00" />
+40 -3
View File
@@ -1,7 +1,44 @@
<resources>
<color name="primary">@color/black</color>
<color name="primaryDark">@color/black</color>
<color name="accent">@color/blue_500</color>
<color name="primary">#FFEB3B</color>
<color name="primaryDark">#FDD835</color>
<color name="accent">#FFEB3B</color>
<color name="blue_500">#2196F3</color>
<color name="green_400">#66BB6A</color>
<color name="grey_600">#757575</color>
<!-- Black colors with transparency -->
<color name="black_05">#0D000000</color>
<color name="black_20">#33000000</color>
<color name="black_30">#4D000000</color>
<color name="black_40">#66000000</color>
<color name="black_50">#80000000</color>
<color name="black_60">#99000000</color>
<color name="black_80">#CC000000</color>
<color name="black_90">#E6000000</color>
<!-- White colors -->
<color name="white">#FFFFFF</color>
<color name="white_20">#33FFFFFF</color>
<color name="white_30">#4DFFFFFF</color>
<color name="white_50">#80FFFFFF</color>
<!-- Pure colors -->
<color name="black">#000000</color>
<!-- Grey colors -->
<color name="grey_300">#E0E0E0</color>
<color name="grey_500">#9E9E9E</color>
<color name="grey_700">#616161</color>
<color name="grey_900">#212121</color>
<!-- Text colors -->
<color name="text">#FFFFFF</color>
<!-- UI specific colors -->
<color name="green_a_400">#00E676</color>
<!-- Transparent -->
<color name="transparent">#00000000</color>
</resources>
+2 -1
View File
@@ -10,6 +10,7 @@
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primaryDark</item>
<item name="colorAccent">@color/accent</item>
<item name="colorControlHighlight">@color/primary</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowBackground">@null</item>
<item name="android:windowDisablePreview">true</item>
@@ -25,7 +26,7 @@
<item name="android:paddingBottom">24dp</item>
</style>
<style name="BottomSheetDialog" parent="Theme.MaterialComponents.Light.BottomSheetDialog">
<style name="BottomSheetDialog" parent="Theme.MaterialComponents.DayNight.BottomSheetDialog">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primaryDark</item>
<item name="colorAccent">@color/accent</item>
+1 -1
View File
@@ -5469,7 +5469,7 @@ body[data-weui-theme="dark"] .weui-picker__mask {
}
.weui-primary-loading_brand {
color: #07c160;
color: #FFEB3B;
color: var(--weui-BRAND);
}
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
#version 100
// Copyright 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ES 2 fragment shader that samples from a (non-external) texture with
// uTexSampler, and multiplies its alpha value by uAlphaScale.
precision mediump float;
uniform sampler2D uTexSampler;
uniform float uAlphaScale;
varying vec2 vTexSamplingCoord;
void main() {
vec4 src = texture2D(uTexSampler, vTexSamplingCoord);
gl_FragColor = vec4(src.rgb, src.a * uAlphaScale);
}
@@ -0,0 +1,25 @@
#version 100
// Copyright 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ES 2 fragment shader that samples from a (non-external) texture with
// uTexSampler and copies this to the output.
precision mediump float;
uniform sampler2D uTexSampler;
varying vec2 vTexSamplingCoord;
void main() {
gl_FragColor = texture2D(uTexSampler, vTexSamplingCoord);
}
@@ -0,0 +1,74 @@
#version 100
// Copyright 2022 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ES 2 fragment shader that samples from a (non-external) texture with
// uTexSampler. It then converts the RGB color input into HSL and adjusts
// the Hue, Saturation, and Lightness and converts it then back to RGB.
// We use the algorithm based on the work by Sam Hocevar, which optimizes
// for an efficient branchless RGB <-> HSL conversion. A blog post is
// at https://www.chilliant.com/rgb2hsv.html and it is further explained at
// http://lolengine.net/blog/2013/01/13/fast-rgb-to-hsv.
precision highp float;
uniform sampler2D uTexSampler;
// uHueAdjustmentDegrees, uSaturationAdjustment, and uLightnessAdjustment
// are normalized to the unit interval [0, 1].
uniform float uHueAdjustmentDegrees;
uniform float uSaturationAdjustment;
uniform float uLightnessAdjustment;
varying vec2 vTexSamplingCoord;
const float epsilon = 1e-10;
vec3 rgbToHcv(vec3 rgb) {
vec4 p = (rgb.g < rgb.b) ? vec4(rgb.bg, -1.0, 2.0 / 3.0)
: vec4(rgb.gb, 0.0, -1.0 / 3.0);
vec4 q = (rgb.r < p.x) ? vec4(p.xyw, rgb.r) : vec4(rgb.r, p.yzx);
float c = q.x - min(q.w, q.y);
float h = abs((q.w - q.y) / (6.0 * c + epsilon) + q.z);
return vec3(h, c, q.x);
}
vec3 rgbToHsl(vec3 rgb) {
vec3 hcv = rgbToHcv(rgb);
float l = hcv.z - hcv.y * 0.5;
float s = hcv.y / (1.0 - abs(l * 2.0 - 1.0) + epsilon);
return vec3(hcv.x, s, l);
}
vec3 hueToRgb(float hue) {
float r = abs(hue * 6.0 - 3.0) - 1.0;
float g = 2.0 - abs(hue * 6.0 - 2.0);
float b = 2.0 - abs(hue * 6.0 - 4.0);
return clamp(vec3(r, g, b), 0.0, 1.0);
}
vec3 hslToRgb(vec3 hsl) {
vec3 rgb = hueToRgb(hsl.x);
float c = (1.0 - abs(2.0 * hsl.z - 1.0)) * hsl.y;
return (rgb - 0.5) * c + hsl.z;
}
void main() {
vec4 inputColor = texture2D(uTexSampler, vTexSamplingCoord);
vec3 hslColor = rgbToHsl(inputColor.rgb);
hslColor.x = mod(hslColor.x + uHueAdjustmentDegrees, 1.0);
hslColor.y = clamp(hslColor.y + uSaturationAdjustment, 0.0, 1.0);
hslColor.z = clamp(hslColor.z + uLightnessAdjustment, 0.0, 1.0);
gl_FragColor = vec4(hslToRgb(hslColor), inputColor.a);
}
@@ -0,0 +1,97 @@
#version 100
// Copyright 2022 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ES2 fragment shader that samples from a (non-external) texture with
// uTexSampler, copying from this texture to the current output while
// applying a 3D color lookup table to change the pixel colors.
precision highp float;
uniform sampler2D uTexSampler;
// The uColorLut texture is a N x N^2 2D texture where each z-plane of the 3D
// LUT is vertically stacked on top of each other. The red channel of the input
// color (z-axis in LUT[R][G][B] = LUT[z][y][x]) points to the plane to sample
// from. For more information check the
// androidx/media3/effect/SingleColorLut.java class, especially the function
// #transformCubeIntoBitmap with a provided example.
uniform sampler2D uColorLut;
uniform float uColorLutLength;
varying vec2 vTexSamplingCoord;
// Applies the color lookup using uLut based on the input colors.
vec3 applyLookup(vec3 color) {
// Reminder: Inside OpenGL vector.xyz is the same as vector.rgb.
// Here we use mentions of x and y coordinates to references to
// the position to sample from inside the 2D LUT plane and
// rgb to create the 3D coordinates based on the input colors.
// To sample from the 3D LUT we interpolate bilinearly twice in the 2D LUT
// to replicate the trilinear interpolation in a 3D LUT. Thus we sample
// from the plane of position redCoordLow and on the plane above.
// redCoordLow points to the lower plane to sample from.
float redCoord = color.r * (uColorLutLength - 1.0);
// Clamping to uColorLutLength - 2 is only needed if redCoord points to the
// most upper plane. In this case there would not be any plane above
// available to sample from.
float redCoordLow = clamp(floor(redCoord), 0.0, uColorLutLength - 2.0);
// lowerY is indexed in two steps. First redCoordLow defines the plane to
// sample from. Next the green color component is added to index the row in
// the found plane. As described in the NVIDIA blog article about LUTs
// https://developer.nvidia.com/gpugems/gpugems2/part-iii-high-quality-rendering/chapter-24-using-lookup-tables-accelerate-color
// (Section 24.2), we sample from color * scale + offset, where offset is
// defined by 1 / (2 * uColorLutLength) and the scale is defined by
// (uColorLutLength - 1.0) / uColorLutLength.
// The following derives the equation of lowerY. For this let
// N = uColorLutLenght. The general formula to sample at row y
// is defined as y = N * r + g.
// Using the offset and scale as described in NVIDIA's blog article we get:
// y = offset + (N * r + g) * scale
// y = 1 / (2 * N) + (N * r + g) * (N - 1) / N
// y = 1 / (2 * N) + N * r * (N - 1) / N + g * (N - 1) / N
// We have defined redCoord as r * (N - 1) if we excluded the clamping for
// now, giving us:
// y = 1 / (2 * N) + N * redCoord / N + g * (N - 1) / N
// This simplifies to:
// y = 0.5 / N + (N * redCoord + g * (N - 1)) / N
// y = (0.5 + N * redCoord + g * (N - 1)) / N
// This formula now assumes a coordinate system in the range of [0, N] but
// OpenGL uses a [0, 1] unit coordinate system internally. Thus dividing
// by N gives us the final formula for y:
// y = ((0.5 + N * redCoord + g * (N - 1)) / N) / N
// y = (0.5 + redCoord * N + g * (N - 1)) / (N * N)
float lowerY = (0.5 + redCoordLow * uColorLutLength +
color.g * (uColorLutLength - 1.0)) /
(uColorLutLength * uColorLutLength);
// The upperY is the same position moved up by one LUT plane.
float upperY = lowerY + 1.0 / uColorLutLength;
// The x position is the blue color channel (x-axis in LUT[R][G][B]).
float x = (0.5 + color.b * (uColorLutLength - 1.0)) / uColorLutLength;
vec3 lowerRgb = texture2D(uColorLut, vec2(x, lowerY)).rgb;
vec3 upperRgb = texture2D(uColorLut, vec2(x, upperY)).rgb;
// Linearly interpolate between lowerRgb and upperRgb based on the
// distance of the actual in the plane and the lower sampling position.
return mix(lowerRgb, upperRgb, redCoord - redCoordLow);
}
void main() {
vec4 inputColor = texture2D(uTexSampler, vTexSamplingCoord);
gl_FragColor.rgb = applyLookup(inputColor.rgb);
gl_FragColor.a = inputColor.a;
}
@@ -0,0 +1,96 @@
#version 300 es
// Copyright 2022 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ES 3 fragment shader that:
// 1. Samples optical linear BT.2020 RGB from a (non-external) texture with
// uTexSampler.
// 2. Applies a 4x4 RGB color matrix to change the pixel colors.
// 3. Outputs electrical (HLG or PQ) BT.2020 RGB based on uOutputColorTransfer,
// via an OETF.
// The output will be red if an error has occurred.
precision mediump float;
uniform sampler2D uTexSampler;
in vec2 vTexSamplingCoord;
out vec4 outColor;
// C.java#ColorTransfer value.
// Only COLOR_TRANSFER_ST2084 and COLOR_TRANSFER_HLG are allowed.
uniform int uOutputColorTransfer;
uniform mat3 uColorTransform;
uniform mat4 uRgbMatrix;
// TODO(b/227624622): Consider using mediump to save precision, if it won't lead
// to noticeable quantization.
// HLG OETF for one channel.
highp float hlgOetfSingleChannel(highp float linearChannel) {
// Specification:
// https://www.khronos.org/registry/DataFormat/specs/1.3/dataformat.1.3.inline.html#TRANSFER_HLG
// Reference implementation:
// https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/renderengine/gl/ProgramCache.cpp;l=529-543;drc=de09f10aa504fd8066370591a00c9ff1cafbb7fa
const highp float a = 0.17883277;
const highp float b = 0.28466892;
const highp float c = 0.55991073;
return linearChannel <= 1.0 / 12.0 ? sqrt(3.0 * linearChannel)
: a * log(12.0 * linearChannel - b) + c;
}
// BT.2100 / BT.2020 HLG OETF.
highp vec3 hlgOetf(highp vec3 linearColor) {
return vec3(hlgOetfSingleChannel(linearColor.r),
hlgOetfSingleChannel(linearColor.g),
hlgOetfSingleChannel(linearColor.b));
}
// BT.2100 / BT.2020, PQ / ST2084 OETF.
highp vec3 pqOetf(highp vec3 linearColor) {
// Specification:
// https://registry.khronos.org/DataFormat/specs/1.3/dataformat.1.3.inline.html#TRANSFER_PQ
// Reference implementation:
// https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/renderengine/gl/ProgramCache.cpp;l=514-527;drc=de09f10aa504fd8066370591a00c9ff1cafbb7fa
const highp float m1 = (2610.0 / 16384.0);
const highp float m2 = (2523.0 / 4096.0) * 128.0;
const highp float c1 = (3424.0 / 4096.0);
const highp float c2 = (2413.0 / 4096.0) * 32.0;
const highp float c3 = (2392.0 / 4096.0) * 32.0;
highp vec3 temp = pow(linearColor, vec3(m1));
temp = (c1 + c2 * temp) / (1.0 + c3 * temp);
return pow(temp, vec3(m2));
}
// Applies the appropriate OETF to convert linear optical signals to nonlinear
// electrical signals. Input and output are both normalized to [0, 1].
highp vec3 applyOetf(highp vec3 linearColor) {
// LINT.IfChange(color_transfer)
const int COLOR_TRANSFER_ST2084 = 6;
const int COLOR_TRANSFER_HLG = 7;
if (uOutputColorTransfer == COLOR_TRANSFER_ST2084) {
return pqOetf(linearColor);
} else if (uOutputColorTransfer == COLOR_TRANSFER_HLG) {
return hlgOetf(linearColor);
} else {
// Output red as an obviously visible error.
return vec3(1.0, 0.0, 0.0);
}
}
void main() {
vec4 inputColor = texture(uTexSampler, vTexSamplingCoord);
// transformedColors is an optical color.
vec4 transformedColors = uRgbMatrix * vec4(inputColor.rgb, 1);
outColor = vec4(applyOetf(transformedColors.rgb), inputColor.a);
}
@@ -0,0 +1,29 @@
#version 100
// Copyright 2022 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ES 2 fragment shader that samples from a (non-external) texture with
// uTexSampler, copying from this texture to the current output while
// applying a 4x4 RGB color matrix to change the pixel colors.
precision mediump float;
uniform sampler2D uTexSampler;
uniform mat4 uRgbMatrix;
varying vec2 vTexSamplingCoord;
void main() {
vec4 inputColor = texture2D(uTexSampler, vTexSamplingCoord);
gl_FragColor = uRgbMatrix * vec4(inputColor.rgb, 1);
gl_FragColor.a = inputColor.a;
}
@@ -0,0 +1,240 @@
#version 300 es
// Copyright 2022 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ES 3 fragment shader that:
// 1. Samples electrical (HLG or PQ) BT.2020 YUV from an external texture with
// uTexSampler, where the sampler uses the EXT_YUV_target extension specified
// at
// https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_YUV_target.txt,
// 2. Applies a YUV to RGB conversion using the specified color transform
// uYuvToRgbColorTransform, yielding electrical (HLG or PQ) BT.2020 RGB,
// 3. Applies an EOTF based on uInputColorTransfer, yielding optical linear
// BT.2020 RGB.
// 4. Optionally applies a BT2020 to BT709 OOTF, if OpenGL tone-mapping is
// requested via uApplyHdrToSdrToneMapping.
// 5. Applies a 4x4 RGB color matrix to change the pixel colors.
// 6. Outputs as requested by uOutputColorTransfer. Use COLOR_TRANSFER_LINEAR
// for outputting to intermediate shaders, or COLOR_TRANSFER_ST2084 /
// COLOR_TRANSFER_HLG to output electrical colors via an OETF (e.g. to an
// encoder).
// The output will be red or blue if an error has occurred.
#extension GL_OES_EGL_image_external : require
#extension GL_EXT_YUV_target : require
precision mediump float;
uniform __samplerExternal2DY2YEXT uTexSampler;
uniform mat3 uYuvToRgbColorTransform;
uniform mat4 uRgbMatrix;
// C.java#ColorTransfer value.
// Only COLOR_TRANSFER_ST2084 and COLOR_TRANSFER_HLG are allowed.
uniform int uInputColorTransfer;
uniform int uApplyHdrToSdrToneMapping;
// C.java#ColorTransfer value.
// Only COLOR_TRANSFER_LINEAR, COLOR_TRANSFER_GAMMA_2_2, COLOR_TRANSFER_ST2084,
// and COLOR_TRANSFER_HLG are allowed.
uniform int uOutputColorTransfer;
in vec2 vTexSamplingCoord;
out vec4 outColor;
// LINT.IfChange(color_transfer)
const int COLOR_TRANSFER_LINEAR = 1;
const int COLOR_TRANSFER_GAMMA_2_2 = 10;
const int COLOR_TRANSFER_ST2084 = 6;
const int COLOR_TRANSFER_HLG = 7;
// TODO(b/227624622): Consider using mediump to save precision, if it won't lead
// to noticeable quantization errors.
// BT.2100 / BT.2020 HLG EOTF for one channel.
highp float hlgEotfSingleChannel(highp float hlgChannel) {
// Specification:
// https://www.khronos.org/registry/DataFormat/specs/1.3/dataformat.1.3.inline.html#TRANSFER_HLG
// Reference implementation:
// https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/renderengine/gl/ProgramCache.cpp;l=265-279;drc=de09f10aa504fd8066370591a00c9ff1cafbb7fa
const highp float a = 0.17883277;
const highp float b = 0.28466892;
const highp float c = 0.55991073;
return hlgChannel <= 0.5 ? hlgChannel * hlgChannel / 3.0
: (b + exp((hlgChannel - c) / a)) / 12.0;
}
// BT.2100 / BT.2020 HLG EOTF.
highp vec3 hlgEotf(highp vec3 hlgColor) {
return vec3(hlgEotfSingleChannel(hlgColor.r),
hlgEotfSingleChannel(hlgColor.g),
hlgEotfSingleChannel(hlgColor.b));
}
// BT.2100 / BT.2020 PQ EOTF.
highp vec3 pqEotf(highp vec3 pqColor) {
// Specification:
// https://registry.khronos.org/DataFormat/specs/1.3/dataformat.1.3.inline.html#TRANSFER_PQ
// Reference implementation:
// https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/renderengine/gl/ProgramCache.cpp;l=250-263;drc=de09f10aa504fd8066370591a00c9ff1cafbb7fa
const highp float m1 = (2610.0 / 16384.0);
const highp float m2 = (2523.0 / 4096.0) * 128.0;
const highp float c1 = (3424.0 / 4096.0);
const highp float c2 = (2413.0 / 4096.0) * 32.0;
const highp float c3 = (2392.0 / 4096.0) * 32.0;
highp vec3 temp = pow(clamp(pqColor, 0.0, 1.0), 1.0 / vec3(m2));
temp = max(temp - c1, 0.0) / (c2 - c3 * temp);
return pow(temp, 1.0 / vec3(m1));
}
// Applies the appropriate EOTF to convert nonlinear electrical values to linear
// optical values. Input and output are both normalized to [0, 1].
highp vec3 applyEotf(highp vec3 electricalColor) {
if (uInputColorTransfer == COLOR_TRANSFER_ST2084) {
return pqEotf(electricalColor);
} else if (uInputColorTransfer == COLOR_TRANSFER_HLG) {
return hlgEotf(electricalColor);
} else {
// Output red as an obviously visible error.
return vec3(1.0, 0.0, 0.0);
}
}
// Apply the HLG BT2020 to BT709 OOTF.
highp vec3 applyHlgBt2020ToBt709Ootf(highp vec3 linearRgbBt2020) {
// Reference ("HLG Reference OOTF" section):
// https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2100-2-201807-I!!PDF-E.pdf
// Matrix values based on computeXYZMatrix(BT2020Primaries, BT2020WhitePoint)
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/libs/hwui/utils/HostColorSpace.cpp;l=200-232;drc=86bd214059cd6150304888a285941bf74af5b687
const mat3 RGB_TO_XYZ_BT2020 =
mat3(0.63695805f, 0.26270021f, 0.00000000f, 0.14461690f, 0.67799807f,
0.02807269f, 0.16888098f, 0.05930172f, 1.06098506f);
// Matrix values based on computeXYZMatrix(BT709Primaries, BT709WhitePoint)
const mat3 XYZ_TO_RGB_BT709 =
mat3(3.24096994f, -0.96924364f, 0.05563008f, -1.53738318f, 1.87596750f,
-0.20397696f, -0.49861076f, 0.04155506f, 1.05697151f);
// hlgGamma is 1.2 + 0.42 * log10(nominalPeakLuminance/1000);
// nominalPeakLuminance was selected to use a 500 as a typical value, used
// in
// https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/tonemap/tonemap.cpp;drc=7a577450e536aa1e99f229a0cb3d3531c82e8a8d;l=62,
// b/199162498#comment35, and
// https://www.microsoft.com/applied-sciences/uploads/projects/investigation-of-hdr-vs-tone-mapped-sdr/investigation-of-hdr-vs-tone-mapped-sdr.pdf.
const float hlgGamma = 1.0735674018211279;
vec3 linearXyzBt2020 = RGB_TO_XYZ_BT2020 * linearRgbBt2020;
vec3 linearXyzBt709 =
linearXyzBt2020 * pow(linearXyzBt2020[1], hlgGamma - 1.0);
vec3 linearRgbBt709 = clamp((XYZ_TO_RGB_BT709 * linearXyzBt709), 0.0, 1.0);
return linearRgbBt709;
}
// Apply the PQ BT2020 to BT709 OOTF.
highp vec3 applyPqBt2020ToBt709Ootf(highp vec3 linearRgbBt2020) {
float pqPeakLuminance = 10000.0;
float sdrPeakLuminance = 500.0;
return linearRgbBt2020 * pqPeakLuminance / sdrPeakLuminance;
}
highp vec3 applyBt2020ToBt709Ootf(highp vec3 linearRgbBt2020) {
if (uInputColorTransfer == COLOR_TRANSFER_ST2084) {
return applyPqBt2020ToBt709Ootf(linearRgbBt2020);
} else if (uInputColorTransfer == COLOR_TRANSFER_HLG) {
return applyHlgBt2020ToBt709Ootf(linearRgbBt2020);
} else {
// Output green as an obviously visible error.
return vec3(0.0, 1.0, 0.0);
}
}
// BT.2100 / BT.2020 HLG OETF for one channel.
highp float hlgOetfSingleChannel(highp float linearChannel) {
// Specification:
// https://www.khronos.org/registry/DataFormat/specs/1.3/dataformat.1.3.inline.html#TRANSFER_HLG
// Reference implementation:
// https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/renderengine/gl/ProgramCache.cpp;l=529-543;drc=de09f10aa504fd8066370591a00c9ff1cafbb7fa
const highp float a = 0.17883277;
const highp float b = 0.28466892;
const highp float c = 0.55991073;
return linearChannel <= 1.0 / 12.0 ? sqrt(3.0 * linearChannel)
: a * log(12.0 * linearChannel - b) + c;
}
// BT.2100 / BT.2020 HLG OETF.
highp vec3 hlgOetf(highp vec3 linearColor) {
return vec3(hlgOetfSingleChannel(linearColor.r),
hlgOetfSingleChannel(linearColor.g),
hlgOetfSingleChannel(linearColor.b));
}
// BT.2100 / BT.2020, PQ / ST2084 OETF.
highp vec3 pqOetf(highp vec3 linearColor) {
// Specification:
// https://registry.khronos.org/DataFormat/specs/1.3/dataformat.1.3.inline.html#TRANSFER_PQ
// Reference implementation:
// https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/renderengine/gl/ProgramCache.cpp;l=514-527;drc=de09f10aa504fd8066370591a00c9ff1cafbb7fa
const highp float m1 = (2610.0 / 16384.0);
const highp float m2 = (2523.0 / 4096.0) * 128.0;
const highp float c1 = (3424.0 / 4096.0);
const highp float c2 = (2413.0 / 4096.0) * 32.0;
const highp float c3 = (2392.0 / 4096.0) * 32.0;
highp vec3 temp = pow(linearColor, vec3(m1));
temp = (c1 + c2 * temp) / (1.0 + c3 * temp);
return pow(temp, vec3(m2));
}
// BT.709 gamma 2.2 OETF for one channel.
float gamma22OetfSingleChannel(highp float linearChannel) {
// Reference:
// https://developer.android.com/reference/android/hardware/DataSpace#TRANSFER_GAMMA2_2
return pow(linearChannel, (1.0 / 2.2));
}
// BT.709 gamma 2.2 OETF.
vec3 gamma22Oetf(highp vec3 linearColor) {
return vec3(gamma22OetfSingleChannel(linearColor.r),
gamma22OetfSingleChannel(linearColor.g),
gamma22OetfSingleChannel(linearColor.b));
}
// Applies the appropriate OETF to convert linear optical signals to nonlinear
// electrical signals. Input and output are both normalized to [0, 1].
highp vec3 applyOetf(highp vec3 linearColor) {
if (uOutputColorTransfer == COLOR_TRANSFER_ST2084) {
return pqOetf(linearColor);
} else if (uOutputColorTransfer == COLOR_TRANSFER_HLG) {
return hlgOetf(linearColor);
} else if (uOutputColorTransfer == COLOR_TRANSFER_GAMMA_2_2) {
return gamma22Oetf(linearColor);
} else if (uOutputColorTransfer == COLOR_TRANSFER_LINEAR) {
return linearColor;
} else {
// Output blue as an obviously visible error.
return vec3(0.0, 0.0, 1.0);
}
}
vec3 yuvToRgb(vec3 yuv) {
const vec3 yuvOffset = vec3(0.0625, 0.5, 0.5);
return clamp(uYuvToRgbColorTransform * (yuv - yuvOffset), 0.0, 1.0);
}
void main() {
vec3 srcYuv = texture(uTexSampler, vTexSamplingCoord).xyz;
vec3 opticalColorBt2020 = applyEotf(yuvToRgb(srcYuv));
vec4 opticalColor =
(uApplyHdrToSdrToneMapping == 1)
? vec4(applyBt2020ToBt709Ootf(opticalColorBt2020), 1.0)
: vec4(opticalColorBt2020, 1.0);
vec4 transformedColors = uRgbMatrix * opticalColor;
outColor = vec4(applyOetf(transformedColors.rgb), 1.0);
}
@@ -0,0 +1,227 @@
#version 300 es
// Copyright 2022 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ES 3 fragment shader that:
// 1. Samples electrical (HLG or PQ) BT.2020 RGB from an internal texture.
// 2. Applies an EOTF based on uInputColorTransfer, yielding optical linear
// BT.2020 RGB.
// 3. Optionally applies a BT2020 to BT709 OOTF, if OpenGL tone-mapping is
// requested via uApplyHdrToSdrToneMapping.
// 4. Applies a 4x4 RGB color matrix to change the pixel colors.
// 5. Outputs as requested by uOutputColorTransfer. Use COLOR_TRANSFER_LINEAR
// for outputting to intermediate shaders, or COLOR_TRANSFER_ST2084 /
// COLOR_TRANSFER_HLG to output electrical colors via an OETF (e.g. to an
// encoder).
// The output will be red or blue if an error has occurred.
precision mediump float;
uniform sampler2D uTexSampler;
uniform mat4 uRgbMatrix;
// C.java#ColorTransfer value.
// Only COLOR_TRANSFER_ST2084 and COLOR_TRANSFER_HLG are allowed.
uniform int uInputColorTransfer;
uniform int uApplyHdrToSdrToneMapping;
// C.java#ColorTransfer value.
// Only COLOR_TRANSFER_LINEAR, COLOR_TRANSFER_GAMMA_2_2, COLOR_TRANSFER_ST2084,
// and COLOR_TRANSFER_HLG are allowed.
uniform int uOutputColorTransfer;
in vec2 vTexSamplingCoord;
out vec4 outColor;
// LINT.IfChange(color_transfer)
const int COLOR_TRANSFER_LINEAR = 1;
const int COLOR_TRANSFER_GAMMA_2_2 = 10;
const int COLOR_TRANSFER_ST2084 = 6;
const int COLOR_TRANSFER_HLG = 7;
// TODO(b/227624622): Consider using mediump to save precision, if it won't lead
// to noticeable quantization errors.
// BT.2100 / BT.2020 HLG EOTF for one channel.
highp float hlgEotfSingleChannel(highp float hlgChannel) {
// Specification:
// https://www.khronos.org/registry/DataFormat/specs/1.3/dataformat.1.3.inline.html#TRANSFER_HLG
// Reference implementation:
// https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/renderengine/gl/ProgramCache.cpp;l=265-279;drc=de09f10aa504fd8066370591a00c9ff1cafbb7fa
const highp float a = 0.17883277;
const highp float b = 0.28466892;
const highp float c = 0.55991073;
return hlgChannel <= 0.5 ? hlgChannel * hlgChannel / 3.0
: (b + exp((hlgChannel - c) / a)) / 12.0;
}
// BT.2100 / BT.2020 HLG EOTF.
highp vec3 hlgEotf(highp vec3 hlgColor) {
return vec3(hlgEotfSingleChannel(hlgColor.r),
hlgEotfSingleChannel(hlgColor.g),
hlgEotfSingleChannel(hlgColor.b));
}
// BT.2100 / BT.2020 PQ EOTF.
highp vec3 pqEotf(highp vec3 pqColor) {
// Specification:
// https://registry.khronos.org/DataFormat/specs/1.3/dataformat.1.3.inline.html#TRANSFER_PQ
// Reference implementation:
// https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/renderengine/gl/ProgramCache.cpp;l=250-263;drc=de09f10aa504fd8066370591a00c9ff1cafbb7fa
const highp float m1 = (2610.0 / 16384.0);
const highp float m2 = (2523.0 / 4096.0) * 128.0;
const highp float c1 = (3424.0 / 4096.0);
const highp float c2 = (2413.0 / 4096.0) * 32.0;
const highp float c3 = (2392.0 / 4096.0) * 32.0;
highp vec3 temp = pow(clamp(pqColor, 0.0, 1.0), 1.0 / vec3(m2));
temp = max(temp - c1, 0.0) / (c2 - c3 * temp);
return pow(temp, 1.0 / vec3(m1));
}
// Applies the appropriate EOTF to convert nonlinear electrical values to linear
// optical values. Input and output are both normalized to [0, 1].
highp vec3 applyEotf(highp vec3 electricalColor) {
if (uInputColorTransfer == COLOR_TRANSFER_ST2084) {
return pqEotf(electricalColor);
} else if (uInputColorTransfer == COLOR_TRANSFER_HLG) {
return hlgEotf(electricalColor);
} else {
// Output red as an obviously visible error.
return vec3(1.0, 0.0, 0.0);
}
}
// Apply the HLG BT2020 to BT709 OOTF.
highp vec3 applyHlgBt2020ToBt709Ootf(highp vec3 linearRgbBt2020) {
// Reference ("HLG Reference OOTF" section):
// https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2100-2-201807-I!!PDF-E.pdf
// Matrix values based on computeXYZMatrix(BT2020Primaries, BT2020WhitePoint)
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/libs/hwui/utils/HostColorSpace.cpp;l=200-232;drc=86bd214059cd6150304888a285941bf74af5b687
const mat3 RGB_TO_XYZ_BT2020 =
mat3(0.63695805f, 0.26270021f, 0.00000000f, 0.14461690f, 0.67799807f,
0.02807269f, 0.16888098f, 0.05930172f, 1.06098506f);
// Matrix values based on computeXYZMatrix(BT709Primaries, BT709WhitePoint)
const mat3 XYZ_TO_RGB_BT709 =
mat3(3.24096994f, -0.96924364f, 0.05563008f, -1.53738318f, 1.87596750f,
-0.20397696f, -0.49861076f, 0.04155506f, 1.05697151f);
// hlgGamma is 1.2 + 0.42 * log10(nominalPeakLuminance/1000);
// nominalPeakLuminance was selected to use a 500 as a typical value, used
// in
// https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/tonemap/tonemap.cpp;drc=7a577450e536aa1e99f229a0cb3d3531c82e8a8d;l=62,
// b/199162498#comment35, and
// https://www.microsoft.com/applied-sciences/uploads/projects/investigation-of-hdr-vs-tone-mapped-sdr/investigation-of-hdr-vs-tone-mapped-sdr.pdf.
const float hlgGamma = 1.0735674018211279;
vec3 linearXyzBt2020 = RGB_TO_XYZ_BT2020 * linearRgbBt2020;
vec3 linearXyzBt709 =
linearXyzBt2020 * pow(linearXyzBt2020[1], hlgGamma - 1.0);
vec3 linearRgbBt709 = clamp((XYZ_TO_RGB_BT709 * linearXyzBt709), 0.0, 1.0);
return linearRgbBt709;
}
// Apply the PQ BT2020 to BT709 OOTF.
highp vec3 applyPqBt2020ToBt709Ootf(highp vec3 linearRgbBt2020) {
float pqPeakLuminance = 10000.0;
float sdrPeakLuminance = 500.0;
return linearRgbBt2020 * pqPeakLuminance / sdrPeakLuminance;
}
highp vec3 applyBt2020ToBt709Ootf(highp vec3 linearRgbBt2020) {
if (uInputColorTransfer == COLOR_TRANSFER_ST2084) {
return applyPqBt2020ToBt709Ootf(linearRgbBt2020);
} else if (uInputColorTransfer == COLOR_TRANSFER_HLG) {
return applyHlgBt2020ToBt709Ootf(linearRgbBt2020);
} else {
// Output green as an obviously visible error.
return vec3(0.0, 1.0, 0.0);
}
}
// BT.2100 / BT.2020 HLG OETF for one channel.
highp float hlgOetfSingleChannel(highp float linearChannel) {
// Specification:
// https://www.khronos.org/registry/DataFormat/specs/1.3/dataformat.1.3.inline.html#TRANSFER_HLG
// Reference implementation:
// https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/renderengine/gl/ProgramCache.cpp;l=529-543;drc=de09f10aa504fd8066370591a00c9ff1cafbb7fa
const highp float a = 0.17883277;
const highp float b = 0.28466892;
const highp float c = 0.55991073;
return linearChannel <= 1.0 / 12.0 ? sqrt(3.0 * linearChannel)
: a * log(12.0 * linearChannel - b) + c;
}
// BT.2100 / BT.2020 HLG OETF.
highp vec3 hlgOetf(highp vec3 linearColor) {
return vec3(hlgOetfSingleChannel(linearColor.r),
hlgOetfSingleChannel(linearColor.g),
hlgOetfSingleChannel(linearColor.b));
}
// BT.2100 / BT.2020, PQ / ST2084 OETF.
highp vec3 pqOetf(highp vec3 linearColor) {
// Specification:
// https://registry.khronos.org/DataFormat/specs/1.3/dataformat.1.3.inline.html#TRANSFER_PQ
// Reference implementation:
// https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/renderengine/gl/ProgramCache.cpp;l=514-527;drc=de09f10aa504fd8066370591a00c9ff1cafbb7fa
const highp float m1 = (2610.0 / 16384.0);
const highp float m2 = (2523.0 / 4096.0) * 128.0;
const highp float c1 = (3424.0 / 4096.0);
const highp float c2 = (2413.0 / 4096.0) * 32.0;
const highp float c3 = (2392.0 / 4096.0) * 32.0;
highp vec3 temp = pow(linearColor, vec3(m1));
temp = (c1 + c2 * temp) / (1.0 + c3 * temp);
return pow(temp, vec3(m2));
}
// BT.709 gamma 2.2 OETF for one channel.
float gamma22OetfSingleChannel(highp float linearChannel) {
// Reference:
// https://developer.android.com/reference/android/hardware/DataSpace#TRANSFER_GAMMA2_2
return pow(linearChannel, (1.0 / 2.2));
}
// BT.709 gamma 2.2 OETF.
vec3 gamma22Oetf(highp vec3 linearColor) {
return vec3(gamma22OetfSingleChannel(linearColor.r),
gamma22OetfSingleChannel(linearColor.g),
gamma22OetfSingleChannel(linearColor.b));
}
// Applies the appropriate OETF to convert linear optical signals to nonlinear
// electrical signals. Input and output are both normalized to [0, 1].
highp vec3 applyOetf(highp vec3 linearColor) {
if (uOutputColorTransfer == COLOR_TRANSFER_ST2084) {
return pqOetf(linearColor);
} else if (uOutputColorTransfer == COLOR_TRANSFER_HLG) {
return hlgOetf(linearColor);
} else if (uOutputColorTransfer == COLOR_TRANSFER_GAMMA_2_2) {
return gamma22Oetf(linearColor);
} else if (uOutputColorTransfer == COLOR_TRANSFER_LINEAR) {
return linearColor;
} else {
// Output blue as an obviously visible error.
return vec3(0.0, 0.0, 1.0);
}
}
void main() {
vec3 opticalColorBt2020 =
applyEotf(texture(uTexSampler, vTexSamplingCoord).xyz);
vec4 opticalColor =
(uApplyHdrToSdrToneMapping == 1)
? vec4(applyBt2020ToBt709Ootf(opticalColorBt2020), 1.0)
: vec4(opticalColorBt2020, 1.0);
vec4 transformedColors = uRgbMatrix * opticalColor;
outColor = vec4(applyOetf(transformedColors.rgb), 1.0);
}
@@ -0,0 +1,109 @@
#version 100
// Copyright 2021 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ES 2 fragment shader that:
// 1. Samples from an external texture with uTexSampler copying from this
// texture to the current output.
// 2. Transforms the electrical colors to optical colors using the SMPTE 170M
// EOTF.
// 3. Applies a 4x4 RGB color matrix to change the pixel colors.
// 4. Outputs as requested by uOutputColorTransfer. Use COLOR_TRANSFER_LINEAR
// for outputting to intermediate shaders, or COLOR_TRANSFER_SDR_VIDEO to
// output electrical colors via an OETF (e.g. to an encoder).
#extension GL_OES_EGL_image_external : require
precision mediump float;
uniform samplerExternalOES uTexSampler;
uniform mat4 uRgbMatrix;
varying vec2 vTexSamplingCoord;
// C.java#ColorTransfer value.
// Only COLOR_TRANSFER_LINEAR and COLOR_TRANSFER_SDR_VIDEO are allowed.
uniform int uOutputColorTransfer;
uniform int uEnableColorTransfer;
const float inverseGamma = 0.4500;
const float gamma = 1.0 / inverseGamma;
const int GL_FALSE = 0;
const int GL_TRUE = 1;
// Transforms a single channel from electrical to optical SDR using the SMPTE
// 170M OETF.
float smpte170mEotfSingleChannel(float electricalChannel) {
// Specification:
// https://www.itu.int/rec/R-REC-BT.1700-0-200502-I/en
return electricalChannel < 0.0812
? electricalChannel / 4.500
: pow((electricalChannel + 0.099) / 1.099, gamma);
}
// Transforms electrical to optical SDR using the SMPTE 170M EOTF.
vec3 smpte170mEotf(vec3 electricalColor) {
return vec3(smpte170mEotfSingleChannel(electricalColor.r),
smpte170mEotfSingleChannel(electricalColor.g),
smpte170mEotfSingleChannel(electricalColor.b));
}
// Transforms a single channel from optical to electrical SDR.
float smpte170mOetfSingleChannel(float opticalChannel) {
// Specification:
// https://www.itu.int/rec/R-REC-BT.1700-0-200502-I/en
return opticalChannel < 0.018
? opticalChannel * 4.500
: 1.099 * pow(opticalChannel, inverseGamma) - 0.099;
}
// Transforms optical SDR colors to electrical SDR using the SMPTE 170M OETF.
vec3 smpte170mOetf(vec3 opticalColor) {
return vec3(smpte170mOetfSingleChannel(opticalColor.r),
smpte170mOetfSingleChannel(opticalColor.g),
smpte170mOetfSingleChannel(opticalColor.b));
}
// Applies the appropriate OETF to convert linear optical signals to nonlinear
// electrical signals. Input and output are both normalized to [0, 1].
highp vec3 applyOetf(highp vec3 linearColor) {
// LINT.IfChange(color_transfer)
const int COLOR_TRANSFER_LINEAR = 1;
const int COLOR_TRANSFER_SDR_VIDEO = 3;
if (uOutputColorTransfer == COLOR_TRANSFER_LINEAR ||
uEnableColorTransfer == GL_FALSE) {
return linearColor;
} else if (uOutputColorTransfer == COLOR_TRANSFER_SDR_VIDEO) {
return smpte170mOetf(linearColor);
} else {
// Output red as an obviously visible error.
return vec3(1.0, 0.0, 0.0);
}
}
vec3 applyEotf(vec3 electricalColor) {
if (uEnableColorTransfer == GL_TRUE) {
return smpte170mEotf(electricalColor);
} else if (uEnableColorTransfer == GL_FALSE) {
return electricalColor;
} else {
// Output blue as an obviously visible error.
return vec3(0.0, 0.0, 1.0);
}
}
void main() {
vec4 inputColor = texture2D(uTexSampler, vTexSamplingCoord);
vec3 linearInputColor = applyEotf(inputColor.rgb);
vec4 transformedColors = uRgbMatrix * vec4(linearInputColor, 1);
gl_FragColor = vec4(applyOetf(transformedColors.rgb), inputColor.a);
}
@@ -0,0 +1,150 @@
#version 100
// Copyright 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ES 2 fragment shader that:
// 1. Samples from an input texture created from an internal texture (e.g. a
// texture created from a bitmap), with uTexSampler copying from this texture
// to the current output.
// 2. Transforms the electrical colors to optical colors using the SMPTE 170M
// EOTF or the sRGB EOTF, as requested by uInputColorTransfer.
// 3. Applies a 4x4 RGB color matrix to change the pixel colors.
// 4. Outputs as requested by uOutputColorTransfer. Use COLOR_TRANSFER_LINEAR
// for outputting to intermediate shaders, or COLOR_TRANSFER_SDR_VIDEO to
// output electrical colors via an OETF (e.g. to an encoder).
precision mediump float;
uniform sampler2D uTexSampler;
uniform mat4 uRgbMatrix;
varying vec2 vTexSamplingCoord;
// C.java#ColorTransfer value.
// Only COLOR_TRANSFER_SRGB and COLOR_TRANSFER_SDR_VIDEO are allowed.
uniform int uInputColorTransfer;
// C.java#ColorTransfer value.
// Only COLOR_TRANSFER_LINEAR and COLOR_TRANSFER_SDR_VIDEO are allowed.
uniform int uOutputColorTransfer;
uniform int uEnableColorTransfer;
const float inverseGamma = 0.4500;
const float gamma = 1.0 / inverseGamma;
const int GL_FALSE = 0;
const int GL_TRUE = 1;
// LINT.IfChange(color_transfer)
const int COLOR_TRANSFER_LINEAR = 1;
const int COLOR_TRANSFER_SRGB = 2;
const int COLOR_TRANSFER_SDR_VIDEO = 3;
// Transforms a single channel from electrical to optical SDR using the sRGB
// EOTF.
float srgbEotfSingleChannel(float electricalChannel) {
// Specification:
// https://developer.android.com/ndk/reference/group/a-data-space#group___a_data_space_1gga2759ad19cae46646cc5f7002758c4a1cac1bef6aa3a72abbf4a651a0bfb117f96
return electricalChannel <= 0.04045
? electricalChannel / 12.92
: pow((electricalChannel + 0.055) / 1.055, 2.4);
}
// Transforms electrical to optical SDR using the sRGB EOTF.
vec3 srgbEotf(const vec3 electricalColor) {
return vec3(srgbEotfSingleChannel(electricalColor.r),
srgbEotfSingleChannel(electricalColor.g),
srgbEotfSingleChannel(electricalColor.b));
}
// Transforms a single channel from electrical to optical SDR using the SMPTE
// 170M OETF.
float smpte170mEotfSingleChannel(float electricalChannel) {
// Specification:
// https://www.itu.int/rec/R-REC-BT.1700-0-200502-I/en
return electricalChannel < 0.0812
? electricalChannel / 4.500
: pow((electricalChannel + 0.099) / 1.099, gamma);
}
// Transforms electrical to optical SDR using the SMPTE 170M EOTF.
vec3 smpte170mEotf(vec3 electricalColor) {
return vec3(smpte170mEotfSingleChannel(electricalColor.r),
smpte170mEotfSingleChannel(electricalColor.g),
smpte170mEotfSingleChannel(electricalColor.b));
}
// Transforms a single channel from optical to electrical SDR.
float smpte170mOetfSingleChannel(float opticalChannel) {
// Specification:
// https://www.itu.int/rec/R-REC-BT.1700-0-200502-I/en
return opticalChannel < 0.018
? opticalChannel * 4.500
: 1.099 * pow(opticalChannel, inverseGamma) - 0.099;
}
// Transforms optical SDR colors to electrical SDR using the SMPTE 170M OETF.
vec3 smpte170mOetf(vec3 opticalColor) {
return vec3(smpte170mOetfSingleChannel(opticalColor.r),
smpte170mOetfSingleChannel(opticalColor.g),
smpte170mOetfSingleChannel(opticalColor.b));
}
// Applies the appropriate EOTF to convert nonlinear electrical signals to
// linear optical signals. Input and output are both normalized to [0, 1].
vec3 applyEotf(vec3 electricalColor) {
if (uEnableColorTransfer == GL_TRUE) {
if (uInputColorTransfer == COLOR_TRANSFER_SRGB) {
return srgbEotf(electricalColor);
} else if (uInputColorTransfer == COLOR_TRANSFER_SDR_VIDEO) {
return smpte170mEotf(electricalColor);
} else {
// Output blue as an obviously visible error.
return vec3(0.0, 0.0, 1.0);
}
} else if (uEnableColorTransfer == GL_FALSE) {
return electricalColor;
} else {
// Output blue as an obviously visible error.
return vec3(0.0, 0.0, 1.0);
}
}
// Applies the appropriate OETF to convert linear optical signals to nonlinear
// electrical signals. Input and output are both normalized to [0, 1].
highp vec3 applyOetf(highp vec3 linearColor) {
if (uOutputColorTransfer == COLOR_TRANSFER_LINEAR ||
uEnableColorTransfer == GL_FALSE) {
return linearColor;
} else if (uOutputColorTransfer == COLOR_TRANSFER_SDR_VIDEO) {
return smpte170mOetf(linearColor);
} else {
// Output red as an obviously visible error.
return vec3(1.0, 0.0, 0.0);
}
}
vec2 getAdjustedTexSamplingCoord(vec2 originalTexSamplingCoord) {
if (uInputColorTransfer == COLOR_TRANSFER_SRGB) {
// Whereas the Android system uses the top-left corner as (0,0) of the
// coordinate system, OpenGL uses the bottom-left corner as (0,0), so the
// texture gets flipped. We flip the texture vertically to ensure the
// orientation of the output is correct.
return vec2(originalTexSamplingCoord.x, 1.0 - originalTexSamplingCoord.y);
} else {
return originalTexSamplingCoord;
}
}
void main() {
vec4 inputColor =
texture2D(uTexSampler, getAdjustedTexSamplingCoord(vTexSamplingCoord));
vec3 linearInputColor = applyEotf(inputColor.rgb);
vec4 transformedColors = uRgbMatrix * vec4(linearInputColor, 1);
gl_FragColor = vec4(applyOetf(transformedColors.rgb), inputColor.a);
}
@@ -0,0 +1,85 @@
#version 100
// Copyright 2022 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ES 2 fragment shader that:
// 1. Samples from uTexSampler, copying from this texture to the current
// output.
// 2. Applies a 4x4 RGB color matrix to change the pixel colors.
// 3. Transforms the optical colors to electrical colors using the SMPTE
// 170M OETF.
precision mediump float;
uniform sampler2D uTexSampler;
uniform mat4 uRgbMatrix;
varying vec2 vTexSamplingCoord;
// C.java#ColorTransfer value.
// Only COLOR_TRANSFER_SDR and COLOR_TRANSFER_GAMMA_2_2 are allowed.
uniform int uOutputColorTransfer;
const float inverseGamma = 0.4500;
// Transforms a single channel from optical to electrical SDR using the SMPTE
// 170M OETF.
float smpte170mOetfSingleChannel(float opticalChannel) {
// Specification:
// https://www.itu.int/rec/R-REC-BT.1700-0-200502-I/en
return opticalChannel < 0.018
? opticalChannel * 4.500
: 1.099 * pow(opticalChannel, inverseGamma) - 0.099;
}
// Transforms optical SDR colors to electrical SDR using the SMPTE 170M OETF.
vec3 smpte170mOetf(vec3 opticalColor) {
return vec3(smpte170mOetfSingleChannel(opticalColor.r),
smpte170mOetfSingleChannel(opticalColor.g),
smpte170mOetfSingleChannel(opticalColor.b));
}
// BT.709 gamma 2.2 OETF for one channel.
float gamma22OetfSingleChannel(highp float linearChannel) {
// Reference:
// https://developer.android.com/reference/android/hardware/DataSpace#TRANSFER_gamma22
return pow(linearChannel, (1.0 / 2.2));
}
// BT.709 gamma 2.2 OETF.
vec3 gamma22Oetf(highp vec3 linearColor) {
return vec3(gamma22OetfSingleChannel(linearColor.r),
gamma22OetfSingleChannel(linearColor.g),
gamma22OetfSingleChannel(linearColor.b));
}
// Applies the appropriate OETF to convert linear optical signals to nonlinear
// electrical signals. Input and output are both normalized to [0, 1].
highp vec3 applyOetf(highp vec3 linearColor) {
// LINT.IfChange(color_transfer_oetf)
const int COLOR_TRANSFER_SDR_VIDEO = 3;
const int COLOR_TRANSFER_GAMMA_2_2 = 10;
if (uOutputColorTransfer == COLOR_TRANSFER_SDR_VIDEO) {
return smpte170mOetf(linearColor);
} else if (uOutputColorTransfer == COLOR_TRANSFER_GAMMA_2_2) {
return gamma22Oetf(linearColor);
} else {
// Output red as an obviously visible error.
return vec3(1.0, 0.0, 0.0);
}
}
void main() {
vec4 inputColor = texture2D(uTexSampler, vTexSamplingCoord);
vec4 transformedColors = uRgbMatrix * vec4(inputColor.rgb, 1);
gl_FragColor = vec4(applyOetf(transformedColors.rgb), inputColor.a);
}
@@ -0,0 +1,35 @@
#version 100
// Copyright 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ES2 vertex shader that tiles frames horizontally.
attribute vec4 aFramePosition;
uniform int uIndex;
uniform int uCount;
varying vec2 vTexSamplingCoord;
void main() {
// Translate the coordinates from -1,+1 to 0,+2.
float x = aFramePosition.x + 1.0;
// Offset the frame by its index times its width (2).
x += float(uIndex) * 2.0;
// Shrink the frame to fit the thumbnail strip.
x /= float(uCount);
// Translate the coordinates back to -1,+1.
x -= 1.0;
gl_Position = vec4(x, aFramePosition.yzw);
vTexSamplingCoord = aFramePosition.xy * 0.5 + 0.5;
}
@@ -0,0 +1,28 @@
#version 100
// Copyright 2021 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ES 2 vertex shader that applies the 4 * 4 transformation matrices
// uTransformationMatrix and the uTexTransformationMatrix.
attribute vec4 aFramePosition;
uniform mat4 uTransformationMatrix;
uniform mat4 uTexTransformationMatrix;
varying vec2 vTexSamplingCoord;
void main() {
gl_Position = uTransformationMatrix * aFramePosition;
vec4 texturePosition = vec4(aFramePosition.x * 0.5 + 0.5,
aFramePosition.y * 0.5 + 0.5, 0.0, 1.0);
vTexSamplingCoord = (uTexTransformationMatrix * texturePosition).xy;
}
@@ -0,0 +1,28 @@
#version 300 es
// Copyright 2021 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ES 3 vertex shader that applies the 4 * 4 transformation matrices
// uTransformationMatrix and the uTexTransformationMatrix.
in vec4 aFramePosition;
uniform mat4 uTransformationMatrix;
uniform mat4 uTexTransformationMatrix;
out vec2 vTexSamplingCoord;
void main() {
gl_Position = uTransformationMatrix * aFramePosition;
vec4 texturePosition = vec4(aFramePosition.x * 0.5 + 0.5,
aFramePosition.y * 0.5 + 0.5, 0.0, 1.0);
vTexSamplingCoord = (uTexTransformationMatrix * texturePosition).xy;
}
@@ -14,6 +14,7 @@ import androidx.core.os.HandlerCompat;
import com.fongmi.android.tv.event.EventIndex;
import com.fongmi.android.tv.ui.activity.CrashActivity;
import com.fongmi.android.tv.utils.CacheCleaner;
import com.fongmi.android.tv.utils.Notify;
import com.fongmi.hook.Hook;
import com.github.catvod.Init;
@@ -41,6 +42,8 @@ public class App extends Application {
private final Gson gson;
private final long time;
private Hook hook;
private final Runnable cleanTask;
private boolean appJustLaunched;
public App() {
instance = this;
@@ -48,6 +51,8 @@ public class App extends Application {
handler = HandlerCompat.createAsync(Looper.getMainLooper());
time = System.currentTimeMillis();
gson = new Gson();
cleanTask = this::checkCacheClean;
appJustLaunched = true;
}
public static App get() {
@@ -65,6 +70,14 @@ public class App extends Application {
public static Activity activity() {
return get().activity;
}
public static boolean isAppJustLaunched() {
return get().appJustLaunched;
}
public static void setAppLaunched() {
get().appJustLaunched = false;
}
public static void execute(Runnable runnable) {
get().executor.execute(runnable);
@@ -113,12 +126,15 @@ public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
Notify.createChannel();
Logger.addLogAdapter(getLogAdapter());
OkHttp.get().setProxy(Setting.getProxy());
OkHttp.get().setDoh(Doh.objectFrom(Setting.getDoh()));
EventBus.builder().addIndex(new EventIndex()).installDefaultEventBus();
CaocConfig.Builder.create().backgroundMode(CaocConfig.BACKGROUND_MODE_SILENT).errorActivity(CrashActivity.class).apply();
// 初始化自动缓存清理
initCacheCleaner();
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
@@ -133,6 +149,8 @@ public class App extends Application {
@Override
public void onActivityResumed(@NonNull Activity activity) {
if (activity != activity()) setActivity(activity);
// 应用回到前台时检查缓存
checkCacheClean();
}
@Override
@@ -156,6 +174,20 @@ public class App extends Application {
});
}
private void initCacheCleaner() {
CacheCleaner cleaner = CacheCleaner.get();
cleaner.setCacheThreshold(200 * 1024 * 1024); // 固定使用200MB阈值
// 定期检查缓存 (每30分钟)
post(cleanTask, 30 * 60 * 1000);
}
private void checkCacheClean() {
CacheCleaner.get().checkAndClean();
// 每30分钟定期检查缓存
post(cleanTask, 30 * 60 * 1000);
}
@Override
public PackageManager getPackageManager() {
return hook != null ? hook : getBaseContext().getPackageManager();
@@ -201,6 +201,22 @@ public class Setting {
Prefers.put("update", update);
}
public static boolean getAutoUpdateCheck() {
return Prefers.getBoolean("auto_update_check", false);
}
public static void putAutoUpdateCheck(boolean autoUpdateCheck) {
Prefers.put("auto_update_check", autoUpdateCheck);
}
public static boolean getUseCnMirror() {
return Prefers.getBoolean("use_cn_mirror", false);
}
public static void putUseCnMirror(boolean useCnMirror) {
Prefers.put("use_cn_mirror", useCnMirror);
}
public static boolean isCaption() {
return Prefers.getBoolean("caption");
}
@@ -300,4 +316,20 @@ public class Setting {
public static boolean hasCaption() {
return new Intent(Settings.ACTION_CAPTIONING_SETTINGS).resolveActivity(App.get().getPackageManager()) != null;
}
public static boolean isPrivacyAgreed() {
return Prefers.getBoolean("privacy_agreed_v1", false);
}
public static void setPrivacyAgreed(boolean agreed) {
Prefers.put("privacy_agreed_v1", agreed);
}
public static boolean isLiveTabVisible() {
return Prefers.getBoolean("live_tab_visible", true);
}
public static void putLiveTabVisible(boolean visible) {
Prefers.put("live_tab_visible", visible);
}
}
@@ -0,0 +1,270 @@
package com.fongmi.android.tv.api;
import java.util.Arrays;
import java.util.List;
/**
* 广告拦截器 - 内置常用广告域名库
*/
public class AdBlocker {
/**
* 赌博类广告域名(澳门新葡京等)
*/
private static final List<String> GAMBLING_ADS = Arrays.asList(
// 澳门博彩广告
".*\\..*葡京.*",
".*\\..*皇冠.*",
".*\\..*金沙.*",
".*\\..*威尼斯人.*",
".*\\..*永利.*",
".*aomen.*",
".*macau.*casino.*",
".*xpj.*\\..*",
".*xinpujing.*",
".*amdc.*\\.com",
".*\\.amdc\\.alipay\\.com",
// 常见博彩推广域名
".*\\.bz.*bet.*",
".*\\.casino.*",
".*\\.poker.*",
".*\\.betting.*",
".*\\.gamble.*",
".*wnsr.*\\..*",
".*js[0-9]+\\..*",
".*vn[0-9]+\\..*",
".*ag[0-9]+\\..*",
// 具体的博彩广告域名
"wan.51img1.com",
"iqiyi.hbuioo.com",
"vip.ffzyad.com",
"https.wshdsm.com",
"v.%E7%88%B1%E4%B8%8A%E5%A5%B9%E5%BD%B1%E9%99%A2.com"
);
/**
* 通用广告联盟域名
*/
private static final List<String> GENERAL_ADS = Arrays.asList(
// Google广告
"googleads.g.doubleclick.net",
"adservice.google.com",
"pagead2.googlesyndication.com",
"www.googletagmanager.com",
"static.doubleclick.net",
".*\\.doubleclick\\.net",
".*\\.googlesyndication\\.com",
// 百度广告
"cpro.baidu.com",
"pos.baidu.com",
"cbjs.baidu.com",
"hm.baidu.com",
".*\\.union\\.baidu\\.com",
// 淘宝/阿里广告
"mclick.simba.taobao.com",
"simba.m.taobao.com",
".*\\.tanx\\.com",
".*\\.mmstat\\.com",
".*\\.atm\\.youku\\.com",
// 腾讯广告
"mi.gdt.qq.com",
"adsmind.gdtimg.com",
".*\\.l\\.qq\\.com",
"pgdt.gtimg.cn",
// 其他主流广告联盟
"union.meituan.com",
"analytics.163.com",
"g.163.com",
"analytics.126.net",
".*\\.irs01\\.com",
".*\\.irs01\\.net"
);
/**
* 视频平台广告域名
*/
private static final List<String> VIDEO_ADS = Arrays.asList(
// 优酷广告
"atm.youku.com",
"stat.youku.com",
"ad.api.3g.youku.com",
"pl.youku.com",
"lstat.youku.com",
".*\\.atm\\.youku\\.com",
// 爱奇艺广告
"cupid.iqiyi.com",
"data.video.iqiyi.com",
"msg.71.am",
".*\\.cupid\\.iqiyi\\.com",
".*\\.data\\.video\\.iqiyi\\.com",
// 腾讯视频广告
"btrace.video.qq.com",
"mtrace.video.qq.com",
"vv.video.qq.com",
"ad.video.qq.com",
// 芒果TV广告
"da.mgtv.com",
"ad.hunantv.com",
"v2.hunantv.com",
// 其他视频平台
"ark.letv.com",
"stat.letv.com",
".*\\.beacon\\.qq\\.com"
);
/**
* 弹窗广告域名
*/
private static final List<String> POPUP_ADS = Arrays.asList(
// 常见弹窗广告
"mimg.0c1q0l.cn",
"www.92424.cn",
"k.jinxiuzhilv.com",
"cdn.bootcss.com",
"ppl.xunzhuo.com",
"xc.hubeijieshikj.cn",
"ssl.kdd.cc",
"push.zhanzhang.baidu.com",
"cpc.cmbchina.com",
"adshow.58.com",
// 移动端弹窗
"afp.csbew.com",
"aoodoo.feng.com",
"*.popin.cc",
"*.supersonicads.com"
);
/**
* 恶意网站和钓鱼网站
*/
private static final List<String> MALICIOUS_ADS = Arrays.asList(
".*\\.17un\\.com",
".*\\.baidustatic\\.com",
".*\\.cnzz\\.com",
".*\\.duomeng\\.cn",
".*\\.shuzilm\\.cn",
".*\\.haoyuemh\\.com",
".*\\.571xz\\.com",
".*\\.madthumbs\\.com"
);
/**
* 跟踪统计域名
*/
private static final List<String> TRACKING_ADS = Arrays.asList(
// 统计跟踪
"hm.baidu.com",
"tongji.baidu.com",
"s95.cnzz.com",
"cnzz.com",
".*\\.umeng\\.com",
".*\\.umtrack\\.com",
// Google Analytics
"www.google-analytics.com",
"ssl.google-analytics.com",
".*\\.googletagmanager\\.com"
);
/**
* 获取所有内置广告域名
* @return 完整的广告域名列表
*/
public static List<String> getAllAdHosts() {
return Arrays.asList(
// 合并所有列表
String.join(",", GAMBLING_ADS),
String.join(",", GENERAL_ADS),
String.join(",", VIDEO_ADS),
String.join(",", POPUP_ADS),
String.join(",", MALICIOUS_ADS),
String.join(",", TRACKING_ADS)
);
}
/**
* 获取赌博类广告域名(澳门新葡京等)
*/
public static List<String> getGamblingAdHosts() {
return GAMBLING_ADS;
}
/**
* 获取通用广告联盟域名
*/
public static List<String> getGeneralAdHosts() {
return GENERAL_ADS;
}
/**
* 获取视频平台广告域名
*/
public static List<String> getVideoAdHosts() {
return VIDEO_ADS;
}
/**
* 获取弹窗广告域名
*/
public static List<String> getPopupAdHosts() {
return POPUP_ADS;
}
/**
* 获取恶意网站域名
*/
public static List<String> getMaliciousAdHosts() {
return MALICIOUS_ADS;
}
/**
* 获取跟踪统计域名
*/
public static List<String> getTrackingAdHosts() {
return TRACKING_ADS;
}
/**
* 检查是否应该拦截该域名
* @param host 要检查的域名
* @return true=应该拦截, false=不拦截
*/
public static boolean shouldBlock(String host) {
if (host == null || host.isEmpty()) return false;
// 检查所有分类
return checkInList(host, GAMBLING_ADS) ||
checkInList(host, GENERAL_ADS) ||
checkInList(host, VIDEO_ADS) ||
checkInList(host, POPUP_ADS) ||
checkInList(host, MALICIOUS_ADS) ||
checkInList(host, TRACKING_ADS);
}
/**
* 检查域名是否在列表中(支持正则)
*/
private static boolean checkInList(String host, List<String> list) {
for (String pattern : list) {
if (host.matches(pattern.replace("*", ".*"))) {
return true;
}
if (host.contains(pattern.replace(".*", ""))) {
return true;
}
}
return false;
}
}
@@ -40,6 +40,13 @@ public class LiveConfig {
private boolean sync;
private Live home;
private LiveConfig() {
// 在构造函数中初始化列表,防止空指针异常
this.ads = new ArrayList<>();
this.rules = new ArrayList<>();
this.lives = new ArrayList<>();
}
private static class Loader {
static volatile LiveConfig INSTANCE = new LiveConfig();
}
@@ -97,9 +104,9 @@ public class LiveConfig {
public LiveConfig clear() {
this.home = null;
this.ads.clear();
this.rules.clear();
this.lives.clear();
if (this.ads != null) this.ads.clear();
if (this.rules != null) this.rules.clear();
if (this.lives != null) this.lives.clear();
return this;
}
@@ -37,6 +37,17 @@ public class VodConfig {
private Parse parse;
private String wall;
private Site home;
private volatile boolean isLoading = false; // 添加加载状态标记
private VodConfig() {
// 在构造函数中初始化列表,防止空指针异常
this.ads = new ArrayList<>();
this.doh = new ArrayList<>();
this.rules = new ArrayList<>();
this.sites = new ArrayList<>();
this.flags = new ArrayList<>();
this.parses = new ArrayList<>();
}
private static class Loader {
static volatile VodConfig INSTANCE = new VodConfig();
@@ -67,7 +78,44 @@ public class VodConfig {
}
public static void load(Config config, Callback callback) {
get().clear().config(config).load(callback);
android.util.Log.d("VodConfig", "load called with config: " + (config != null ? config.toString() : "null"));
// 参数检查
if (config == null || callback == null) {
android.util.Log.e("VodConfig", "Invalid parameters: config=" + config + ", callback=" + callback);
if (callback != null) {
App.post(() -> callback.error("配置参数无效"));
}
return;
}
android.util.Log.d("VodConfig", "Parameters valid, proceeding with load");
// 添加加载状态检查,防止并发加载
VodConfig instance = get();
synchronized (instance) {
if (instance.isLoading) {
android.util.Log.d("VodConfig", "Already loading, cancelling previous load");
// 如果正在加载,取消之前的加载
try {
OkHttp.cancel("vod");
} catch (Exception e) {
android.util.Log.e("VodConfig", "Error cancelling previous load", e);
e.printStackTrace();
}
}
instance.isLoading = true;
}
android.util.Log.d("VodConfig", "Calling instance.clear().config(config).load(callback)");
try {
instance.clear().config(config).load(callback);
} catch (Exception e) {
instance.isLoading = false;
android.util.Log.e("VodConfig", "Exception during load", e);
e.printStackTrace();
App.post(() -> callback.error("配置加载失败: " + e.getMessage()));
}
}
public VodConfig init() {
@@ -94,12 +142,12 @@ public class VodConfig {
this.wall = null;
this.home = null;
this.parse = null;
this.ads.clear();
this.doh.clear();
this.rules.clear();
this.sites.clear();
this.flags.clear();
this.parses.clear();
if (this.ads != null) this.ads.clear();
if (this.doh != null) this.doh.clear();
if (this.rules != null) this.rules.clear();
if (this.sites != null) this.sites.clear();
if (this.flags != null) this.flags.clear();
if (this.parses != null) this.parses.clear();
this.loadLive = true;
BaseLoader.get().clear();
return this;
@@ -114,15 +162,23 @@ public class VodConfig {
OkHttp.cancel("vod");
checkJson(Json.parse(Decoder.getJson(UrlUtil.convert(config.getUrl()), "vod")).getAsJsonObject(), callback);
} catch (Throwable e) {
if (TextUtils.isEmpty(config.getUrl())) App.post(() -> callback.error(""));
else loadCache(callback, e);
if (TextUtils.isEmpty(config.getUrl())) {
isLoading = false;
App.post(() -> callback.error(""));
} else {
loadCache(callback, e);
}
e.printStackTrace();
}
}
private void loadCache(Callback callback, Throwable e) {
if (!TextUtils.isEmpty(config.getJson())) checkJson(Json.parse(config.getJson()).getAsJsonObject(), callback);
else App.post(() -> callback.error(Notify.getError(R.string.error_config_get, e)));
if (!TextUtils.isEmpty(config.getJson())) {
checkJson(Json.parse(config.getJson()).getAsJsonObject(), callback);
} else {
isLoading = false;
App.post(() -> callback.error(Notify.getError(R.string.error_config_get, e)));
}
}
private void checkJson(JsonObject object, Callback callback) {
@@ -152,11 +208,21 @@ public class VodConfig {
if (loadLive && object.has("lives")) initLive(object);
String notice = Json.safeString(object, "notice");
config.logo(Json.safeString(object, "logo"));
App.post(() -> callback.success(notice));
config.json(object.toString()).update();
App.post(callback::success);
// 重置加载状态
isLoading = false;
// 只调用一次success回调,优先显示通知消息
if (!TextUtils.isEmpty(notice)) {
App.post(() -> callback.success(notice));
} else {
App.post(callback::success);
}
} catch (Throwable e) {
e.printStackTrace();
// 重置加载状态
isLoading = false;
App.post(() -> callback.error(Notify.getError(R.string.error_config_parse, e)));
}
}
@@ -167,14 +233,26 @@ public class VodConfig {
return;
}
String spider = Json.safeString(object, "spider");
BaseLoader.get().parseJar(spider, true);
try {
BaseLoader.get().parseJar(spider, true);
} catch (Throwable e) {
android.util.Log.e("VodConfig", "Failed to parse spider jar: " + spider, e);
e.printStackTrace();
}
for (JsonElement element : Json.safeListElement(object, "sites")) {
Site site = Site.objectFrom(element);
if (sites.contains(site)) continue;
site.setApi(UrlUtil.convert(site.getApi()));
site.setExt(UrlUtil.convert(site.getExt()));
site.setJar(parseJar(site, spider));
sites.add(site.trans().sync());
try {
Site site = Site.objectFrom(element);
if (sites.contains(site)) continue;
site.setApi(UrlUtil.convert(site.getApi()));
site.setExt(UrlUtil.convert(site.getExt()));
site.setJar(parseJar(site, spider));
sites.add(site.trans().sync());
} catch (Throwable e) {
android.util.Log.e("VodConfig", "Failed to add site: " + element, e);
e.printStackTrace();
// 继续处理下一个站点
}
}
for (Site site : sites) {
if (site.getKey().equals(config.getHome())) {
@@ -318,10 +396,32 @@ public class VodConfig {
}
public void setHome(Site home) {
if (home == null) {
// 如果传入null,使用默认站点或创建空站点
home = sites.isEmpty() ? new Site() : sites.get(0);
}
this.home = home;
this.home.setActivated(true);
config.home(home.getKey()).save();
for (Site item : getSites()) item.setActivated(home);
// 安全地保存配置,防止空指针异常
try {
if (home.getKey() != null && config != null) {
config.home(home.getKey()).save();
}
} catch (Exception e) {
e.printStackTrace();
}
// 安全地更新所有站点的激活状态
try {
for (Site item : getSites()) {
if (item != null) {
item.setActivated(home);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void setWall(String wall) {
@@ -46,10 +46,15 @@ public class JarLoader {
}
private void load(String key, File file) {
if (!file.setReadOnly()) return;
loaders.put(key, dex(file));
invokeInit(key);
putProxy(key);
try {
if (!file.setReadOnly()) return;
loaders.put(key, dex(file));
invokeInit(key);
putProxy(key);
} catch (Throwable e) {
android.util.Log.e("JarLoader", "Failed to load jar for key: " + key, e);
e.printStackTrace();
}
}
private DexClassLoader dex(File file) {
@@ -85,19 +90,24 @@ public class JarLoader {
}
public synchronized void parseJar(String key, String jar) {
if (loaders.containsKey(key)) return;
String[] texts = jar.split(";md5;");
String md5 = texts.length > 1 ? texts[1].trim() : "";
if (md5.startsWith("http")) md5 = OkHttp.string(md5).trim();
jar = texts[0];
if (!md5.isEmpty() && Util.equals(jar, md5)) {
load(key, Path.jar(jar));
} else if (jar.startsWith("http")) {
load(key, download(jar));
} else if (jar.startsWith("file")) {
load(key, Path.local(jar));
} else if (jar.startsWith("assets")) {
parseJar(key, UrlUtil.convert(jar));
try {
if (loaders.containsKey(key)) return;
String[] texts = jar.split(";md5;");
String md5 = texts.length > 1 ? texts[1].trim() : "";
if (md5.startsWith("http")) md5 = OkHttp.string(md5).trim();
jar = texts[0];
if (!md5.isEmpty() && Util.equals(jar, md5)) {
load(key, Path.jar(jar));
} else if (jar.startsWith("http")) {
load(key, download(jar));
} else if (jar.startsWith("file")) {
load(key, Path.local(jar));
} else if (jar.startsWith("assets")) {
parseJar(key, UrlUtil.convert(jar));
}
} catch (Throwable e) {
android.util.Log.e("JarLoader", "Failed to parse jar for key: " + key + ", jar: " + jar, e);
e.printStackTrace();
}
}
@@ -267,6 +267,19 @@ public class Config {
Keep.delete(getId());
}
public Config copy() {
Config copy = new Config();
copy.setType(type);
copy.setUrl(url);
copy.setJson(json);
copy.setName(TextUtils.isEmpty(name) ? url + " 副本" : name + " 副本");
copy.setLogo(logo);
copy.setHome(home);
copy.setParse(parse);
copy.setTime(System.currentTimeMillis());
return copy;
}
@NonNull
@Override
public String toString() {
@@ -17,7 +17,7 @@ public abstract class ConfigDao extends BaseDao<Config> {
@Query("SELECT * FROM Config WHERE type = :type ORDER BY time DESC")
public abstract List<Config> findByType(int type);
@SuppressWarnings(RoomWarnings.QUERY_MISMATCH)
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
@Query("SELECT id, name, url, type, time FROM Config WHERE type = :type ORDER BY time DESC")
public abstract List<Config> findUrlByType(int type);
@@ -2,7 +2,9 @@ package com.fongmi.android.tv.ui.custom;
import android.content.Context;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.widget.FrameLayout;
import android.widget.TextView;
@@ -33,6 +35,7 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
private long currentPosition;
private long currentBuffered;
private boolean scrubbing;
private boolean isPressed;
public CustomSeekView(Context context) {
this(context, null);
@@ -55,11 +58,71 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
timeBar = findViewById(R.id.timeBar);
timeBar.addListener(this);
refresh = this::refresh;
// 添加触摸事件处理实现按住时圆球变大的效果
timeBar.setOnTouchListener((v, event) -> {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!isPressed) {
isPressed = true;
// 按住时轨道变高到4dp
setTimeBarHeight(4);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (isPressed) {
isPressed = false;
// 松开时轨道恢复到2dp
setTimeBarHeight(2);
}
break;
}
return false; // 不拦截事件让DefaultTimeBar正常处理
});
}
public void setListener(Players player) {
this.player = player;
}
public void setPosition(long position) {
timeBar.setPosition(position);
}
public void setDuration(long duration) {
timeBar.setDuration(duration);
}
/**
* 动态调整进度条高度
* @param barHeightDp 轨道高度dp
*/
private void setTimeBarHeight(int barHeightDp) {
int barHeightPx = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
barHeightDp,
getContext().getResources().getDisplayMetrics()
);
// 尝试通过反射设置DefaultTimeBar的内部barHeight字段
try {
java.lang.reflect.Field barHeightField = timeBar.getClass().getDeclaredField("barHeight");
barHeightField.setAccessible(true);
barHeightField.setInt(timeBar, barHeightPx);
// 强制刷新
timeBar.invalidate();
timeBar.requestLayout();
} catch (Exception e) {
// 如果反射失败尝试调整布局参数
android.util.Log.w("CustomSeekView", "Failed to set bar height via reflection: " + e.getMessage());
if (timeBar.getLayoutParams() != null) {
timeBar.getLayoutParams().height = barHeightPx;
timeBar.requestLayout();
}
}
}
private void start() {
removeCallbacks(refresh);
@@ -102,7 +165,7 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
}
}
private void setKeyTimeIncrement(long duration) {
public void setKeyTimeIncrement(long duration) {
if (duration > TimeUnit.HOURS.toMillis(3)) {
timeBar.setKeyTimeIncrement(TimeUnit.MINUTES.toMillis(5));
} else if (duration > TimeUnit.MINUTES.toMillis(30)) {
@@ -124,8 +187,22 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
}
private void seekToTimeBarPosition(long positionMs) {
// 先设置播放位置
player.seekTo(positionMs);
refresh();
// 延迟刷新进度条确保播放器已经处理了跳转操作
removeCallbacks(refresh);
postDelayed(() -> {
// 只有在非拖动状态下才刷新进度条位置
if (!scrubbing) {
refresh();
// 确保进度条位置与实际播放位置一致
long actualPosition = player.getPosition();
if (Math.abs(actualPosition - positionMs) > 100) { // 如果差异超过100ms再次调整
timeBar.setPosition(actualPosition);
positionView.setText(player.stringToTime(actualPosition));
}
}
}, 100); // 增加延迟时间确保拖拽状态完全结束
}
@Override
@@ -148,6 +225,20 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
@Override
public void onScrubStop(@NonNull TimeBar timeBar, long position, boolean canceled) {
scrubbing = false;
if (!canceled) seekToTimeBarPosition(position);
if (!canceled) {
// 立即设置进度条位置到目标位置避免圆球跳回原始位置
timeBar.setPosition(position);
positionView.setText(player.stringToTime(position));
// 调整播放位置
seekToTimeBarPosition(position);
// 确保播放状态正确
if (!player.isPlaying()) {
player.play();
}
}
// 不干预DefaultTimeBar的圆球绘制让它自己处理
}
}
@@ -19,6 +19,7 @@ import androidx.annotation.NonNull;
import com.fongmi.android.tv.App;
import com.fongmi.android.tv.Constant;
import com.fongmi.android.tv.Setting;
import com.fongmi.android.tv.api.AdBlocker;
import com.fongmi.android.tv.api.config.LiveConfig;
import com.fongmi.android.tv.api.config.VodConfig;
import com.fongmi.android.tv.impl.ParseCallback;
@@ -177,8 +178,13 @@ public class CustomWebView extends WebView implements DialogInterface.OnDismissL
}
private boolean isAd(String host) {
// 1. 首先检查内置广告域名库包含常见的澳门新葡京等赌博广告
if (AdBlocker.shouldBlock(host)) return true;
// 2. 然后检查用户自定义的广告域名
for (String ad : VodConfig.get().getAds()) if (Util.containOrMatch(host, ad)) return true;
for (String ad : LiveConfig.get().getAds()) if (Util.containOrMatch(host, ad)) return true;
return false;
}
@@ -9,6 +9,7 @@ import android.widget.RelativeLayout;
import com.fongmi.android.tv.databinding.ViewEmptyBinding;
import com.fongmi.android.tv.databinding.ViewProgressBinding;
import com.airbnb.lottie.LottieAnimationView;
import java.util.ArrayList;
import java.util.List;
@@ -47,7 +48,8 @@ public class ProgressLayout extends RelativeLayout {
}
private void initView() {
mEmptyView = ViewEmptyBinding.inflate(LayoutInflater.from(getContext())).getRoot();
// 使用新的Lottie动画空状态布局
mEmptyView = LayoutInflater.from(getContext()).inflate(com.fongmi.android.tv.R.layout.view_empty_lottie, null);
mEmptyView.setTag(TAG_PROGRESS);
mEmptyView.setVisibility(GONE);
mProgressView = ViewProgressBinding.inflate(LayoutInflater.from(getContext())).getRoot();
@@ -103,21 +105,46 @@ public class ProgressLayout extends RelativeLayout {
case CONTENT:
mEmptyView.setVisibility(GONE);
mProgressView.setVisibility(GONE);
pauseLottieAnimation();
setContentVisibility(true);
break;
case PROGRESS:
mEmptyView.setVisibility(GONE);
mProgressView.setVisibility(VISIBLE);
pauseLottieAnimation();
setContentVisibility(false);
break;
case EMPTY:
mEmptyView.setVisibility(VISIBLE);
mProgressView.setVisibility(GONE);
playLottieAnimation();
setContentVisibility(false);
break;
}
}
private void playLottieAnimation() {
try {
LottieAnimationView lottieView = mEmptyView.findViewById(com.fongmi.android.tv.R.id.lottieAnimation);
if (lottieView != null) {
lottieView.playAnimation();
}
} catch (Exception e) {
// 忽略错误保持兼容性
}
}
private void pauseLottieAnimation() {
try {
LottieAnimationView lottieView = mEmptyView.findViewById(com.fongmi.android.tv.R.id.lottieAnimation);
if (lottieView != null) {
lottieView.pauseAnimation();
}
} catch (Exception e) {
// 忽略错误保持兼容性
}
}
private void setContentVisibility(boolean visible) {
for (View view : mContentViews) {
if (visible) showView(view);
@@ -0,0 +1,53 @@
package com.fongmi.android.tv.ui.dialog;
import android.app.Activity;
import android.view.LayoutInflater;
import androidx.appcompat.app.AlertDialog;
import com.fongmi.android.tv.bean.History;
import com.fongmi.android.tv.databinding.DialogLastWatchBinding;
import com.fongmi.android.tv.ui.activity.VideoActivity;
import com.fongmi.android.tv.utils.ResUtil;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
public class LastWatchDialog {
private final DialogLastWatchBinding binding;
private final AlertDialog dialog;
private final Activity activity;
private final History history;
public static LastWatchDialog create(Activity activity, History history) {
return new LastWatchDialog(activity, history);
}
private LastWatchDialog(Activity activity, History history) {
this.activity = activity;
this.history = history;
this.binding = DialogLastWatchBinding.inflate(LayoutInflater.from(activity));
this.dialog = new MaterialAlertDialogBuilder(activity).setView(binding.getRoot()).create();
}
public void show() {
initView();
initEvent();
dialog.getWindow().setDimAmount(0.5f);
dialog.show();
}
private void initView() {
binding.content.setText(history.getVodName());
}
private void initEvent() {
binding.play.setOnClickListener(v -> {
dismiss();
VideoActivity.start(activity, history.getSiteKey(), history.getVodId(), history.getVodName(), history.getVodPic());
});
}
private void dismiss() {
dialog.dismiss();
}
}
@@ -0,0 +1,115 @@
package com.fongmi.android.tv.ui.dialog;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.os.Handler;
import android.os.Looper;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.widget.PopupWindow;
import android.widget.TextView;
import com.fongmi.android.tv.R;
import com.fongmi.android.tv.bean.History;
import com.fongmi.android.tv.ui.activity.VideoActivity;
public class LastWatchToast {
private final Activity activity;
private final History history;
private final Handler handler;
private PopupWindow popupWindow;
private View contentView;
private static final int ANIMATION_DURATION = 300; // 动画持续时间(毫秒)
private static final int DISPLAY_DURATION = 2500; // 显示持续时间(毫秒)
public static LastWatchToast create(Activity activity, History history) {
return new LastWatchToast(activity, history);
}
private LastWatchToast(Activity activity, History history) {
this.activity = activity;
this.history = history;
this.handler = new Handler(Looper.getMainLooper());
}
public void show() {
if (popupWindow != null && popupWindow.isShowing()) {
// 如果已经显示先取消当前显示的然后重新显示
dismiss();
}
contentView = LayoutInflater.from(activity).inflate(R.layout.view_last_watch_toast, null);
TextView title = contentView.findViewById(R.id.title);
TextView content = contentView.findViewById(R.id.content);
title.setText(R.string.last_watch);
content.setText(history.getVodName());
// 设置点击事件
contentView.setOnClickListener(v -> {
dismiss();
VideoActivity.start(activity, history.getSiteKey(), history.getVodId(), history.getVodName(), history.getVodPic());
});
// 初始化时设置透明度为0准备执行淡入动画
contentView.setAlpha(0f);
// 创建PopupWindow
popupWindow = new PopupWindow(contentView,
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
true);
// 设置背景为透明避免PopupWindow有默认背景
popupWindow.setBackgroundDrawable(null);
popupWindow.setOutsideTouchable(true);
// 在屏幕中央显示
popupWindow.showAtLocation(activity.getWindow().getDecorView(), Gravity.CENTER, 0, 0);
// 淡入动画
animateIn();
// 一段时间后自动关闭
handler.removeCallbacksAndMessages(null);
handler.postDelayed(this::animateOut, DISPLAY_DURATION);
}
private void animateIn() {
ObjectAnimator fadeIn = ObjectAnimator.ofFloat(contentView, "alpha", 0f, 1f);
fadeIn.setDuration(ANIMATION_DURATION);
fadeIn.setInterpolator(new DecelerateInterpolator());
fadeIn.start();
}
private void animateOut() {
if (contentView == null || popupWindow == null || !popupWindow.isShowing()) return;
ObjectAnimator fadeOut = ObjectAnimator.ofFloat(contentView, "alpha", 1f, 0f);
fadeOut.setDuration(ANIMATION_DURATION);
fadeOut.setInterpolator(new AccelerateInterpolator());
fadeOut.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
dismiss();
}
});
fadeOut.start();
}
private void dismiss() {
handler.removeCallbacksAndMessages(null);
if (popupWindow != null && popupWindow.isShowing()) {
popupWindow.dismiss();
}
popupWindow = null;
contentView = null;
}
}
@@ -0,0 +1,92 @@
package com.fongmi.android.tv.utils;
import android.os.StatFs;
import com.fongmi.android.tv.App;
import com.fongmi.android.tv.impl.Callback;
import com.github.catvod.utils.Path;
/**
* 缓存自动清理管理器
*/
public class CacheCleaner {
// 默认缓存清理阈值 200MB
private static final long DEFAULT_CACHE_THRESHOLD = 200 * 1024 * 1024;
// 最小保留空间 500MB
private static final long MIN_FREE_SPACE = 500 * 1024 * 1024;
// 单例实例
private static CacheCleaner instance;
// 缓存清理阈值
private long cacheThreshold;
private CacheCleaner() {
this.cacheThreshold = DEFAULT_CACHE_THRESHOLD;
}
public static CacheCleaner get() {
if (instance == null) {
synchronized (CacheCleaner.class) {
if (instance == null) {
instance = new CacheCleaner();
}
}
}
return instance;
}
/**
* 设置缓存阈值
* @param threshold 阈值大小字节
*/
public void setCacheThreshold(long threshold) {
this.cacheThreshold = threshold;
}
/**
* 检查缓存如果超过阈值则清理
*/
public void checkAndClean() {
App.execute(() -> {
try {
// 获取当前缓存大小
long cacheSize = FileUtil.getDirectorySize(Path.cache());
// 获取剩余存储空间
long freeSpace = getAvailableStorageSpace();
// 如果缓存超过阈值或可用空间低于最小要求清理缓存
if (cacheSize > cacheThreshold || freeSpace < MIN_FREE_SPACE) {
cleanCache();
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
/**
* 清理缓存
*/
private void cleanCache() {
FileUtil.clearCache(new Callback() {
@Override
public void success() {
// 缓存清理成功
}
});
}
/**
* 获取设备可用存储空间
* @return 可用空间字节
*/
private long getAvailableStorageSpace() {
try {
StatFs stat = new StatFs(Path.cache().getPath());
return stat.getAvailableBlocksLong() * stat.getBlockSizeLong();
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
@@ -2,11 +2,13 @@ package com.fongmi.android.tv.utils;
import com.fongmi.android.tv.App;
import com.github.catvod.net.OkHttp;
import com.github.catvod.utils.Logger;
import com.github.catvod.utils.Path;
import com.google.common.net.HttpHeaders;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
@@ -47,14 +49,22 @@ public class Download {
private void doInBackground() {
try (Response res = OkHttp.newCall(url, url).execute()) {
Path.create(file);
download(res.body().byteStream(), Double.parseDouble(res.header(HttpHeaders.CONTENT_LENGTH, "1")));
long expectedLength = Long.parseLong(res.header(HttpHeaders.CONTENT_LENGTH, "0"));
download(res.body().byteStream(), expectedLength);
// 验证下载的文件
if (!verifyDownloadedFile(file, expectedLength)) {
App.post(() -> {if (callback != null) callback.error("下载的文件可能已损坏,请重试");});
return;
}
App.post(() -> {if (callback != null) callback.success(file);});
} catch (Exception e) {
App.post(() -> {if (callback != null) callback.error(e.getMessage());});
}
}
private void download(InputStream is, double length) throws Exception {
private void download(InputStream is, long length) throws Exception {
try (BufferedInputStream input = new BufferedInputStream(is); FileOutputStream os = new FileOutputStream(file)) {
byte[] buffer = new byte[4096];
int readBytes;
@@ -68,6 +78,45 @@ public class Download {
}
}
private boolean verifyDownloadedFile(File file, long expectedLength) {
try {
// 检查文件大小
if (file.length() != expectedLength) {
Logger.e("File size mismatch: expected " + expectedLength + ", actual " + file.length());
return false;
}
// 检查APK文件头 (ZIP文件头)
if (file.length() < 4) return false;
try (FileInputStream fis = new FileInputStream(file)) {
byte[] header = new byte[4];
fis.read(header);
// ZIP文件头应该是 0x504B0304 (PK..)
if (header[0] != 0x50 || header[1] != 0x4B || header[2] != 0x03 || header[3] != 0x04) {
Logger.e("Invalid APK file header");
return false;
}
// 额外验证检查APK文件是否完整
// 尝试读取ZIP文件结构
fis.getChannel().position(0);
byte[] buffer = new byte[1024];
int bytesRead = fis.read(buffer);
if (bytesRead < 4) {
Logger.e("APK file too small or corrupted");
return false;
}
}
Logger.d("APK file verification passed: " + file.getName() + " (" + file.length() + " bytes)");
return true;
} catch (Exception e) {
Logger.e("File verification failed: " + e.getMessage());
return false;
}
}
public interface Callback {
void progress(int progress);
@@ -11,6 +11,7 @@ import androidx.core.content.FileProvider;
import com.fongmi.android.tv.App;
import com.fongmi.android.tv.R;
import com.fongmi.android.tv.impl.Callback;
import com.github.catvod.utils.Logger;
import com.github.catvod.utils.Path;
import java.io.BufferedInputStream;
@@ -34,11 +35,35 @@ public class FileUtil {
}
public static void openFile(File file) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(getShareUri(file), FileUtil.getMimeType(file.getName()));
App.get().startActivity(intent);
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// 对于APK文件使用特定的MIME类型
String mimeType = file.getName().toLowerCase().endsWith(".apk") ?
"application/vnd.android.package-archive" : getMimeType(file.getName());
intent.setDataAndType(getShareUri(file), mimeType);
// 添加额外的安装权限检查
if (file.getName().toLowerCase().endsWith(".apk")) {
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
App.get().startActivity(intent);
} catch (Exception e) {
Logger.e("Failed to open file: " + e.getMessage());
// 如果失败尝试使用通用方式
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(getShareUri(file), "*/*");
App.get().startActivity(intent);
} catch (Exception ex) {
Logger.e("Fallback open file also failed: " + ex.getMessage());
}
}
}
public static void gzipCompress(File target) {
@@ -4,8 +4,11 @@ import android.Manifest;
import android.app.Notification;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Gravity;
import android.widget.TextView;
import android.widget.Toast;
@@ -25,6 +28,7 @@ public class Notify {
public static final int ID = 9527;
private AlertDialog mDialog;
private Toast mToast;
private Handler mHandler;
private static class Loader {
static volatile Notify INSTANCE = new Notify();
@@ -57,6 +61,14 @@ public class Notify {
get().makeText(text);
}
public static void showCenter(int resId) {
if (resId != 0) showCenter(ResUtil.getString(resId));
}
public static void showCenter(String text) {
get().makeTextCenter(text);
}
public static void progress(Context context) {
dismiss();
get().create(context);
@@ -78,12 +90,38 @@ public class Notify {
private void makeText(String message) {
if (mToast != null) mToast.cancel();
if (mHandler == null) mHandler = new Handler(Looper.getMainLooper());
if (TextUtils.isEmpty(message)) return;
mToast = new Toast(App.get());
TextView view = (TextView) LayoutInflater.from(App.get()).inflate(R.layout.view_toast, null);
view.setText(message);
mToast.setView(view);
mToast.setDuration(Toast.LENGTH_LONG);
mToast.setDuration(Toast.LENGTH_SHORT);
mToast.show();
// 1秒后取消Toast
mHandler.removeCallbacksAndMessages(null);
mHandler.postDelayed(() -> {
if (mToast != null) mToast.cancel();
}, 1000); // 1000毫秒 = 1秒
}
private void makeTextCenter(String message) {
if (mToast != null) mToast.cancel();
if (mHandler == null) mHandler = new Handler(Looper.getMainLooper());
if (TextUtils.isEmpty(message)) return;
mToast = new Toast(App.get());
TextView view = (TextView) LayoutInflater.from(App.get()).inflate(R.layout.view_toast, null);
view.setText(message);
mToast.setView(view);
mToast.setDuration(Toast.LENGTH_SHORT);
mToast.setGravity(Gravity.CENTER, 0, 0);
mToast.show();
// 1秒后取消Toast
mHandler.removeCallbacksAndMessages(null);
mHandler.postDelayed(() -> {
if (mToast != null) mToast.cancel();
}, 1000); // 1000毫秒 = 1秒
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M13,2L3,14h8l-1,8l10,-12h-8l1,-8z"/>
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM12,5c1.66,0 3,1.34 3,3s-1.34,3 -3,3S9,9.66 9,8S10.34,5 12,5zM12,19.2c-2.5,0 -4.71,-1.28 -6,-3.22c0.03,-1.99 4,-3.08 6,-3.08c1.99,0 5.97,1.09 6,3.08C16.71,17.92 14.5,19.2 12,19.2z"/>
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5 5,-2.24 5,-5 -2.24,-5 -5,-5zM15.19,14.85L13.2,12.86c.5,-1.19.25,-2.58 -0.73,-3.55 -1.17,-1.18 -3.01,-1.39 -4.39,-0.51l1.99,1.99c0.22,0.22 0.22,0.57 0,0.79l-0.79,0.79c-0.22,0.22 -0.57,0.22 -0.79,0L6.5,10.38c-0.88,1.37 -0.67,3.22 0.51,4.39 0.97,0.98 2.36,1.23 3.55,0.73l1.99,1.99c0.22,0.22 0.57,0.22 0.79,0l0.79,-0.79c0.22,-0.22 0.22,-0.57 0,-0.79z"/>
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,19c1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3s-3,1.34 -3,3S10.34,19 12,19zM12,17c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1s1,0.45 1,1S12.55,17 12,17zM17,9c0,-2.76 -2.24,-5 -5,-5S7,6.24 7,9c0,1.59 0.76,3 1.95,3.91C7.58,13.71 6.5,15.27 6.5,17c0,0.55 0.45,1 1,1s1,-0.45 1,-1c0,-1.65 1.35,-3 3,-3s3,1.35 3,3c0,0.55 0.45,1 1,1s1,-0.45 1,-1c0,-1.73 -1.08,-3.29 -2.45,-4.09C15.24,12 16,10.59 16,9zM12,12c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3s3,1.34 3,3S13.66,12 12,12z"/>
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19,5v14L5,19L5,5h14m1.1,-2L3.9,3c-0.5,0 -0.9,0.4 -0.9,0.9v16.2c0,0.4 0.4,0.9 0.9,0.9h16.2c0.4,0 0.9,-0.5 0.9,-0.9L21,3.9c0,-0.5 -0.4,-0.9 -0.9,-0.9zM11,7h6v2h-6L11,7zM11,11h6v2h-6v-2zM11,15h6v2h-6zM7,7h2v2L7,9zM7,11h2v2L7,13zM7,15h2v2L7,17z"/>
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,3C8.69,3 6,5.69 6,9s2.69,6 6,6s6,-2.69 6,-6S15.31,3 12,3zM12,13c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4s4,1.79 4,4S14.21,13 12,13zM21,18h-2v2h-1.5v-2h-2v-1.5h2v-2h1.5v2h2V18zM11,18c0,-0.28 0.05,-0.54 0.12,-0.8c-0.87,-0.54 -1.89,-0.88 -3,-0.99C5.73,15.91 3,16.71 3,18.5V20h8.26C11.1,19.36 11,18.69 11,18z"/>
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M21,15h2v2h-2v-2zM21,11h2v2h-2v-2zM23,19h-2v2c1,0 2,-1 2,-2zM13,3h2v2h-2L13,3zM21,7h2v2h-2L21,7zM21,3v2h2c0,-1 -1,-2 -2,-2zM1,7h2v2L1,9L1,7zM17,3h2v2h-2L17,3zM17,19h2v2h-2v-2zM3,3C2,3 1,4 1,5h2L3,3zM9,3h2v2L9,5L9,3zM5,3h2v2L5,5L5,3zM1,11v8c0,1.1 0.9,2 2,2h12L15,11L1,11zM3,19l2.5,-3.21 1.79,2.15 2.5,-3.22L13,19L3,19z"/>
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M21,3H3C2,3 1,4 1,5v14c0,1.1 0.9,2 2,2h18c1,0 2,-1 2,-2V5c0,-1 -1,-2 -2,-2zM21,19H3V5h18v14zM14.5,11.5c0,0.83 -0.67,1.5 -1.5,1.5h-2v2h2v1h-3v-4c0,-0.83 0.67,-1.5 1.5,-1.5h2c0.83,0 1.5,0.67 1.5,1.5zM11.5,8.5H13v1h-2V7h2v1h-1.5zM16,13.5c0,0.83 -0.67,1.5 -1.5,1.5h-2v-2h2v-1h-2v-2h2c0.83,0 1.5,0.67 1.5,1.5v2z"/>
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M18.5,8C19.88,8 21,6.88 21,5.5C21,4.12 19.88,3 18.5,3C17.12,3 16,4.12 16,5.5C16,6.88 17.12,8 18.5,8zM5.5,8C6.88,8 8,6.88 8,5.5C8,4.12 6.88,3 5.5,3C4.12,3 3,4.12 3,5.5C3,6.88 4.12,8 5.5,8zM7.5,17H16.5C17.6,17 18.5,16.1 18.5,15V11.41C18.5,10.08 16.92,9.43 16,10.36C14.55,11.8 13.45,11.8 12,10.36C10.55,8.91 9.45,8.91 8,10.36C7.07,11.29 5.5,10.64 5.5,9.31V15C5.5,16.1 6.4,17 7.5,17z"/>
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M15,8v8H5V8h10m1,-2H4C3.45,6 3,6.45 3,7v10c0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1v-3.5l4,4v-11l-4,4V7c0,-0.55 -0.45,-1 -1,-1z"/>
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,16c-1.79,0 -3.23,-1.43 -3.23,-3.23 0,-0.36 0.06,-0.7 0.17,-1.02l4.08,4.08c-0.32,0.11 -0.66,0.17 -1.02,0.17zM12,8c1.79,0 3.23,1.43 3.23,3.23 0,0.36 -0.06,0.7 -0.17,1.02L10.98,8.17C11.3,8.06 11.64,8 12,8zM19.94,19.5L17.77,17.33C16.68,18.15 15.39,18.75 14,19.08v-2.11c0.63,-0.23 1.21,-0.54 1.73,-0.92l-1.4,-1.4c-0.64,0.51 -1.44,0.81 -2.33,0.81 -2.09,0 -3.77,-1.68 -3.77,-3.77 0,-0.89 0.31,-1.69 0.81,-2.33L7.65,8.97c-0.92,0.74 -1.73,1.61 -2.38,2.57C5.09,11.83 5,12.15 5,12.5s0.09,0.67 0.27,0.96c1.87,2.95 5.07,4.77 8.73,4.77 1.35,0 2.63,-0.22 3.83,-0.61l-0.39,-0.39 2.5,2.5c0.39,0.39 1.02,0.39 1.41,0 0.4,-0.39 0.4,-1.02 0,-1.41l-1.41,-1.41zM5.06,4.5l2.18,2.18c1.09,-0.82 2.37,-1.42 3.76,-1.76L11,7.03c-0.63,0.23 -1.21,0.54 -1.73,0.92l1.4,1.4c0.64,-0.51 1.44,-0.81 2.33,-0.81 2.09,0 3.77,1.68 3.77,3.77 0,0.89 -0.31,1.69 -0.81,2.33l1.4,1.4c0.92,-0.74 1.73,-1.61 2.38,-2.57 0.18,-0.29 0.27,-0.61 0.27,-0.96s-0.09,-0.67 -0.27,-0.96C18.87,8.6 15.67,6.78 12,6.78c-1.35,0 -2.63,0.22 -3.83,0.61L5.06,4.27c-0.39,-0.39 -1.02,-0.39 -1.41,0s-0.39,1.02 0,1.41l1.41,1.41z"/>
</vector>
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?attr/colorPrimary" />
<corners android:radius="4dp" />
</shape>
+1 -1
View File
@@ -7,7 +7,7 @@
android:topLeftRadius="8dp"
android:topRightRadius="8dp" />
<solid android:color="#B32196F3" />
<solid android:color="#B3000000" />
<padding
android:bottom="4dp"
@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:fitsSystemWindows="true"
android:orientation="vertical">
<!-- 标题栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/privacy_agreement_title"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/privacy_agreement_tip"
android:textColor="@color/white"
android:textSize="14sp"
android:alpha="0.8" />
</LinearLayout>
<!-- 协议内容滚动区域 -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:paddingBottom="16dp">
<TextView
android:id="@+id/contentText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/privacy_agreement_content"
android:textColor="@color/white"
android:textSize="14sp"
android:lineSpacingMultiplier="1.4"
android:padding="16dp"
android:background="@drawable/selector_item_round"
android:alpha="0.9" />
</ScrollView>
<!-- 按钮区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="8dp"
android:paddingBottom="24dp"
android:paddingStart="24dp"
android:paddingEnd="24dp">
<Button
android:id="@+id/disagreeButton"
android:layout_width="0dp"
android:layout_height="56dp"
android:layout_weight="1"
android:layout_marginEnd="12dp"
android:text="@string/privacy_agreement_disagree"
android:textColor="@color/white"
android:backgroundTint="@color/black_60"
android:textSize="13sp"
android:maxLines="2"
android:gravity="center" />
<Button
android:id="@+id/agreeButton"
android:layout_width="0dp"
android:layout_height="56dp"
android:layout_weight="1"
android:layout_marginStart="12dp"
android:text="@string/privacy_agreement_agree"
android:textColor="@color/black"
android:backgroundTint="@color/primary"
android:textSize="13sp"
android:maxLines="2"
android:gravity="center" />
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="16dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/last_watch"
android:textColor="?attr/colorPrimary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textSize="14sp" />
<Button
android:id="@+id/play"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/play"
android:background="@drawable/shape_blue"
android:textColor="@color/white" />
</LinearLayout>
+1 -1
View File
@@ -19,6 +19,6 @@
android:layout_marginTop="16dp"
android:text="@string/error_empty"
android:textColor="@color/white"
android:textSize="16sp" />
android:textSize="14sp" />
</LinearLayout>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottieAnimation"
android:layout_width="180dp"
android:layout_height="180dp"
app:lottie_fileName="lottie_empty_1.json"
app:lottie_loop="true"
app:lottie_autoPlay="true" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/error_keep_empty"
android:textColor="@color/white"
android:textSize="14sp" />
</LinearLayout>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottieAnimation"
android:layout_width="180dp"
android:layout_height="180dp"
app:lottie_fileName="lottie_empty_1.json"
app:lottie_loop="true"
app:lottie_autoPlay="true" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/error_empty"
android:textColor="@color/white"
android:textSize="14sp"
android:alpha="0.8" />
</LinearLayout>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottieAnimation"
android:layout_width="180dp"
android:layout_height="180dp"
app:lottie_fileName="lottie_empty_1.json"
app:lottie_loop="true"
app:lottie_autoPlay="true" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/error_search_empty"
android:textColor="@color/white"
android:textSize="14sp" />
</LinearLayout>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:background="@drawable/bg_toast"
android:gravity="center_vertical">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="12dp"
android:src="@drawable/ic_notify_play"
android:contentDescription="@string/play"
android:tint="@color/yellow_500" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="@string/last_watch"
android:textColor="@color/yellow_500"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
@@ -1,9 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/white" />
<!-- 使用纯色背景,自动适配深浅色模式 -->
<background android:drawable="@color/launcher_background" />
<!-- 前景图标:使用 inset 缩小显示(因为图标铺满了画布)-->
<foreground>
<inset
android:drawable="@mipmap/ic_launcher_foreground"
android:inset="20%" />
</foreground>
<!-- 主题图标:也需要使用 inset 保持大小一致 -->
<monochrome>
<inset
android:drawable="@mipmap/ic_launcher_foreground"
android:inset="20%" />
</monochrome>
</adaptive-icon>
@@ -1,9 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/white" />
<!-- 使用纯色背景,自动适配深浅色模式 -->
<background android:drawable="@color/launcher_background" />
<!-- 前景图标:使用 inset 缩小显示(因为图标铺满了画布)-->
<foreground>
<inset
android:drawable="@mipmap/ic_launcher_foreground"
android:inset="20%" />
</foreground>
</adaptive-icon>
<!-- 主题图标:也需要使用 inset 保持大小一致 -->
<monochrome>
<inset
android:drawable="@mipmap/ic_launcher_foreground"
android:inset="20%" />
</monochrome>
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

+7
View File
@@ -0,0 +1,7 @@
<resources>
<!-- Launcher icon background (Dark mode) -->
<color name="launcher_background">#222222</color>
</resources>
+14 -1
View File
@@ -73,11 +73,14 @@
<string name="setting_subscription">订阅管理</string>
<string name="setting_player">播放设置</string>
<string name="setting_incognito">无痕模式</string>
<string name="setting_live_tab_visible">隐藏直播</string>
<string name="setting_quality">图片品质</string>
<string name="setting_size">图片尺寸</string>
<string name="setting_doh">DoH</string>
<string name="setting_proxy">Proxy</string>
<string name="setting_cache">缓存</string>
<string name="setting_auto_clean">自动清理缓存</string>
<string name="setting_cache_threshold">清理阈值</string>
<string name="setting_backup">备份</string>
<string name="setting_restore">恢复</string>
<string name="setting_version">版本</string>
@@ -88,6 +91,8 @@
<string name="setting_app">应用设置</string>
<string name="setting_network">网络设置</string>
<string name="setting_data">数据管理</string>
<string name="app_version">v3.0.9</string>
<string name="about_github">在GitHub上查看</string>
<!-- Backup & Restore -->
<string name="restore_select">选择备份</string>
@@ -121,7 +126,7 @@
<string name="dialog_edit">修改</string>
<string name="dialog_positive">确定</string>
<string name="dialog_negative">取消</string>
<string name="dialog_paste">从剪贴板粘贴</string>
<string name="dialog_paste">点我粘贴</string>
<string name="dialog_config_hint">请输入接口…</string>
<string name="dialog_config_name">请输入名称…</string>
<string name="dialog_config_url">请输入地址…</string>
@@ -143,6 +148,7 @@
<string name="error_cast_file">不支持的文件格式</string>
<string name="error_device_limit">设备授权数已达上限</string>
<string name="error_live_empty">该订阅无直播内容</string>
<string name="error_no_live">当前源没有直播内容</string>
<!-- Update -->
<string name="update_version">发现新版本 <xliff:g name="name">%s</xliff:g></string>
@@ -166,6 +172,7 @@
<string name="none"></string>
<string name="times"></string>
<string name="lines"></string>
<string name="last_watch">上次播放</string>
<string-array name="select_decode">
<item>软解</item>
@@ -209,4 +216,10 @@
<item>选择字幕</item>
</string-array>
<string name="source_hint">您还没有添加视频源\n点击下方按钮添加</string>
<string name="add_source">添加源</string>
<string name="source_hint_setting">添加视频源</string>
<string name="source_hint_live">添加直播源</string>
<string name="source_hint_wall">添加壁纸源</string>
</resources>
+14 -2
View File
@@ -76,7 +76,9 @@
<string name="setting_size">圖片尺寸</string>
<string name="setting_doh">DoH</string>
<string name="setting_proxy">Proxy</string>
<string name="setting_cache"></string>
<string name="setting_cache"></string>
<string name="setting_auto_clean">自動清理緩存</string>
<string name="setting_cache_threshold">清理閾值</string>
<string name="setting_backup">備份</string>
<string name="setting_restore">還原</string>
<string name="setting_version">版本</string>
@@ -87,6 +89,8 @@
<string name="setting_app">應用設置</string>
<string name="setting_network">網絡設置</string>
<string name="setting_data">數據管理</string>
<string name="app_version">v3.0.9</string>
<string name="about_github">在GitHub上查看</string>
<!-- Backup & Restore -->
<string name="restore_select">選擇備份</string>
@@ -137,7 +141,11 @@
<string name="error_play_flag">暫無線路資料</string>
<string name="error_play_timeout">連線逾時</string>
<string name="error_detail">暫無播放資料</string>
<string name="error_empty">找不到資料</string>
<string name="error_empty">這裡撒子內容都沒得~</string>
<string name="error_cast_file">不支持的檔案格式</string>
<string name="error_device_limit">設備授權數已達上限</string>
<string name="error_live_empty">該訂閱無直播內容</string>
<string name="error_no_live">當前源沒有直播內容</string>
<!-- Update -->
<string name="update_version">發現新版本 <xliff:g name="name">%s</xliff:g></string>
@@ -204,4 +212,8 @@
<item>選擇字幕</item>
</string-array>
<string name="config_set_current">已設為當前點播源</string>
<string name="source_hint">點我添加源</string>
</resources>
+3
View File
@@ -27,5 +27,8 @@
<color name="white_90">#E6FFFFFF</color>
<color name="text_toast">#FFEB3B</color>
<color name="yellow_500">#FFEB3B</color>
<!-- Launcher icon background (Light mode) -->
<color name="launcher_background">#FFFFFF</color>
</resources>
+34 -7
View File
@@ -65,7 +65,7 @@
<!-- Push -->
<string name="push">Push</string>
<string name="push_image" translatable="false">data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCADwAPADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD9U6KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACvN7bxhffFCaRPB+oWZ8KSQX1hd65C7reWd/GwRPKjZdrAHLZPB4OcY3aPxe1j+z/CiWMeo6lo95rN3DpdpqOlWn2ma2mkJ2vt/u/KQT2zXXafafYbGGAyNO6KA8zKqtI38TkKAMsck4AGSafmI42H4S2T7Z7/AFjWNQ1JvDx8OT3rXjI00JOWmKrwJicnzBzzVO78L+JfAdi174a1C88RwaZoK6fZeGb6WNftdyjArPJdPzvK5B6A8dMV6PRRcLGL4a8VWHiaK7S1u7Wa+sJRa6hbWtwJvslwFDPEzDHK7sdB0+orarloPDOo6f47bUtPudPsfD9xbOb7TorBRPdXhYYnaYEHIQbec5/IjqaACiiikMKKimuIrdQZZFjBzjccZwCTj8AT+FcVbfFrTNdl0dPDdpeeI4dYtbq4sdSs4j9g3QEqUln/AOWe5xtBKkZpgd1UX2iIXAg81PPKlxFuG4qCATj0yRz71wVjp/jnxZb2E+sXMPhW0vNImttS0izcS3NvdOSFlhuVPBVeRjPP5i/ovw98PeC5bHXLqV7vWNN0lNJfxDq0+65ktVbdiV+FJLcliM5oEdnRXiev/tWeG21R9G8D6bqPxI11Tg2+gRFreM88yXB+RRx1G6vmb9ov9or4iaes+jar4lsvDmqS5RvDXhKXzbi2BGALq852tkn5Isk4525BNqDZLmkfY/xI+O3gT4Sqn/CUeIrWwuHIC2ke6a4Oe/lICwHuRj3rvq/NT4Xfs1apF8XvhtbeLA41zWJn1++0uYkvZ2MBDJ5+cnfM4K7eq4AJ3MQn6V0pRUbWCMnLcKKKKgsKKKKACiiigAooooAKKKKACiiigAooooA4T4xag2h+G7HW21bVdLtdK1O2u7mPR7X7RLex79n2ZkHOx2dckcjFd0DkA0yaLzoZI9zJvUruQ4YZHUHsa8w03xO3wa06503xXPeDwpo9rB5fjLW75Jpb6eWVgY2RBuypKjOOnX1L3Fsep0VV/tSz3zobuAPAVWVTIAYywyoYdiQRjPrXOeJvil4d8K2eoTz3v219PuILW7ttOQ3M0EkxAjDomSuc55xxSGdbSEgdTiuDvtY8d61caja6Potjof2DVoI0vdYmM0WoWOA0rxLH80b/AMI3gjrz6DfCW11a4d/Eeq3/AIiSHXl17TY7iUxf2fIgxHGhjILIvXaxIJ5INMRcuPiv4eFzp0NjcS62LzVW0Yy6TGbmO3uVXcyzMvCBR1J6d6qaXqHjzxDcaRdzadY+FbW31C5j1HT7qQXct1aqCsLxSJgIWPzEMMjgfXrrPTNP0OK6a0tbXT45pnu7gwxrEHkbl5HwBlj1LHk968s8XftSeDtD1JtG0D7Z468R52jSvDUJumBzj55B8ij15JGDxTWuyFtudP4d+EemaU/h+81W+1DxPrmhSXUljrGq3Ba4j+0Z8xcrtDLtO0BgcAYFTeL/AIjeB/g3o0Z1vVtN8O2aL+5tFwrMOeI4UG49/uivL7yL4y/Ej59Y1bT/AIReH5RkWmnkXurOvXBk+4h6cryOeKs+EPhD4D+H94dQ0/Rn13XmO59e8RSG8u2b+8C3Cn3UCnp1YteiGN8bviB8TPk+GvgWSw0uQceJvGGbW3x03xwD55B1wenHIqnH8ALTxMZNX+K/jW+8di0BuJraSb7BotoB8xJRSAQoH3ieg5Fdf8QPiFo/w98PN4g8caubCw5+z2i8z3bgZ2RRjqTxz0Gckgc14Z8QbzUPHHhgeMvjF9o8E/DS2kX+yfAdk5S+1iT7yC4OQctj7vBABPyY3tUb9NBO3XUrfEv9oaN/CF7pvwrji8DfD2wf7Jc+Kre0ET3UxGPs+nQjaXlIGS/BHUmMYZtf9mL9l218L6pY+N/GGl+Tr87CTR9Bum82TT16i4uWIG+4P3ugCHsrYWLofhf8L72+1XTfiH8QtLt9O1C1jC+FfBMMYW10C36q7RgAed0PIypAJAYARegeOfHLeAfh/wCL/GtzIDPptg7W7Sfda4f5IV+hcov/AAKm5fZiJR+1Iwfgj/xX/wAcvil8QHzJaWlwnhXS3PIEVvhrgqe6tLtYY9TXvtea/s5eA3+HPwX8L6POpW/NqLu8LcsbiYmWQMe5Bfb/AMBFelVnLcuOwUUUVJQUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAVFc2sN5C8NxEk8LjDRyKGVvqDUtFAHGa38HfBviKLXY9Q0OG4XXJoZ9R/eSIbiSH/AFTMVYEFe2MdBXSWeg6bpupahqFpp9tbX+oMjXl1DCqy3BRdqGRgMttXgZ6CvOvHH7SngfwXfHS476XxJ4hJ2ponh+I3l0zehCfKp6feI61yd1q3xn+JibkTTvhB4ek4868K32rSL/sp9yM4zwfmGRV2fUm66HsPjDx54d+H+mtqHiPWrLRrQdJLuYIW9lHVj7AE15G37Q/iX4ifuvhT4Fu9atW4/wCEi8QBrHTR/tKD88o6cDB5qDw78FfA3hfVP7Xu7S68c+JDy+teKJjdPu9URvlUZJxgAj1rubzV7q+ULJKRGOBGnyqPbApaINWec3nwZv8AxlJ5/wAVvHl94rXO4+HNDJstMX/ZbaQ0g9yQeTXd6Db6Z4N0saZ4X0ex8O6eP+WNjCqFvdmxlj7nn3ptWLWza4jlmd0t7WFTJNczMFjiUDJZmPAAAJpXbHZIYqzXk4UB5pXP1JrjfiB8XLPwBrUHhXw9ph8b/Eu8GLXQ7RgYrQkcSXL5wigHcQSOOSVB31zuofFHXfixeXvhz4RSjS/D0BMOs/Ea+iIhhAALpaA43uAcbvcEbRtkq/oz+Av2Z/hze6zEJotMlk/e6tcYfVfEdyckIhODsJz6D7x4+ZjMpKDUXrJ9DSNOVSLmtIrd/p5spTeEdI+C9nJ8WvjZrY8W+OFIFjbp81vaScmO3soTgF85O8gY+98uGdrvgXwLrfivxRbfFT4qW6jxABu8OeE3yYdEiJyskinrOcA8jKkAnDACJvw9+Hus+JPEtt8UvilbKPEIXPh3woR+40OHqrup/wCW3Q8jKkZPzYEfpdxcSXczyyuXkY5JNdlGiqCet5Pd9/8AgHm4rFPFSVo8sF8MVsv82+r6hcXEl3M8srF5GOSTUVFFanIM1bxNB4C8I+JPFlyFaLRdPluVRzgSSBTsT6scL/wIVn/s+2lh8Jfgz4NtfEeow2Ws+IphPI144V7m+uiZRH7vjC4/2a5f47Wp8Taf4A+G0fL+MNbSe/QDOdPtcTTZ9ORGR9DXqXiC4XVPip4Y0O3vtBki0+1m1O90e8t/Mvdn+rt7i3OMRhJAQW9Gx6VfQjqd/RRRUFhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQBDdWsN9byW9zDHcQSDa8UqhlYehB4Irx3xR+yX4D1jUW1bQob3wLr3JXUvC9y1m4J55QfJjPXCgn1r2iimm1sJpPc+eLjQfjr8OU2wXei/F7RY+lvqKDT9SC+iyD92T/tMST6VRs/2iPBf9oJpfjCw1r4Ya2/At/EFowtnP8AsTAEFf8AabaK+la5j4m6VZax8PvENvf2dvfW/wBgnfyrmJZE3CNiDhgRkVV090TZrZngGqfFjXPijql/4S+DBjjtLcmPWPiFeri0tFAyy22fvt2DfUjjEgy9Hk8O/B3TbnTfBHmX2t3m7+1vF18S95fSFizFWOdqluePQHk/Oa2h3c2m/s0/CqytHNta3djNLcQw/KsrB1ILY68sx+pzU3gex0uD+1fEXiLjw5oFq99e5x+82glIxkjLMRwuecY7181mGMrTr/UcPo9rn3uT5XhaWD/tbGvmSu0umjt823t0GSSaJ8O/C7+OvHTudPLH+z9Lzm41SfqFUH+HPU/ieOvRfDr4d6z4i8TW/wAU/inbofEe3Ph7wqw/caJD1R3Q/wDLbocHlTyfnwI/G/CHxK1CP4hWHxa+M/g3W59Gngjfw3eWdv5umaUpJ2v5WeGwAysTu/iCk7GX6o8O+JtD+J1rJqnhXxFZeJYT80gt5MTR56b4zhk+hA+lezhcHDA0+SG73fc+WzDM6uaVvaVNEto9l/XUs3FxJdTPLKxeRjkk1FT5I3hcpIjIw6qwwaZXSecFS20DXVxFEv3pGCj8TUVStrlr4R0XWvEt9/x5aLYzXsgzjdsQnaPc4IHvigDlvAca+PP2nvGWvgb9L8HafD4bsTn5ftD/AL24YD+8vEZ9sV33gu+HiLx54v1OLUdE1Sxs5o9KtvsVvi9s5Ix/pMFxIevz7GCjpnnsa5P9mjSz4G+BFvr/AIglEF7q/wBo8TatcyKRhpsyl2HXiMJnvxXafCFbiTwLZX95f6Vq13qTPfSalo9r9nguw7HZLt6ljGEyx6kVciEdpRRRUFhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFYXjz/kR/EX/YOuP/RTVu1hePP+RH8Rf9g64/8ARTU1uB8p2P8Aybv8H/8AsHT/APoUdRfETQ5vE194J+CNhK0MusSLr3iyeNsG3s0+ZImIOAcLnDD7wiP8VdV8OLewtfgH8LPEGtP5Wg6Bot1qV6/HKoUKoASMszDAHfpWB8IPEln4L8C+LPjj8QIXn13xxdmPTdLQF55rcHEFrCp+YhyoGOR5ccZPFedRwrWNrYlrsl9yPfxOPTyvDYGL01cv/AnZfr9x6H4v+I7/AAysf7SW3mvLnVwNK8J+Crb/AJelUBUdo/4UAIJPYMB1Kiuf8FfsV6fDoZ1nWdXvdE+Il5M17Lqnhmb7JFZuxBEEUajYY1OM8ZPPIGK7r4M/DHWJtcufiT8QVSXxzqkXl29mDui0W0OSttF6Ngnew6knnlifZq7qcfYx5U7vq/M8WvV+sT52rJaJdktvn5nzldt8avhYpTVdMsvjD4bj4FzZKLTV4155MfSQ9OFyT61e8F/GTwF8Rbo2Onay2h68p2yaF4gQ2tyj4GVG7hjz0BNe/wBcZ8RPg74M+K1mbfxR4fs9UYLtS5ZNlxH/ALkq4dfoDitbp7mFmtjGvNNudPbbcQtH6NjIP41518fo5NY8G+FfAFsxS8da3DaTbSQy2ULCWdwRzwFX8GNTyfBX4nfCdd/w38bf8JHoqf8yx4w/ertzkrFcDkdwAQoHcml+F9n4v8AiP8AHCLxZ4w8H3Xg6Hwxop020sZ3WSF7yaQmWWGQcOvlhVyOOcZOKaVtbkt30seofFbVo/C/gCS2s9Y07w3dXTRabp9xqUBmt/NcgLEYwDuDKGXFdhptmun6fa2qrGiwRLEFhQRoAoAwqjhRxwB0p11ZW98ipcwRXCK6yKsqBgGByGGe4PQ1PUFhRRRSGFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAVhePP+RH8Rf9g64/9FNW7WD483HwN4i2RvK/9nXOI413Mx8puAO5NNbgfB/jX4kWV58A/hN8NZdQaw0u40+PVvEl1Dy0NjHI2yMdi7sPlU9XEQ/ir6Q+DPw1vPGOt6f8RfGGljTBZ262/hTwww/d6LZgAK7L/wA92ULk/wAIAHYBfFf2N/2XJtelsfiN47tWeKIRjR9LuoyN4jUIk8in+EBRsB643dMZ+6K1m0tEYwTerCiiisTYKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Z</string>
<string name="push_image" translatable="false">data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCADwAPADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD9U6KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACvN7bxhffFCaRPB+oWZ8KSQX1hd65C7reWd/GwRPKjZdrAHLZPB4OcY3aPxe1j+z/CiWMeo6lo95rN3DpdpqOlWn2ma2mkJ2vt/u/KQT2zXXafafYbGGAyNO6KA8zKqtI38TkKAMsck4AGSafmI42H4S2T7Z7/AFjWNQ1JvDx8OT3rXjI00JOWmKrwJicnzBzzVO78L+JfAdi174a1C88RwaZoK6fZeGb6WNftdyjArPJdPzvK5B6A8dMV6PRRcLGL4a8VWHiaK7S1u7Wa+sJRa6hbWtwJvslwFDPEzDHK7sdB0+orarloPDOo6f47bUtPudPsfD9xbOb7TorBRPdXhYYnaYEHIQbec5/IjqaACiiikMKKimuIrdQZZFjBzjccZwCTj8AT+FcVbfFrTNdl0dPDdpeeI4dYtbq4sdSs4j9g3QEqUln/AOWe5xtBKkZpgd1UX2iIXAg81PPKlxFuG4qCATj0yRz71wVjp/jnxZb2E+sXMPhW0vNImttS0izcS3NvdOSFlhuVPBVeRjPP5i/ovw98PeC5bHXLqV7vWNN0lNJfxDq0+65ktVbdiV+FJLcliM5oEdnRXiev/tWeG21R9G8D6bqPxI11Tg2+gRFreM88yXB+RRx1G6vmb9ov9or4iaes+jar4lsvDmqS5RvDXhKXzbi2BGALq852tkn5Isk4525BNqDZLmkfY/xI+O3gT4Sqn/CUeIrWwuHIC2ke6a4Oe/lICwHuRj3rvq/NT4Xfs1apF8XvhtbeLA41zWJn1++0uYkvZ2MBDJ5+cnfM4K7eq4AJ3MQn6V0pRUbWCMnLcKKKKgsKKKKACiiigAooooAKKKKACiiigAooooA4T4xag2h+G7HW21bVdLtdK1O2u7mPR7X7RLex79n2ZkHOx2dckcjFd0DkA0yaLzoZI9zJvUruQ4YZHUHsa8w03xO3wa06503xXPeDwpo9rB5fjLW75Jpb6eWVgY2RBuypKjOOnX1L3Fsep0VV/tSz3zobuAPAVWVTIAYywyoYdiQRjPrXOeJvil4d8K2eoTz3v219PuILW7ttOQ3M0EkxAjDomSuc55xxSGdbSEgdTiuDvtY8d61caja6Potjof2DVoI0vdYmM0WoWOA0rxLH80b/AMI3gjrz6DfCW11a4d/Eeq3/AIiSHXl17TY7iUxf2fIgxHGhjILIvXaxIJ5INMRcuPiv4eFzp0NjcS62LzVW0Yy6TGbmO3uVXcyzMvCBR1J6d6qaXqHjzxDcaRdzadY+FbW31C5j1HT7qQXct1aqCsLxSJgIWPzEMMjgfXrrPTNP0OK6a0tbXT45pnu7gwxrEHkbl5HwBlj1LHk968s8XftSeDtD1JtG0D7Z468R52jSvDUJumBzj55B8ij15JGDxTWuyFtudP4d+EemaU/h+81W+1DxPrmhSXUljrGq3Ba4j+0Z8xcrtDLtO0BgcAYFTeL/AIjeB/g3o0Z1vVtN8O2aL+5tFwrMOeI4UG49/uivL7yL4y/Ej59Y1bT/AIReH5RkWmnkXurOvXBk+4h6cryOeKs+EPhD4D+H94dQ0/Rn13XmO59e8RSG8u2b+8C3Cn3UCnp1YteiGN8bviB8TPk+GvgWSw0uQceJvGGbW3x03xwD55B1wenHIqnH8ALTxMZNX+K/jW+8di0BuJraSb7BotoB8xJRSAQoH3ieg5Fdf8QPiFo/w98PN4g8caubCw5+z2i8z3bgZ2RRjqTxz0Gckgc14Z8QbzUPHHhgeMvjF9o8E/DS2kX+yfAdk5S+1iT7yC4OQctj7vBABPyY3tUb9NBO3XUrfEv9oaN/CF7pvwrji8DfD2wf7Jc+Kre0ET3UxGPs+nQjaXlIGS/BHUmMYZtf9mL9l218L6pY+N/GGl+Tr87CTR9Bum82TT16i4uWIG+4P3ugCHsrYWLofhf8L72+1XTfiH8QtLt9O1C1jC+FfBMMYW10C36q7RgAed0PIypAJAYARegeOfHLeAfh/wCL/GtzIDPptg7W7Sfda4f5IV+hcov/AAKm5fZiJR+1Iwfgj/xX/wAcvil8QHzJaWlwnhXS3PIEVvhrgqe6tLtYY9TXvtea/s5eA3+HPwX8L6POpW/NqLu8LcsbiYmWQMe5Bfb/AMBFelVnLcuOwUUUVJQUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAVFc2sN5C8NxEk8LjDRyKGVvqDUtFAHGa38HfBviKLXY9Q0OG4XXJoZ9R/eSIbiSH/AFTMVYEFe2MdBXSWeg6bpupahqFpp9tbX+oMjXl1DCqy3BRdqGRgMttXgZ6CvOvHH7SngfwXfHS476XxJ4hJ2ponh+I3l0zehCfKp6feI61yd1q3xn+JibkTTvhB4ek4868K32rSL/sp9yM4zwfmGRV2fUm66HsPjDx54d+H+mtqHiPWrLRrQdJLuYIW9lHVj7AE15G37Q/iX4ifuvhT4Fu9atW4/wCEi8QBrHTR/tKD88o6cDB5qDw78FfA3hfVP7Xu7S68c+JDy+teKJjdPu9URvlUZJxgAj1rubzV7q+ULJKRGOBGnyqPbApaINWec3nwZv8AxlJ5/wAVvHl94rXO4+HNDJstMX/ZbaQ0g9yQeTXd6Db6Z4N0saZ4X0ex8O6eP+WNjCqFvdmxlj7nn3ptWLWza4jlmd0t7WFTJNczMFjiUDJZmPAAAJpXbHZIYqzXk4UB5pXP1JrjfiB8XLPwBrUHhXw9ph8b/Eu8GLXQ7RgYrQkcSXL5wigHcQSOOSVB31zuofFHXfixeXvhz4RSjS/D0BMOs/Ea+iIhhAALpaA43uAcbvcEbRtkq/oz+Av2Z/hze6zEJotMlk/e6tcYfVfEdyckIhODsJz6D7x4+ZjMpKDUXrJ9DSNOVSLmtIrd/p5spTeEdI+C9nJ8WvjZrY8W+OFIFjbp81vaScmO3soTgF85O8gY+98uGdrvgXwLrfivxRbfFT4qW6jxABu8OeE3yYdEiJyskinrOcA8jKkAnDACJvw9+Hus+JPEtt8UvilbKPEIXPh3woR+40OHqrup/wCW3Q8jKkZPzYEfpdxcSXczyyuXkY5JNdlGiqCet5Pd9/8AgHm4rFPFSVo8sF8MVsv82+r6hcXEl3M8srF5GOSTUVFFanIM1bxNB4C8I+JPFlyFaLRdPluVRzgSSBTsT6scL/wIVn/s+2lh8Jfgz4NtfEeow2Ws+IphPI144V7m+uiZRH7vjC4/2a5f47Wp8Taf4A+G0fL+MNbSe/QDOdPtcTTZ9ORGR9DXqXiC4XVPip4Y0O3vtBki0+1m1O90e8t/Mvdn+rt7i3OMRhJAQW9Gx6VfQjqd/RRRUFhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFYXjz/kR/EX/YOuP/RTVu1hePP+RH8Rf9g64/9FNW7WD483HwN4i2RvK/9nXOI413Mx8puAO5NNbgfB/jX4kWV58A/hN8NZdQaw0u40+PVvEl1Dy0NjHI2yMdi7sPlU9XEQ/ir6Q+DPw1vPGOt6f8RfGGljTBZ262/hTwww/d6LZgAK7L/wA92ULk/wAIAHYBfFf2N/2XJtelsfiN47tWeKIRjR9LuoyN4jUIk8in+EBRsB643dMZ+6K1m0tEYwTerCiiisTYKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Z</string>
<!-- Setting -->
<string name="setting_vod">Vod</string>
@@ -78,17 +78,22 @@
<string name="setting_data">Data Management</string>
<string name="setting_player">Player setting</string>
<string name="setting_incognito">Incognito mode</string>
<string name="setting_live_tab_visible">Hide Live Tab</string>
<string name="setting_quality">Image quality</string>
<string name="setting_size">Image size</string>
<string name="setting_doh">DoH</string>
<string name="setting_proxy">Proxy</string>
<string name="setting_cache">Cache</string>
<string name="setting_backup">Backup</string>
<string name="setting_restore">Restore</string>
<string name="setting_version">Version</string>
<string name="setting_cache">缓存</string>
<string name="setting_auto_clean">Auto Clean Cache</string>
<string name="setting_cache_threshold">Cache Threshold</string>
<string name="setting_backup">备份</string>
<string name="setting_restore">恢复</string>
<string name="setting_version">版本</string>
<string name="setting_choose">Choose</string>
<string name="setting_off">Off</string>
<string name="setting_on">On</string>
<string name="app_version">v3.0.9</string>
<string name="about_github">View on GitHub</string>
<!-- Backup & Restore -->
<string name="restore_select">Select backup</string>
@@ -122,7 +127,7 @@
<string name="dialog_edit">Edit</string>
<string name="dialog_positive">OK</string>
<string name="dialog_negative">Cancel</string>
<string name="dialog_paste">Paste from clipboard</string>
<string name="dialog_paste">Click to paste</string>
<string name="dialog_config_hint">Please enter the config…</string>
<string name="dialog_config_name">Please enter the name…</string>
<string name="dialog_config_url">Please enter the url…</string>
@@ -146,7 +151,10 @@
<string name="error_cast_file">Unsupported file format</string>
<string name="error_device_limit">Device authorization limit reached</string>
<string name="error_live_empty">This subscription has no live content</string>
<string name="error_empty">Not found</string>
<string name="error_no_live">Current source has no live content</string>
<string name="error_empty">空谷待音~</string>
<string name="error_keep_empty">老表~没得收藏哈</string>
<string name="error_search_empty">搜索无结果,换个关键词试试</string>
<string name="error_detail">No play data</string>
<string name="error_play_flag">No flag data</string>
<string name="error_play_code">Error code: <xliff:g name="name">%s</xliff:g></string>
@@ -168,12 +176,14 @@
<!-- Hint -->
<string name="copied">Copied</string>
<string name="copied_to_clipboard">Error log copied to clipboard</string>
<!-- UNIT -->
<string name="all">All</string>
<string name="none">None</string>
<string name="times">times</string>
<string name="lines">lines</string>
<string name="last_watch">上次播放</string>
<string-array name="select_decode">
<item>Soft</item>
@@ -224,4 +234,21 @@
<string name="config_set_current">已设为当前点播源</string>
<string name="remember_setting">Remember settings</string>
<string name="target_size">Target size</string>
<string name="scan_result">Scan result</string>
<string name="source_hint">空谷无音,待君添源</string>
<string name="add_source">添加源</string>
<!-- 隐私协议相关 -->
<string name="privacy_agreement_title">XMBOX软件许可协议</string>
<string name="privacy_agreement_tip">请仔细阅读以下协议条款</string>
<string name="privacy_agreement_agree">我已阅读并同意</string>
<string name="privacy_agreement_disagree">不同意并退出</string>
<string name="privacy_agreement_content">XMBOX软件许可协议:\n\n- 以下是对[GPL-3.0](LICENSE.md)开源协议的补充,如有冲突,以以下协议为准。\n- 词语约定: 本协议中的"本软件"指"XMBOX软件""用户"指签署本协议的使用者,"版权数据"指包括但不限于视频、图像、音频、名字等在内的他人拥有所属版权的数据。\n\n1. 本软件仅为技术性多媒体播放器外壳("空壳播放器"),核心功能限于基础媒体文件解析与播放。\n\n2. 本软件自身不包含、不预装、不内置、不集成、不主动推荐、不直接或间接提供任何音视频、直播、图文等媒体资源内容。软件播放的任何资源均非由本软件或其开发者提供。\n\n3. 用户通过本软件播放的任何内容均完全来源于用户自行配置、输入、添加、获取或选择的第三方来源(如网络地址、本地文件、用户安装的插件/扩展/配置源等)。本软件仅作为访问用户自行指定内容的技术工具。\n\n4. 本软件无法控制、筛选、审查或保证用户访问的任何第三方内容的合法性、版权状态、准确性、安全性或适宜性。用户对其播放的内容负全部责任。\n\n5. 关于用户责任与风险承担:\n • 用户必须确保其通过本软件配置、访问或播放的所有内容均已获相关权利人合法授权,或属于法律允许的自由使用范畴。\n • 用户理解并同意,使用本软件访问第三方资源可能涉及侵犯版权、传播非法信息、隐私泄露、网络安全等风险。因用户使用本软件访问、播放或传播内容产生的一切法律责任、纠纷、损失及后果(包括法律诉讼、行政处罚、民事赔偿等),均由用户自行承担,与本软件及其开发者无涉。\n • 开发者不认可、不支持任何利用本软件规避技术保护措施(如DRM)的行为,此类行为导致的侵权责任由用户全权承担。\n\n6. 用户承诺并保证不利用本软件从事任何侵犯他人知识产权或其他合法权益的活动,或进行任何违反法律法规的行为。严禁使用本软件播放、传播盗版、色情、暴力、赌博、诈骗、危害国家安全、危害社会稳定等非法或侵权内容。\n\n7. 在任何情况下,本软件开发者均不就因用户使用或无法使用本软件、用户配置或访问的第三方资源、用户违反本协议或法律法规的行为导致的任何直接、间接、偶然、特殊、惩罚性或结果性损害(包括利润损失、数据丢失、业务中断、声誉损害等)承担任何责任(无论基于合同、侵权、严格责任或其他法律理论)。\n\n8. 本软件运行可能依赖第三方库、服务或技术。开发者不对这些第三方组件的可用性、准确性、功能或合法性负责。\n\n9. 用户理解并同意,使用本软件(包括下载、安装、运行)存在固有技术风险(如软件缺陷、兼容性问题、系统不稳定等),用户应自行承担此风险。\n\n10. 本软件仅用于对技术可行性的探索及研究,不接受任何商业(包括但不限于广告等)合作及捐赠。\n\n11. 本软件内使用的部分包括但不限于字体、图片等资源来源于互联网。如果出现侵权可联系开发者移除。\n\n12. 使用本软件的过程中可能会产生版权数据。对于这些版权数据,本软件不拥有它们的所有权。为了避免侵权,用户务必在 24 小时内 清除使用本项目的过程中所产生的版权数据。\n\n13. 本协议受中华人民共和国法律管辖并据其解释。若用户所在地法律强制规定特定责任条款,应以当地法律要求为准,但其他条款仍保持有效。任何由本协议或使用本软件引起的争议,应首先通过友好协商解决。\n\n14. 若你使用了本软件,即代表你接受本协议。\n\n15. 本协议更新后,继续使用视为接受新协议。</string>
<string name="source_hint_setting">Add Source</string>
<string name="source_hint_live">Add Live Source</string>
<string name="source_hint_wall">Add Wallpaper Source</string>
</resources>
+12
View File
@@ -10,6 +10,13 @@
<application>
<activity
android:name=".ui.activity.PrivacyAgreementActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode|orientation"
android:exported="false"
android:screenOrientation="fullUser"
android:windowSoftInputMode="adjustPan" />
<activity
android:name=".ui.activity.HomeActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode|orientation"
@@ -106,6 +113,11 @@
android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode"
android:screenOrientation="fullUser" />
<activity
android:name=".ui.activity.SettingPlayerActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode"
android:screenOrientation="fullUser" />
<activity
android:name=".ui.activity.VideoActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode|orientation"
@@ -2,6 +2,8 @@ package com.fongmi.android.tv;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
@@ -14,6 +16,7 @@ import com.fongmi.android.tv.utils.Notify;
import com.fongmi.android.tv.utils.ResUtil;
import com.github.catvod.net.OkHttp;
import com.github.catvod.utils.Github;
import com.github.catvod.utils.Logger;
import com.github.catvod.utils.Path;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@@ -28,9 +31,11 @@ public class Updater implements Download.Callback {
private final Download download;
private AlertDialog dialog;
private boolean dev;
private boolean forceCheck; // 是否为手动检查
private String latestVersion; // 存储检测到的最新版本
private File getFile() {
return Path.cache("update.apk");
return Path.root("Download", "XMBOX-update.apk");
}
private String getJson() {
@@ -38,7 +43,31 @@ public class Updater implements Download.Callback {
}
private String getApk() {
return Github.getApk(dev, BuildConfig.FLAVOR_mode + "-" + BuildConfig.FLAVOR_abi);
// 使用JSON中指定的具体下载路径
try {
String response = OkHttp.string(getJson());
JSONObject object = new JSONObject(response);
JSONObject downloads = object.optJSONObject("downloads");
if (downloads != null) {
String abi = BuildConfig.FLAVOR_abi;
String downloadPath = downloads.optString(abi);
if (!downloadPath.isEmpty()) {
// 直接构建完整URL不通过Github.getApk()避免重复添加路径
String baseUrl = Github.useCnMirror() ?
"https://gitee.com/ochenoktochen/XMBOX-Release/raw/main" :
"https://raw.githubusercontent.com/Tosencen/XMBOX-Release/main";
String fullUrl = baseUrl + "/apk/" + (dev ? "dev" : "release") + "/" + downloadPath;
Logger.d("APK download URL: " + fullUrl);
return fullUrl;
}
}
} catch (Exception e) {
Logger.e("Failed to get download path from JSON: " + e.getMessage());
}
// 回退到原来的方式
String fallbackUrl = Github.getApk(dev, BuildConfig.FLAVOR_mode + "-" + BuildConfig.FLAVOR_abi);
Logger.d("APK fallback URL: " + fallbackUrl);
return fallbackUrl;
}
public static Updater create() {
@@ -47,11 +76,13 @@ public class Updater implements Download.Callback {
public Updater() {
this.download = Download.create(getApk(), getFile(), this);
this.forceCheck = false;
}
public Updater force() {
Notify.show(R.string.update_check);
Setting.putUpdate(true);
this.forceCheck = true; // 标记为手动检查
return this;
}
@@ -79,14 +110,87 @@ public class Updater implements Download.Callback {
}
private void doInBackground(Activity activity) {
Logger.d("Updater: Starting update check...");
try {
JSONObject object = new JSONObject(OkHttp.string(getJson()));
String name = object.optString("name");
String desc = object.optString("desc");
int code = object.optInt("code");
if (need(code, name)) App.post(() -> show(activity, name, desc));
// 直接使用GitHub Releases API检测最新版本
String releasesUrl = "https://api.github.com/repos/Tosencen/XMBOX/releases/latest";
Logger.d("Updater: GitHub Releases API URL: " + releasesUrl);
String response = OkHttp.string(releasesUrl);
Logger.d("Updater: API response length: " + response.length());
// 检查响应是否包含错误信息
if (response.contains("rate limit exceeded")) {
Logger.e("Updater: Rate limit exceeded");
if (forceCheck) {
App.post(() -> Notify.show("检查更新失败:API请求过于频繁,请稍后重试"));
}
return;
}
if (response.contains("Not Found") || response.contains("404")) {
Logger.e("Updater: Release not found");
if (forceCheck) {
App.post(() -> Notify.show("检查更新失败:更新服务暂时不可用"));
}
return;
}
JSONObject release = new JSONObject(response);
String tagName = release.optString("tag_name");
String body = release.optString("body");
// 提取版本号去掉v前缀
String version = tagName.startsWith("v") ? tagName.substring(1) : tagName;
Logger.d("Updater: Remote version: " + version);
Logger.d("Updater: Local version: " + BuildConfig.VERSION_NAME);
// 比较版本号
if (needUpdate(version)) {
Logger.d("Updater: Update needed, showing dialog");
this.latestVersion = version; // 保存最新版本号
App.post(() -> show(activity, version, body));
} else {
Logger.d("Updater: No update needed");
if (forceCheck) {
App.post(() -> Notify.show("已是最新版本 " + version));
}
}
} catch (Exception e) {
Logger.e("Updater: Exception during update check: " + e.getMessage());
e.printStackTrace();
if (forceCheck) {
App.post(() -> Notify.show("检查更新失败:网络连接异常"));
}
}
}
private boolean needUpdate(String remoteVersion) {
if (!Setting.getUpdate()) return false;
try {
// 简单的版本号比较假设版本格式为 x.y.z
String[] remoteParts = remoteVersion.split("\\.");
String[] localParts = BuildConfig.VERSION_NAME.split("\\.");
// 确保两个版本号都有足够的段
int maxLength = Math.max(remoteParts.length, localParts.length);
for (int i = 0; i < maxLength; i++) {
int remotePart = i < remoteParts.length ? Integer.parseInt(remoteParts[i]) : 0;
int localPart = i < localParts.length ? Integer.parseInt(localParts[i]) : 0;
if (remotePart > localPart) {
return true;
} else if (remotePart < localPart) {
return false;
}
}
return false; // 版本相同
} catch (Exception e) {
Logger.e("Updater: Version comparison error: " + e.getMessage());
return false;
}
}
@@ -109,8 +213,31 @@ public class Updater implements Download.Callback {
}
private void confirm(View view) {
view.setEnabled(false);
download.start();
// 跳转到具体版本的GitHub Releases页面
try {
String url = "https://github.com/Tosencen/XMBOX/releases/tag/v" + latestVersion;
Logger.d("Updater: Attempting to open URL: " + url);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// 检查是否有应用可以处理这个Intent
if (intent.resolveActivity(App.get().getPackageManager()) != null) {
App.get().startActivity(intent);
Logger.d("Updater: Successfully started browser intent");
dismiss();
} else {
Logger.e("Updater: No app can handle the URL");
Notify.show("没有找到可以打开链接的应用,请手动访问GitHub下载");
dismiss();
}
} catch (Exception e) {
Logger.e("Updater: Failed to open GitHub releases page: " + e.getMessage());
e.printStackTrace();
Notify.show("无法打开更新页面,请手动访问GitHub下载");
dismiss();
}
}
private void dismiss() {
@@ -136,4 +263,4 @@ public class Updater implements Download.Callback {
FileUtil.openFile(file);
dismiss();
}
}
}
@@ -44,6 +44,7 @@ import com.fongmi.android.tv.utils.Util;
import com.github.catvod.net.OkHttp;
import com.google.android.flexbox.FlexDirection;
import com.google.android.flexbox.FlexboxLayoutManager;
import com.airbnb.lottie.LottieAnimationView;
import java.io.IOException;
import java.net.URLEncoder;
@@ -151,12 +152,14 @@ public class CollectActivity extends BaseActivity implements CustomScroller.Call
if (mCollectAdapter.getPosition() == 0) mSearchAdapter.addAll(result.getList());
mCollectAdapter.add(Collect.create(result.getList()));
mCollectAdapter.add(result.getList());
updateEmptyState();
});
mViewModel.result.observe(this, result -> {
boolean same = !result.getList().isEmpty() && mCollectAdapter.getActivated().getSite().equals(result.getList().get(0).getSite());
if (same) mCollectAdapter.getActivated().getList().addAll(result.getList());
if (same) mSearchAdapter.addAll(result.getList());
mScroller.endLoading(result);
updateEmptyState();
});
}
@@ -187,6 +190,7 @@ public class CollectActivity extends BaseActivity implements CustomScroller.Call
mBinding.agent.setVisibility(View.GONE);
mBinding.view.setVisibility(View.VISIBLE);
mBinding.result.setVisibility(View.VISIBLE);
updateEmptyState(); // 搜索开始时显示空状态
if (mExecutor != null) mExecutor.shutdownNow();
mExecutor = new PauseExecutor(20);
String keyword = mBinding.keyword.getText().toString().trim();
@@ -194,6 +198,27 @@ public class CollectActivity extends BaseActivity implements CustomScroller.Call
App.post(() -> mRecordAdapter.add(keyword), 250);
}
private void updateEmptyState() {
// 只有在结果页面可见且搜索结果为空时才显示空状态动画
boolean isResultVisible = isVisible(mBinding.result);
boolean isEmpty = mSearchAdapter.getItemCount() == 0;
boolean shouldShowEmpty = isResultVisible && isEmpty;
mBinding.emptyLayout.getRoot().setVisibility(shouldShowEmpty ? View.VISIBLE : View.GONE);
// 控制Lottie动画播放
if (shouldShowEmpty) {
try {
LottieAnimationView lottieView = mBinding.emptyLayout.getRoot().findViewById(R.id.lottieAnimation);
if (lottieView != null) {
lottieView.playAnimation();
}
} catch (Exception e) {
// 忽略错误
}
}
}
private void search(Site site, String keyword) {
try {
mViewModel.searchContent(site, keyword, false);
@@ -235,6 +260,7 @@ public class CollectActivity extends BaseActivity implements CustomScroller.Call
mBinding.result.setVisibility(View.GONE);
mBinding.site.setVisibility(View.VISIBLE);
mBinding.agent.setVisibility(View.VISIBLE);
mBinding.emptyLayout.getRoot().setVisibility(View.GONE); // 隐藏空状态动画
if (mExecutor != null) mExecutor.shutdownNow();
}
@@ -0,0 +1,56 @@
package com.fongmi.android.tv.ui.activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Bundle;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.viewbinding.ViewBinding;
import com.fongmi.android.tv.R;
import com.fongmi.android.tv.databinding.ActivityCrashBinding;
import com.fongmi.android.tv.ui.base.BaseActivity;
import java.util.Objects;
import cat.ereza.customactivityoncrash.CustomActivityOnCrash;
public class CrashActivity extends BaseActivity {
private ActivityCrashBinding mBinding;
private String errorDetails;
@Override
protected ViewBinding getBinding() {
return mBinding = ActivityCrashBinding.inflate(getLayoutInflater());
}
@Override
protected void initView(Bundle savedInstanceState) {
errorDetails = CustomActivityOnCrash.getAllErrorDetailsFromIntent(this, getIntent());
mBinding.error.setText(errorDetails);
}
@Override
protected void initEvent() {
mBinding.copy.setOnClickListener(v -> copyErrorToClipboard());
mBinding.restart.setOnClickListener(v -> CustomActivityOnCrash.restartApplication(this, Objects.requireNonNull(CustomActivityOnCrash.getConfigFromIntent(getIntent()))));
}
private void copyErrorToClipboard() {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(getString(R.string.crash_details_title), errorDetails);
clipboard.setPrimaryClip(clip);
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show();
showError();
}
private void showError() {
new AlertDialog.Builder(this)
.setTitle(R.string.crash_details_title)
.setMessage(errorDetails)
.setPositiveButton(R.string.crash_details_close, null)
.show();
}
}
@@ -17,6 +17,7 @@ import com.fongmi.android.tv.ui.adapter.HistoryAdapter;
import com.fongmi.android.tv.ui.base.BaseActivity;
import com.fongmi.android.tv.ui.dialog.SyncDialog;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.airbnb.lottie.LottieAnimationView;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
@@ -59,6 +60,25 @@ public class HistoryActivity extends BaseActivity implements HistoryAdapter.OnCl
private void getHistory() {
mAdapter.addAll(History.get());
mBinding.delete.setVisibility(mAdapter.getItemCount() > 0 ? View.VISIBLE : View.GONE);
updateEmptyState();
}
private void updateEmptyState() {
boolean isEmpty = mAdapter.getItemCount() == 0;
mBinding.emptyLayout.getRoot().setVisibility(isEmpty ? View.VISIBLE : View.GONE);
mBinding.recycler.setVisibility(isEmpty ? View.GONE : View.VISIBLE);
// 控制Lottie动画播放
if (isEmpty) {
try {
LottieAnimationView lottieView = mBinding.emptyLayout.getRoot().findViewById(R.id.lottieAnimation);
if (lottieView != null) {
lottieView.playAnimation();
}
} catch (Exception e) {
// 忽略错误
}
}
}
private void onSync(View view) {
@@ -67,7 +87,10 @@ public class HistoryActivity extends BaseActivity implements HistoryAdapter.OnCl
private void onDelete(View view) {
if (mAdapter.isDelete()) {
new MaterialAlertDialogBuilder(this).setTitle(R.string.dialog_delete_record).setMessage(R.string.dialog_delete_history).setNegativeButton(R.string.dialog_negative, null).setPositiveButton(R.string.dialog_positive, (dialog, which) -> mAdapter.clear()).show();
new MaterialAlertDialogBuilder(this).setTitle(R.string.dialog_delete_record).setMessage(R.string.dialog_delete_history).setNegativeButton(R.string.dialog_negative, null).setPositiveButton(R.string.dialog_positive, (dialog, which) -> {
mAdapter.clear();
updateEmptyState();
}).show();
} else if (mAdapter.getItemCount() > 0) {
mAdapter.setDelete(true);
} else {
@@ -91,6 +114,7 @@ public class HistoryActivity extends BaseActivity implements HistoryAdapter.OnCl
if (mAdapter.getItemCount() > 0) return;
mBinding.delete.setVisibility(View.GONE);
mAdapter.setDelete(false);
updateEmptyState();
}
@Override
@@ -16,6 +16,7 @@ import androidx.viewbinding.ViewBinding;
import com.fongmi.android.tv.App;
import com.fongmi.android.tv.R;
import com.fongmi.android.tv.Setting;
import com.fongmi.android.tv.Updater;
import com.fongmi.android.tv.api.config.LiveConfig;
import com.fongmi.android.tv.api.config.VodConfig;
@@ -33,7 +34,6 @@ import com.fongmi.android.tv.server.Server;
import com.fongmi.android.tv.ui.base.BaseActivity;
import com.fongmi.android.tv.ui.custom.FragmentStateManager;
import com.fongmi.android.tv.ui.fragment.SettingFragment;
import com.fongmi.android.tv.ui.fragment.SettingPlayerFragment;
import com.fongmi.android.tv.ui.fragment.VodFragment;
import com.fongmi.android.tv.utils.FileChooser;
import com.fongmi.android.tv.utils.Notify;
@@ -63,6 +63,18 @@ public class HomeActivity extends BaseActivity implements NavigationBarView.OnIt
@Override
protected void initView(Bundle savedInstanceState) {
// 检查隐私协议
if (!Setting.isPrivacyAgreed()) {
Intent intent = new Intent(this, PrivacyAgreementActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
finish();
return;
}
// 确保通知渠道已创建用户已同意协议的情况
com.fongmi.android.tv.utils.Notify.createChannel();
orientation = getResources().getConfiguration().orientation;
Updater.create().release().start(this);
initFragment(savedInstanceState);
@@ -95,7 +107,6 @@ public class HomeActivity extends BaseActivity implements NavigationBarView.OnIt
public Fragment getItem(int position) {
if (position == 0) return VodFragment.newInstance();
if (position == 1) return SettingFragment.newInstance();
if (position == 2) return SettingPlayerFragment.newInstance();
return null;
}
};
@@ -143,7 +154,7 @@ public class HomeActivity extends BaseActivity implements NavigationBarView.OnIt
private void setNavigation() {
mBinding.navigation.getMenu().findItem(R.id.vod).setVisible(true);
mBinding.navigation.getMenu().findItem(R.id.setting).setVisible(true);
mBinding.navigation.getMenu().findItem(R.id.live).setVisible(LiveConfig.hasUrl());
mBinding.navigation.getMenu().findItem(R.id.live).setVisible(LiveConfig.hasUrl() && !Setting.isLiveTabVisible());
}
private boolean openLive() {
@@ -179,7 +190,13 @@ public class HomeActivity extends BaseActivity implements NavigationBarView.OnIt
if (mBinding.navigation.getSelectedItemId() == item.getItemId()) return false;
if (item.getItemId() == R.id.setting) return mManager.change(1);
if (item.getItemId() == R.id.vod) return mManager.change(0);
if (item.getItemId() == R.id.live) return openLive();
if (item.getItemId() == R.id.live) {
if (LiveConfig.isEmpty()) {
Notify.showCenter(R.string.error_no_live);
return false;
}
return openLive();
}
return false;
}
@@ -204,8 +221,6 @@ public class HomeActivity extends BaseActivity implements NavigationBarView.OnIt
protected void onBackPress() {
if (!mBinding.navigation.getMenu().findItem(R.id.vod).isVisible()) {
setNavigation();
} else if (mManager.isVisible(2)) {
change(1);
} else if (mManager.isVisible(1)) {
mBinding.navigation.setSelectedItemId(R.id.vod);
} else if (mManager.canBack(0)) {
@@ -21,6 +21,7 @@ import com.fongmi.android.tv.ui.base.BaseActivity;
import com.fongmi.android.tv.ui.dialog.SyncDialog;
import com.fongmi.android.tv.utils.Notify;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.airbnb.lottie.LottieAnimationView;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
@@ -63,6 +64,25 @@ public class KeepActivity extends BaseActivity implements KeepAdapter.OnClickLis
private void getKeep() {
mAdapter.addAll(Keep.getVod());
mBinding.delete.setVisibility(mAdapter.getItemCount() > 0 ? View.VISIBLE : View.GONE);
updateEmptyState();
}
private void updateEmptyState() {
boolean isEmpty = mAdapter.getItemCount() == 0;
mBinding.emptyLayout.getRoot().setVisibility(isEmpty ? View.VISIBLE : View.GONE);
mBinding.recycler.setVisibility(isEmpty ? View.GONE : View.VISIBLE);
// 控制Lottie动画播放
if (isEmpty) {
try {
LottieAnimationView lottieView = mBinding.emptyLayout.getRoot().findViewById(R.id.lottieAnimation);
if (lottieView != null) {
lottieView.playAnimation();
}
} catch (Exception e) {
// 忽略错误
}
}
}
private void onSync(View view) {
@@ -71,7 +91,10 @@ public class KeepActivity extends BaseActivity implements KeepAdapter.OnClickLis
private void onDelete(View view) {
if (mAdapter.isDelete()) {
new MaterialAlertDialogBuilder(this).setTitle(R.string.dialog_delete_record).setMessage(R.string.dialog_delete_keep).setNegativeButton(R.string.dialog_negative, null).setPositiveButton(R.string.dialog_positive, (dialog, which) -> mAdapter.clear()).show();
new MaterialAlertDialogBuilder(this).setTitle(R.string.dialog_delete_record).setMessage(R.string.dialog_delete_keep).setNegativeButton(R.string.dialog_negative, null).setPositiveButton(R.string.dialog_positive, (dialog, which) -> {
mAdapter.clear();
updateEmptyState();
}).show();
} else if (mAdapter.getItemCount() > 0) {
mAdapter.setDelete(true);
} else {
@@ -114,6 +137,7 @@ public class KeepActivity extends BaseActivity implements KeepAdapter.OnClickLis
if (mAdapter.getItemCount() > 0) return;
mBinding.delete.setVisibility(View.GONE);
mAdapter.setDelete(false);
updateEmptyState();
}
@Override
@@ -1,8 +1,10 @@
package com.fongmi.android.tv.ui.activity;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.drawable.Drawable;
@@ -98,6 +100,8 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
private String tag;
private int count;
private PiP mPiP;
private BroadcastReceiver mScreenReceiver;
private boolean mPausedByScreen = false;
public static void start(Context context) {
if (!LiveConfig.isEmpty()) context.startActivity(new Intent(context, LiveActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).putExtra("empty", false));
@@ -148,6 +152,7 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
mR2 = this::setTraffic;
mR3 = this::hideInfo;
mPiP = new PiP();
initScreenReceiver();
Server.get().start();
setRecyclerView();
setVideoView();
@@ -155,6 +160,33 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
checkLive();
}
private void initScreenReceiver() {
// 屏幕开关监听 - 仅用于画中画模式下控制播放
mScreenReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || intent.getAction() == null) return;
// 只在画中画模式下处理屏幕开关
if (isInPictureInPictureMode()) {
if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
// 画中画模式下关屏暂停播放
if (mPlayers.isPlaying()) {
onPaused();
mPausedByScreen = true;
}
} else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {
// 画中画模式下开屏恢复播放
if (mPausedByScreen) {
onPlay();
mPausedByScreen = false;
}
}
}
}
};
}
@Override
@SuppressLint("ClickableViewAccessibility")
protected void initEvent() {
@@ -1048,6 +1080,8 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
hideInfo();
hideUI();
} else {
// 退出画中画模式时重置屏幕暂停标志
mPausedByScreen = false;
hideInfo();
if (isStop()) finish();
}
@@ -1075,6 +1109,13 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
@Override
protected void onResume() {
super.onResume();
// 注册屏幕开关监听
if (mScreenReceiver != null) {
IntentFilter screenFilter = new IntentFilter();
screenFilter.addAction(Intent.ACTION_SCREEN_ON);
screenFilter.addAction(Intent.ACTION_SCREEN_OFF);
registerReceiver(mScreenReceiver, screenFilter);
}
if (isRedirect()) onPlay();
setRedirect(false);
}
@@ -1082,6 +1123,14 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
@Override
protected void onPause() {
super.onPause();
// 注销屏幕开关监听
try {
if (mScreenReceiver != null) {
unregisterReceiver(mScreenReceiver);
}
} catch (Exception e) {
// Ignore
}
if (isRedirect()) onPaused();
}
@@ -0,0 +1,92 @@
package com.fongmi.android.tv.ui.activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import androidx.viewbinding.ViewBinding;
import com.fongmi.android.tv.R;
import com.fongmi.android.tv.Setting;
import com.fongmi.android.tv.databinding.ActivityPrivacyAgreementBinding;
import com.fongmi.android.tv.ui.base.BaseActivity;
public class PrivacyAgreementActivity extends BaseActivity {
private ActivityPrivacyAgreementBinding mBinding;
@Override
protected ViewBinding getBinding() {
return mBinding = ActivityPrivacyAgreementBinding.inflate(getLayoutInflater());
}
@Override
protected void initView(Bundle savedInstanceState) {
// 隐私协议页面初始化完成
}
@Override
protected void initEvent() {
if (mBinding != null) {
if (mBinding.agreeButton != null) {
mBinding.agreeButton.setOnClickListener(this::onAgree);
}
if (mBinding.disagreeButton != null) {
mBinding.disagreeButton.setOnClickListener(this::onDisagree);
}
}
}
private void onAgree(View view) {
// 用户同意协议
Setting.setPrivacyAgreed(true);
// 创建通知渠道此时才请求通知权限
com.fongmi.android.tv.utils.Notify.createChannel();
// 跳转到主界面清除任务栈避免用户通过任务管理器回到协议页面
Intent intent = new Intent(this, HomeActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
finish();
}
private void onDisagree(View view) {
// 用户不同意协议退出应用
try {
// 清除隐私协议状态可选确保下次启动重新询问
Setting.setPrivacyAgreed(false);
// 优雅地退出应用
finishAffinity();
// 延迟退出 Activity 完成销毁
new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
System.exit(0);
}, 100);
} catch (Exception e) {
e.printStackTrace();
// 备选退出方案
System.exit(0);
}
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// 禁用返回键用户必须做出选择
if (keyCode == KeyEvent.KEYCODE_BACK) {
onDisagree(null);
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
protected void onDestroy() {
// 清理 binding 引用
mBinding = null;
super.onDestroy();
}
}
@@ -1,41 +1,38 @@
package com.fongmi.android.tv.ui.fragment;
package com.fongmi.android.tv.ui.activity;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.provider.Settings;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewbinding.ViewBinding;
import com.fongmi.android.tv.R;
import com.fongmi.android.tv.Setting;
import com.fongmi.android.tv.databinding.FragmentSettingPlayerBinding;
import com.fongmi.android.tv.databinding.ActivitySettingPlayerBinding;
import com.fongmi.android.tv.impl.BufferCallback;
import com.fongmi.android.tv.impl.SpeedCallback;
import com.fongmi.android.tv.impl.UaCallback;
import com.fongmi.android.tv.ui.base.BaseFragment;
import com.fongmi.android.tv.ui.base.BaseActivity;
import com.fongmi.android.tv.ui.dialog.BufferDialog;
import com.fongmi.android.tv.ui.dialog.SpeedDialog;
import com.fongmi.android.tv.ui.dialog.UaDialog;
import com.fongmi.android.tv.utils.ResUtil;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.text.DecimalFormat;
public class SettingPlayerFragment extends BaseFragment implements UaCallback, BufferCallback, SpeedCallback {
public class SettingPlayerActivity extends BaseActivity implements UaCallback, BufferCallback, SpeedCallback {
private FragmentSettingPlayerBinding mBinding;
private ActivitySettingPlayerBinding mBinding;
private DecimalFormat format;
private String[] background;
private String[] caption;
private String[] render;
private String[] scale;
public static SettingPlayerFragment newInstance() {
return new SettingPlayerFragment();
public static void start(Activity activity) {
activity.startActivity(new Intent(activity, SettingPlayerActivity.class));
}
private String getSwitch(boolean value) {
@@ -43,14 +40,13 @@ public class SettingPlayerFragment extends BaseFragment implements UaCallback, B
}
@Override
protected ViewBinding getBinding(@NonNull LayoutInflater inflater, @Nullable ViewGroup container) {
return mBinding = FragmentSettingPlayerBinding.inflate(inflater, container, false);
protected ViewBinding getBinding() {
return mBinding = ActivitySettingPlayerBinding.inflate(getLayoutInflater());
}
@Override
protected void initView() {
protected void initView(Bundle savedInstanceState) {
format = new DecimalFormat("0.#");
mBinding.back.setOnClickListener(v -> requireActivity().onBackPressed());
mBinding.uaText.setText(Setting.getUa());
mBinding.tunnelSwitch.setChecked(Setting.isTunnel());
mBinding.audioDecodeSwitch.setChecked(Setting.isAudioPrefer());
@@ -63,44 +59,25 @@ public class SettingPlayerFragment extends BaseFragment implements UaCallback, B
mBinding.renderText.setText((render = ResUtil.getStringArray(R.array.select_render))[Setting.getRender()]);
mBinding.captionText.setText((caption = ResUtil.getStringArray(R.array.select_caption))[Setting.isCaption() ? 1 : 0]);
mBinding.backgroundText.setText((background = ResUtil.getStringArray(R.array.select_background))[Setting.getBackground()]);
// 设置开关的颜色为黄色
int accentColor = getResources().getColor(R.color.accent);
android.content.res.ColorStateList colorStateList = new android.content.res.ColorStateList(
new int[][]{
new int[]{-android.R.attr.state_checked},
new int[]{android.R.attr.state_checked}
},
new int[]{
0x66FFFFFF, // 未选中时的颜色
accentColor // 选中时的颜色
}
);
mBinding.tunnelSwitch.setThumbTintList(android.content.res.ColorStateList.valueOf(android.graphics.Color.WHITE));
mBinding.tunnelSwitch.setTrackTintList(colorStateList);
mBinding.audioDecodeSwitch.setThumbTintList(android.content.res.ColorStateList.valueOf(android.graphics.Color.WHITE));
mBinding.audioDecodeSwitch.setTrackTintList(colorStateList);
mBinding.aacSwitch.setThumbTintList(android.content.res.ColorStateList.valueOf(android.graphics.Color.WHITE));
mBinding.aacSwitch.setTrackTintList(colorStateList);
mBinding.danmakuLoadSwitch.setThumbTintList(android.content.res.ColorStateList.valueOf(android.graphics.Color.WHITE));
mBinding.danmakuLoadSwitch.setTrackTintList(colorStateList);
}
@Override
protected void initEvent() {
mBinding.back.setOnClickListener(v -> finish());
mBinding.ua.setOnClickListener(this::onUa);
mBinding.aac.setOnClickListener(this::setAAC);
mBinding.scale.setOnClickListener(this::onScale);
mBinding.speed.setOnClickListener(this::onSpeed);
mBinding.buffer.setOnClickListener(this::onBuffer);
mBinding.render.setOnClickListener(this::setRender);
mBinding.tunnel.setOnClickListener(this::setTunnel);
mBinding.caption.setOnClickListener(this::setCaption);
mBinding.caption.setOnLongClickListener(this::onCaption);
mBinding.background.setOnClickListener(this::onBackground);
mBinding.audioDecode.setOnClickListener(this::setAudioDecode);
mBinding.danmakuLoad.setOnClickListener(this::setDanmakuLoad);
// 直接给开关按钮设置点击监听器避免双重点击冲突
mBinding.tunnelSwitch.setOnClickListener(this::setTunnel);
mBinding.audioDecodeSwitch.setOnClickListener(this::setAudioDecode);
mBinding.aacSwitch.setOnClickListener(this::setAAC);
mBinding.danmakuLoadSwitch.setOnClickListener(this::setDanmakuLoad);
}
private void onUa(View view) {
@@ -116,11 +93,11 @@ public class SettingPlayerFragment extends BaseFragment implements UaCallback, B
private void setAAC(View view) {
boolean isChecked = !Setting.isPreferAAC();
Setting.putPreferAAC(isChecked);
mBinding.aacSwitch.setChecked(isChecked);
// 不需要再次调用 setChecked因为点击已经触发了状态变化
}
private void onScale(View view) {
new MaterialAlertDialogBuilder(getActivity()).setTitle(R.string.player_scale).setNegativeButton(R.string.dialog_negative, null).setSingleChoiceItems(scale, Setting.getScale(), (dialog, which) -> {
new com.google.android.material.dialog.MaterialAlertDialogBuilder(this).setTitle(R.string.player_scale).setNegativeButton(R.string.dialog_negative, null).setSingleChoiceItems(scale, Setting.getScale(), (dialog, which) -> {
mBinding.scaleText.setText(scale[which]);
Setting.putScale(which);
dialog.dismiss();
@@ -157,7 +134,7 @@ public class SettingPlayerFragment extends BaseFragment implements UaCallback, B
private void setTunnel(View view) {
boolean isChecked = !Setting.isTunnel();
Setting.putTunnel(isChecked);
mBinding.tunnelSwitch.setChecked(isChecked);
// 不需要再次调用 setChecked因为点击已经触发了状态变化
if (isChecked && Setting.getRender() == 1) setRender(view);
}
@@ -172,7 +149,7 @@ public class SettingPlayerFragment extends BaseFragment implements UaCallback, B
}
private void onBackground(View view) {
new MaterialAlertDialogBuilder(getActivity()).setTitle(R.string.player_background).setNegativeButton(R.string.dialog_negative, null).setSingleChoiceItems(background, Setting.getBackground(), (dialog, which) -> {
new com.google.android.material.dialog.MaterialAlertDialogBuilder(this).setTitle(R.string.player_background).setNegativeButton(R.string.dialog_negative, null).setSingleChoiceItems(background, Setting.getBackground(), (dialog, which) -> {
mBinding.backgroundText.setText(background[which]);
Setting.putBackground(which);
dialog.dismiss();
@@ -182,17 +159,12 @@ public class SettingPlayerFragment extends BaseFragment implements UaCallback, B
private void setAudioDecode(View view) {
boolean isChecked = !Setting.isAudioPrefer();
Setting.putAudioPrefer(isChecked);
mBinding.audioDecodeSwitch.setChecked(isChecked);
// 不需要再次调用 setChecked因为点击已经触发了状态变化
}
private void setDanmakuLoad(View view) {
boolean isChecked = !Setting.isDanmakuLoad();
Setting.putDanmakuLoad(isChecked);
mBinding.danmakuLoadSwitch.setChecked(isChecked);
// 不需要再次调用 setChecked因为点击已经触发了状态变化
}
@Override
public void onHiddenChanged(boolean hidden) {
if (!hidden) initView();
}
}
}
@@ -4,17 +4,24 @@ import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Dialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.BatteryManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.text.Html;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.text.style.ClickableSpan;
import android.view.MotionEvent;
import android.view.View;
@@ -22,6 +29,8 @@ import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import java.util.concurrent.TimeUnit;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -63,6 +72,7 @@ import com.fongmi.android.tv.event.RefreshEvent;
import com.fongmi.android.tv.model.SiteViewModel;
import com.fongmi.android.tv.player.Players;
import com.fongmi.android.tv.player.exo.ExoUtil;
import com.fongmi.android.tv.player.Source;
import com.fongmi.android.tv.service.PlaybackService;
import com.fongmi.android.tv.ui.adapter.EpisodeAdapter;
import com.fongmi.android.tv.ui.adapter.FlagAdapter;
@@ -98,6 +108,7 @@ import com.github.catvod.utils.Trans;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.permissionx.guolindev.PermissionX;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
@@ -148,6 +159,13 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
private Clock mClock;
private String tag;
private PiP mPiP;
private Handler mHandler;
private Runnable mTimeUpdateRunnable;
private BroadcastReceiver mBatteryReceiver;
private BroadcastReceiver mScreenReceiver;
private int mBatteryLevel = -1;
private boolean mIsCharging = false;
private boolean mPausedByScreen = false;
public static void push(FragmentActivity activity, String text) {
if (FileChooser.isValid(activity, Uri.parse(text))) file(activity, FileChooser.getPathFromUri(activity, Uri.parse(text)));
@@ -302,6 +320,131 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
showProgress();
showDanmaku();
checkId();
mHandler = new Handler(Looper.getMainLooper());
initTimeBatteryUpdate();
}
private void initTimeBatteryUpdate() {
mBatteryReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {
int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
if (level != -1 && scale != -1) {
mBatteryLevel = (int) ((level / (float) scale) * 100);
mIsCharging = (status == BatteryManager.BATTERY_STATUS_CHARGING ||
status == BatteryManager.BATTERY_STATUS_FULL);
updateTimeBattery();
}
}
}
};
// 屏幕开关监听 - 仅用于画中画模式下控制播放
mScreenReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || intent.getAction() == null) return;
// 只在画中画模式下处理屏幕开关
if (isInPictureInPictureMode()) {
if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
// 画中画模式下关屏暂停播放
if (mPlayers.isPlaying()) {
onPaused();
mPausedByScreen = true;
}
} else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {
// 画中画模式下开屏恢复播放
if (mPausedByScreen) {
onPlay();
mPausedByScreen = false;
}
}
}
}
};
mTimeUpdateRunnable = new Runnable() {
@Override
public void run() {
updateTimeBattery();
mHandler.postDelayed(this, 30000);
}
};
}
private void updateTimeBattery() {
TextView timeBattery = findViewById(R.id.time_battery);
TextView batteryText = findViewById(R.id.battery_icon);
android.widget.ImageView chargingIndicator = findViewById(R.id.charging_indicator);
// 只在全屏模式下显示
if (isFullscreen()) {
// 更新时间
if (timeBattery != null) {
String time = DateFormat.getTimeFormat(this).format(System.currentTimeMillis());
timeBattery.setText(time);
timeBattery.setVisibility(View.VISIBLE);
}
// 更新充电图标
if (chargingIndicator != null) {
chargingIndicator.setVisibility(mIsCharging && mBatteryLevel >= 0 ? View.VISIBLE : View.GONE);
}
// 更新电池百分比文字
if (batteryText != null && mBatteryLevel >= 0) {
batteryText.setText(mBatteryLevel + "%");
batteryText.setVisibility(View.VISIBLE);
} else if (batteryText != null) {
batteryText.setVisibility(View.GONE);
}
} else {
if (timeBattery != null) {
timeBattery.setVisibility(View.GONE);
}
if (batteryText != null) {
batteryText.setVisibility(View.GONE);
}
if (chargingIndicator != null) {
chargingIndicator.setVisibility(View.GONE);
}
}
}
private void startTimeBatteryUpdates() {
registerReceiver(mBatteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
// 注册屏幕开关监听
IntentFilter screenFilter = new IntentFilter();
screenFilter.addAction(Intent.ACTION_SCREEN_ON);
screenFilter.addAction(Intent.ACTION_SCREEN_OFF);
registerReceiver(mScreenReceiver, screenFilter);
updateTimeBattery();
mHandler.post(mTimeUpdateRunnable);
}
private void stopTimeBatteryUpdates() {
try {
if (mBatteryReceiver != null) {
unregisterReceiver(mBatteryReceiver);
}
} catch (Exception e) {
}
try {
if (mScreenReceiver != null) {
unregisterReceiver(mScreenReceiver);
}
} catch (Exception e) {
}
mHandler.removeCallbacks(mTimeUpdateRunnable);
}
@Override
@@ -441,7 +584,10 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
mBinding.swipeLayout.setRefreshing(false);
if (result.getList().isEmpty()) setEmpty(result.hasMsg());
else setDetail(result.getList().get(0));
Notify.show(result.getMsg());
// 只在有错误或重要消息时显示提示
if (result.hasMsg() && result.getList().isEmpty()) {
Notify.show(result.getMsg());
}
}
private void setEmpty(boolean finish) {
@@ -960,6 +1106,7 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
mBinding.control.bottom.setVisibility(isLock() ? View.GONE : View.VISIBLE);
mBinding.control.top.setVisibility(isLock() ? View.GONE : View.VISIBLE);
mBinding.control.getRoot().setVisibility(View.VISIBLE);
updateTimeBattery();
setR1Callback();
checkPlayImg();
}
@@ -1389,6 +1536,55 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
this.rotate = rotate;
if (fullscreen && rotate) noPadding(mBinding.control.getRoot());
if (fullscreen && !rotate) setPadding(mBinding.control.getRoot());
// 检测屏幕方向变化并处理
onOrientationChanged();
}
// 添加屏幕方向变化处理方法
private void onOrientationChanged() {
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
// 切换到横屏模式
onLandscapeMode();
} else {
// 切换到竖屏模式
onPortraitMode();
}
}
private void onLandscapeMode() {
// 横屏模式下的特殊处理
// 调整进度条的敏感度
if (mPlayers != null) {
long duration = mPlayers.getDuration();
if (duration > TimeUnit.MINUTES.toMillis(30)) {
mBinding.control.seek.setKeyTimeIncrement(TimeUnit.MINUTES.toMillis(1));
} else if (duration > TimeUnit.MINUTES.toMillis(10)) {
mBinding.control.seek.setKeyTimeIncrement(TimeUnit.SECONDS.toMillis(30));
} else if (duration > 0) {
mBinding.control.seek.setKeyTimeIncrement(TimeUnit.SECONDS.toMillis(15));
}
}
// 确保进度条状态正确
if (mPlayers != null) {
long position = mPlayers.getPosition();
long duration = mPlayers.getDuration();
if (position > 0 && duration > 0) {
mBinding.control.seek.setPosition(position);
mBinding.control.seek.setDuration(duration);
}
}
}
private void onPortraitMode() {
// 竖屏模式下的处理
// 恢复进度条的默认敏感度
if (mPlayers != null) {
long duration = mPlayers.getDuration();
if (duration > 0) {
mBinding.control.seek.setKeyTimeIncrement(duration);
}
}
}
public boolean isStop() {
@@ -1490,10 +1686,40 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
@Override
public void onSeekEnd(long time) {
mBinding.widget.seek.setVisibility(View.GONE);
mPlayers.seek(time);
showProgress();
onPlay();
handleLandscapeSeek(time);
}
// 添加新的方法处理横屏模式下的特殊逻辑
private void handleLandscapeSeek(long time) {
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
// 横屏模式下的特殊处理
mBinding.widget.seek.setVisibility(View.GONE);
mPlayers.pause();
mPlayers.seek(time);
showProgress();
App.post(() -> {
long actualPosition = mPlayers.getPosition();
if (Math.abs(actualPosition - time) > 500) {
mPlayers.seek(time);
}
onPlay();
hideProgress();
}, 150); // 横屏模式下延迟更长确保跳转完成
} else {
// 竖屏模式使用原有逻辑
mBinding.widget.seek.setVisibility(View.GONE);
mPlayers.pause();
mPlayers.seek(time);
showProgress();
App.post(() -> {
long actualPosition = mPlayers.getPosition();
if (Math.abs(actualPosition - time) > 500) {
mPlayers.seek(time);
}
onPlay();
hideProgress();
}, 100); // 竖屏模式下延迟较短
}
}
@Override
@@ -1544,6 +1770,8 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
hideDanmaku();
hideSheet();
} else {
// 退出画中画模式时重置屏幕暂停标志
mPausedByScreen = false;
showDanmaku();
if (isStop()) finish();
}
@@ -1555,6 +1783,7 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
if (isAutoRotate() && isPort() && newConfig.orientation == Configuration.ORIENTATION_PORTRAIT && !isRotate()) exitFullscreen();
if (isAutoRotate() && isPort() && newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) enterFullscreen();
if (isFullscreen()) Util.hideSystemUI(this);
updateTimeBattery();
}
@Override
@@ -1574,6 +1803,7 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
@Override
protected void onResume() {
super.onResume();
startTimeBatteryUpdates();
if (isRedirect()) onPlay();
setRedirect(false);
}
@@ -1581,6 +1811,7 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
@Override
protected void onPause() {
super.onPause();
stopTimeBatteryUpdates();
if (isRedirect()) onPaused();
}
@@ -1608,14 +1839,17 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
protected void onDestroy() {
super.onDestroy();
stopSearch();
mClock.release();
mPlayers.release();
mClock.release();
Timer.get().reset();
RefreshEvent.history();
PlaybackService.stop();
mHandler.removeCallbacksAndMessages(null);
App.removeCallbacks(mR1, mR2, mR3, mR4);
EventBus.getDefault().unregister(this);
mViewModel.result.removeObserver(mObserveDetail);
mViewModel.player.removeObserver(mObservePlayer);
mViewModel.search.removeObserver(mObserveSearch);
stopTimeBatteryUpdates();
}
}
@@ -11,6 +11,7 @@ import com.fongmi.android.tv.api.config.VodConfig;
import com.fongmi.android.tv.bean.Config;
import com.fongmi.android.tv.databinding.AdapterConfigBinding;
import java.util.ArrayList;
import java.util.List;
public class ConfigAdapter extends RecyclerView.Adapter<ConfigAdapter.ViewHolder> {
@@ -26,19 +27,36 @@ public class ConfigAdapter extends RecyclerView.Adapter<ConfigAdapter.ViewHolder
void onTextClick(Config item);
void onCopyClick(Config item);
void onDeleteClick(Config item);
}
public ConfigAdapter addAll(int type) {
mItems = Config.getAll(type);
mItems.remove(type == 0 ? VodConfig.get().getConfig() : LiveConfig.get().getConfig());
mItems = new ArrayList<>();
List<Config> configs = Config.getAll(type);
Config currentConfig = type == 0 ? VodConfig.get().getConfig() : LiveConfig.get().getConfig();
for (Config config : configs) {
if (config.equals(currentConfig) || config.isEmpty()) continue;
mItems.add(config);
}
return this;
}
public void addItem(Config item) {
if (item.isEmpty()) return;
mItems.add(0, item);
notifyItemInserted(0);
}
public int remove(Config item) {
int position = mItems.indexOf(item);
item.delete();
mItems.remove(item);
notifyDataSetChanged();
notifyItemRemoved(position);
return getItemCount();
}
@@ -58,6 +76,7 @@ public class ConfigAdapter extends RecyclerView.Adapter<ConfigAdapter.ViewHolder
Config item = mItems.get(position);
holder.binding.text.setText(item.getDesc());
holder.binding.text.setOnClickListener(v -> mListener.onTextClick(item));
holder.binding.copy.setOnClickListener(v -> mListener.onCopyClick(item));
holder.binding.delete.setOnClickListener(v -> mListener.onDeleteClick(item));
}
@@ -44,7 +44,6 @@ public class TypeAdapter extends RecyclerView.Adapter<TypeAdapter.ViewHolder> {
public void addAll(Result result) {
mItems.addAll(result.getTypes());
if (!result.getList().isEmpty()) mItems.add(0, home());
if (!mItems.isEmpty()) mItems.get(0).setActivated(true);
notifyDataSetChanged();
}
@@ -12,6 +12,7 @@ import android.view.WindowManager;
import androidx.activity.OnBackPressedCallback;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.viewbinding.ViewBinding;
import com.fongmi.android.tv.R;
@@ -32,6 +33,7 @@ public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
super.onCreate(savedInstanceState);
if (transparent()) setTransparent(this);
setContentView(getBinding().getRoot());
@@ -8,6 +8,7 @@ import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.WindowManager;
import android.content.res.Configuration;
import androidx.annotation.NonNull;
@@ -109,6 +110,17 @@ public class CustomKeyDownVod extends GestureDetector.SimpleOnGestureListener im
if (isEdge(e1) || changeScale || lock || e1.getPointerCount() > 1) return true;
float deltaX = e2.getX() - e1.getX();
float deltaY = e1.getY() - e2.getY();
// 在横屏模式下调整触摸事件的处理逻辑
if (activity.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
// 横屏模式下增加对水平滑动的敏感度
if (Math.abs(deltaX) > Math.abs(deltaY) * 0.5f) {
if (touch) checkFunc(distanceX, distanceY, e2);
if (changeTime) listener.onSeek(time = (long) (deltaX * 50));
return true;
}
}
if (touch) checkFunc(distanceX, distanceY, e2);
if (changeTime) listener.onSeek(time = (long) (deltaX * 50));
if (changeBright) setBright(deltaY);
@@ -145,9 +157,32 @@ public class CustomKeyDownVod extends GestureDetector.SimpleOnGestureListener im
private void checkFunc(float distanceX, float distanceY, MotionEvent e2) {
int four = ResUtil.getScreenWidth(activity) / 4;
if (e2.getX() > four && e2.getX() < four * 3) center = true;
else if (Math.abs(distanceX) < Math.abs(distanceY)) checkSide(e2);
if (Math.abs(distanceX) >= Math.abs(distanceY)) changeTime = true;
// 在横屏模式下调整中心区域的判断
if (activity.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
// 横屏模式下扩大中心区域更容易触发进度条调整
int centerStart = ResUtil.getScreenWidth(activity) / 3;
int centerEnd = ResUtil.getScreenWidth(activity) * 2 / 3;
if (e2.getX() > centerStart && e2.getX() < centerEnd) {
center = true;
} else if (Math.abs(distanceX) < Math.abs(distanceY)) {
checkSide(e2);
}
// 横屏模式下降低触发进度条调整的阈值
if (Math.abs(distanceX) >= Math.abs(distanceY) * 0.7f) {
changeTime = true;
}
} else {
// 竖屏模式保持原有逻辑
if (e2.getX() > four && e2.getX() < four * 3) {
center = true;
} else if (Math.abs(distanceX) < Math.abs(distanceY)) {
checkSide(e2);
}
if (Math.abs(distanceX) >= Math.abs(distanceY)) {
changeTime = true;
}
}
touch = false;
}
@@ -0,0 +1,137 @@
package com.fongmi.android.tv.ui.custom;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import androidx.appcompat.widget.AppCompatCheckBox;
public class CustomSwitch extends AppCompatCheckBox {
private Paint trackPaint;
private Paint thumbPaint;
private RectF trackRect;
private RectF thumbRect;
private float thumbPosition = 0f; // 0 = 左边, 1 = 右边
private int currentTrackColor;
private int currentThumbColor;
private static final int TRACK_COLOR_OFF = 0xFF555555; // 灰色
private static final int TRACK_COLOR_ON = 0xFFFFEB3B; // 黄色
private static final int THUMB_COLOR_OFF = 0xFFFFFFFF; // 白色
private static final int THUMB_COLOR_ON = 0xFF000000; // 黑色
private ValueAnimator animator;
public CustomSwitch(Context context) {
super(context);
init();
}
public CustomSwitch(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CustomSwitch(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
// 隐藏默认的checkbox样式
setButtonDrawable(null);
trackPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
thumbPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
trackRect = new RectF();
thumbRect = new RectF();
currentTrackColor = TRACK_COLOR_OFF;
currentThumbColor = THUMB_COLOR_OFF;
// 监听状态变化
setOnCheckedChangeListener((buttonView, isChecked) -> animateSwitch(isChecked));
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 固定尺寸50dp × 30dp
int width = (int) (50 * getResources().getDisplayMetrics().density);
int height = (int) (30 * getResources().getDisplayMetrics().density);
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
int width = getWidth();
int height = getHeight();
float radius = height / 2f;
// 绘制轨道
trackRect.set(0, 0, width, height);
trackPaint.setColor(currentTrackColor);
canvas.drawRoundRect(trackRect, radius, radius, trackPaint);
// 计算小圆位置
float thumbSize = height - 8 * getResources().getDisplayMetrics().density; // 22dp
float padding = 4 * getResources().getDisplayMetrics().density;
float thumbLeft = padding + thumbPosition * (width - thumbSize - 2 * padding);
float thumbTop = padding;
// 绘制小圆
thumbRect.set(thumbLeft, thumbTop, thumbLeft + thumbSize, thumbTop + thumbSize);
thumbPaint.setColor(currentThumbColor);
canvas.drawOval(thumbRect, thumbPaint);
}
private void animateSwitch(boolean isChecked) {
if (animator != null && animator.isRunning()) {
animator.cancel();
}
float targetPosition = isChecked ? 1f : 0f;
int targetTrackColor = isChecked ? TRACK_COLOR_ON : TRACK_COLOR_OFF;
int targetThumbColor = isChecked ? THUMB_COLOR_ON : THUMB_COLOR_OFF;
animator = ValueAnimator.ofFloat(thumbPosition, targetPosition);
animator.setDuration(250); // 250ms动画时长
final ArgbEvaluator colorEvaluator = new ArgbEvaluator();
animator.addUpdateListener(animation -> {
thumbPosition = (float) animation.getAnimatedValue();
// 颜色渐变
currentTrackColor = (int) colorEvaluator.evaluate(
thumbPosition, TRACK_COLOR_OFF, TRACK_COLOR_ON
);
currentThumbColor = (int) colorEvaluator.evaluate(
thumbPosition, THUMB_COLOR_OFF, THUMB_COLOR_ON
);
invalidate();
});
animator.start();
}
@Override
public void setChecked(boolean checked) {
super.setChecked(checked);
// 初始化时不播放动画
if (!isAttachedToWindow()) {
thumbPosition = checked ? 1f : 0f;
currentTrackColor = checked ? TRACK_COLOR_ON : TRACK_COLOR_OFF;
currentThumbColor = checked ? THUMB_COLOR_ON : THUMB_COLOR_OFF;
}
}
}
@@ -0,0 +1,54 @@
package com.fongmi.android.tv.ui.dialog;
import android.content.Intent;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewbinding.ViewBinding;
import com.fongmi.android.tv.databinding.DialogAboutBinding;
public class AboutDialog extends BaseDialog {
private DialogAboutBinding binding;
public static void show(FragmentActivity activity) {
new AboutDialog().show(activity.getSupportFragmentManager(), "AboutDialog");
}
public static void show(Fragment fragment) {
new AboutDialog().show(fragment.getChildFragmentManager(), "AboutDialog");
}
@Override
protected ViewBinding getBinding(@NonNull LayoutInflater inflater, @Nullable ViewGroup container) {
binding = DialogAboutBinding.inflate(inflater, container, false);
return binding;
}
@Override
protected void initEvent() {
binding.github.setOnClickListener(v -> openGitHub());
}
private void openGitHub() {
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://github.com/Tosencen/XMBOX/releases/latest"));
startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}
@@ -1,5 +1,6 @@
package com.fongmi.android.tv.ui.dialog;
import android.app.Activity;
import android.content.DialogInterface;
import android.view.LayoutInflater;
@@ -22,11 +23,20 @@ public class BufferDialog {
return new BufferDialog(fragment);
}
public static BufferDialog create(Activity activity) {
return new BufferDialog(activity);
}
public BufferDialog(Fragment fragment) {
this.callback = (BufferCallback) fragment;
this.binding = DialogBufferBinding.inflate(LayoutInflater.from(fragment.getContext()));
}
public BufferDialog(Activity activity) {
this.callback = (BufferCallback) activity;
this.binding = DialogBufferBinding.inflate(LayoutInflater.from(activity));
}
public void show() {
initDialog();
initView();
@@ -21,6 +21,7 @@ import com.fongmi.android.tv.api.config.WallConfig;
import com.fongmi.android.tv.bean.Config;
import com.fongmi.android.tv.databinding.DialogConfigBinding;
import com.fongmi.android.tv.impl.ConfigCallback;
import com.fongmi.android.tv.impl.Callback;
import com.fongmi.android.tv.ui.custom.CustomTextListener;
import com.fongmi.android.tv.utils.FileChooser;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@@ -52,6 +53,10 @@ public class ConfigDialog {
public ConfigDialog(Fragment fragment) {
this.fragment = fragment;
// 确保fragment实现了ConfigCallback接口
if (!(fragment instanceof ConfigCallback)) {
throw new IllegalArgumentException("Fragment must implement ConfigCallback");
}
this.callback = (ConfigCallback) fragment;
this.binding = DialogConfigBinding.inflate(LayoutInflater.from(fragment.getContext()));
this.append = true;
@@ -144,9 +149,89 @@ public class ConfigDialog {
private void onPositive(DialogInterface dialog, int which) {
String url = binding.url.getText().toString().trim();
String name = binding.name.getText().toString().trim();
android.util.Log.d("ConfigDialog", "onPositive: type=" + type + ", url=" + url + ", name=" + name);
// 如果是编辑模式更新现有配置
if (edit) Config.find(ori, type).url(url).name(name).update();
if (url.isEmpty()) Config.delete(ori, type);
callback.setConfig(Config.find(url, type));
// 如果URL为空删除配置
if (url.isEmpty()) {
android.util.Log.d("ConfigDialog", "URL is empty, deleting config");
Config.delete(ori, type);
dialog.dismiss();
return;
}
// 只有URL不为空时才设置配置
// 保存原始URL以便在添加失败时恢复
String originalUrl = ori;
android.util.Log.d("ConfigDialog", "Calling Config.find with url=" + url + ", type=" + type);
Config config = Config.find(url, type);
android.util.Log.d("ConfigDialog", "Config.find returned: " + (config != null ? config.toString() : "null"));
android.util.Log.d("ConfigDialog", "Checking callback: " + (callback != null ? callback.getClass().getName() : "null"));
android.util.Log.d("ConfigDialog", "Checking fragment: " + (fragment != null ? fragment.getClass().getName() : "null"));
android.util.Log.d("ConfigDialog", "Calling callback.setConfig");
callback.setConfig(config);
android.util.Log.d("ConfigDialog", "setConfig completed");
// 添加一个延迟检查如果配置没有成功加载则恢复原始URL
new android.os.Handler().postDelayed(() -> {
// 检查配置是否成功加载
Config currentConfig = getConfig();
if (currentConfig == null || !currentConfig.getUrl().equals(url)) {
// 配置加载失败恢复原始URL
if (!TextUtils.isEmpty(originalUrl)) {
// 如果有原始URL恢复原始URL
callback.setConfig(Config.find(originalUrl, type));
} else {
// 如果没有原始URL设置为空
switch (type) {
case 0:
VodConfig.get().clear().config(Config.vod()).load(new Callback() {
@Override
public void success() {}
@Override
public void success(String result) {}
@Override
public void error(String msg) {}
});
break;
case 1:
LiveConfig.get().clear().config(Config.live()).load(new Callback() {
@Override
public void success() {}
@Override
public void success(String result) {}
@Override
public void error(String msg) {}
});
break;
case 2:
WallConfig.get().clear().config(Config.wall()).load(new Callback() {
@Override
public void success() {}
@Override
public void success(String result) {}
@Override
public void error(String msg) {}
});
break;
}
}
}
}, 2000); // 2秒后检查
dialog.dismiss();
}
@@ -23,13 +23,16 @@ import com.fongmi.android.tv.ui.base.ViewType;
import com.fongmi.android.tv.ui.custom.SpaceItemDecoration;
import com.fongmi.android.tv.utils.ResUtil;
import com.fongmi.android.tv.utils.Timer;
import com.fongmi.android.tv.utils.Util;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.google.android.material.slider.Slider;
import java.util.Arrays;
import java.util.Formatter;
import java.util.List;
import java.util.Locale;
public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickListener {
public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickListener, Timer.Callback {
private DialogControlBinding binding;
private ActivityVideoBinding parent;
@@ -40,6 +43,8 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
private History history;
private Players player;
private boolean parse;
private StringBuilder builder;
private Formatter formatter;
public static ControlDialog create() {
return new ControlDialog();
@@ -47,6 +52,8 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
public ControlDialog() {
this.scale = ResUtil.getStringArray(R.array.select_scale);
this.builder = new StringBuilder();
this.formatter = new Formatter(builder, Locale.getDefault());
}
public ControlDialog parent(ActivityVideoBinding parent) {
@@ -93,6 +100,15 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
binding.opening.setText(parent.control.action.opening.getText());
binding.loop.setActivated(parent.control.action.loop.isActivated());
binding.timer.setActivated(Timer.get().isRunning());
// 设置定时器回调并更新按钮文字
if (Timer.get().isRunning()) {
Timer.get().setCallback(this);
updateTimerText(Timer.get().getTick());
} else {
binding.timer.setText(R.string.play_timer);
}
setTrackVisible();
setScaleText();
setPlayer();
@@ -126,6 +142,8 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
private void setSpeed(@NonNull Slider slider, float value, boolean fromUser) {
parent.control.action.speed.setText(player.setSpeed(value));
if (history != null) history.setSpeed(player.getSpeed());
// 实时更新倍速数值显示
binding.speedValue.setText(String.format("%.1fx", value));
}
private void setScaleText() {
@@ -179,6 +197,8 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
binding.player.setText(parent.control.action.player.getText());
binding.decode.setVisibility(parent.control.action.decode.getVisibility());
binding.danmaku.setVisibility(parent.control.action.danmaku.getVisibility());
// 初始化倍速数值显示
binding.speedValue.setText(String.format("%.1fx", Math.max(player.getSpeed(), 0.5f)));
}
public void setParseVisible(boolean visible) {
@@ -199,6 +219,42 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
binding.parse.getAdapter().notifyItemRangeChanged(0, binding.parse.getAdapter().getItemCount());
}
/**
* 更新定时按钮文字为倒计时
*/
private void updateTimerText(long tick) {
if (tick > 0) {
binding.timer.setText(Util.format(builder, formatter, tick));
} else {
binding.timer.setText(R.string.play_timer);
}
}
/**
* Timer.Callback 接口实现 - 定时器每秒回调
*/
@Override
public void onTick(long tick) {
updateTimerText(tick);
}
/**
* Timer.Callback 接口实现 - 定时完成回调
*/
@Override
public void onFinish() {
// 定时结束恢复按钮文字
binding.timer.setText(R.string.play_timer);
binding.timer.setActivated(false);
}
@Override
public void dismiss() {
// 关闭对话框时取消定时器回调
Timer.get().setCallback(null);
super.dismiss();
}
public interface Listener {
void onScale(int tag);
@@ -1,15 +1,20 @@
package com.fongmi.android.tv.ui.dialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.view.LayoutInflater;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import com.fongmi.android.tv.App;
import com.fongmi.android.tv.bean.Config;
import com.fongmi.android.tv.databinding.DialogHistoryBinding;
import com.fongmi.android.tv.impl.ConfigCallback;
import com.fongmi.android.tv.ui.adapter.ConfigAdapter;
import com.fongmi.android.tv.ui.custom.SpaceItemDecoration;
import com.fongmi.android.tv.utils.Notify;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
public class HistoryDialog implements ConfigAdapter.OnClickListener {
@@ -45,7 +50,6 @@ public class HistoryDialog implements ConfigAdapter.OnClickListener {
binding.recycler.setHasFixedSize(true);
binding.recycler.setAdapter(adapter.addAll(type));
binding.recycler.addItemDecoration(new SpaceItemDecoration(1, 8));
binding.recycler.post(() -> binding.recycler.scrollToPosition(0));
}
private void setDialog() {
@@ -56,12 +60,53 @@ public class HistoryDialog implements ConfigAdapter.OnClickListener {
@Override
public void onTextClick(Config item) {
callback.setConfig(item);
// 防止重复点击和空值
if (!dialog.isShowing() || item == null) return;
// 检查callback是否有效
if (callback == null) {
dialog.dismiss();
return;
}
// 先关闭对话框避免时序冲突
dialog.dismiss();
// 延迟执行配置设置确保对话框完全关闭
App.post(() -> {
try {
// 双重检查callback和item是否仍然有效
if (callback != null && item != null && !item.isEmpty()) {
callback.setConfig(item);
}
} catch (Exception e) {
e.printStackTrace();
// 如果出现异常显示错误提示
try {
Notify.show("配置切换失败: " + e.getMessage());
} catch (Exception ex) {
ex.printStackTrace();
}
}
}, 150); // 增加延迟到150毫秒
}
@Override
public void onCopyClick(Config item) {
ClipboardManager manager = (ClipboardManager) App.get().getSystemService(Context.CLIPBOARD_SERVICE);
manager.setPrimaryClip(ClipData.newPlainText("url", item.getUrl()));
Notify.showCenter("复制成功");
}
@Override
public void onDeleteClick(Config item) {
if (adapter.remove(item) == 0) dialog.dismiss();
int count = adapter.remove(item);
if (count == 0) {
dialog.dismiss();
} else {
// 强制重新测量布局高度
binding.recycler.requestLayout();
dialog.getWindow().setLayout(dialog.getWindow().getAttributes().width, android.view.ViewGroup.LayoutParams.WRAP_CONTENT);
}
}
}
@@ -1,5 +1,6 @@
package com.fongmi.android.tv.ui.dialog;
import android.app.Activity;
import android.content.DialogInterface;
import android.view.LayoutInflater;
@@ -22,11 +23,20 @@ public class SpeedDialog {
return new SpeedDialog(fragment);
}
public static SpeedDialog create(Activity activity) {
return new SpeedDialog(activity);
}
public SpeedDialog(Fragment fragment) {
this.callback = (SpeedCallback) fragment;
this.binding = DialogSpeedBinding.inflate(LayoutInflater.from(fragment.getContext()));
}
public SpeedDialog(Activity activity) {
this.callback = (SpeedCallback) activity;
this.binding = DialogSpeedBinding.inflate(LayoutInflater.from(activity));
}
public void show() {
initDialog();
initView();
@@ -60,6 +60,8 @@ public class TimerDialog extends BaseDialog implements Timer.Callback {
binding.time2.setOnClickListener(this::setTimer);
binding.time3.setOnClickListener(this::setTimer);
binding.time4.setOnClickListener(this::setTimer);
binding.time5.setOnClickListener(this::setTimer);
binding.time6.setOnClickListener(this::setTimer);
}
private void setTimer(View view) {
@@ -1,5 +1,6 @@
package com.fongmi.android.tv.ui.dialog;
import android.app.Activity;
import android.content.DialogInterface;
import android.text.TextUtils;
import android.view.LayoutInflater;
@@ -27,12 +28,22 @@ public class UaDialog {
return new UaDialog(fragment);
}
public static UaDialog create(Activity activity) {
return new UaDialog(activity);
}
public UaDialog(Fragment fragment) {
this.callback = (UaCallback) fragment;
this.binding = DialogUaBinding.inflate(LayoutInflater.from(fragment.getContext()));
this.append = true;
}
public UaDialog(Activity activity) {
this.callback = (UaCallback) activity;
this.binding = DialogUaBinding.inflate(LayoutInflater.from(activity));
this.append = true;
}
public void show() {
initDialog();
initView();
@@ -3,9 +3,17 @@ package com.fongmi.android.tv.ui.fragment;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -31,7 +39,9 @@ import com.fongmi.android.tv.impl.ProxyCallback;
import com.fongmi.android.tv.impl.SiteCallback;
import com.fongmi.android.tv.player.Source;
import com.fongmi.android.tv.ui.activity.HomeActivity;
import com.fongmi.android.tv.ui.activity.SettingPlayerActivity;
import com.fongmi.android.tv.ui.base.BaseFragment;
import com.fongmi.android.tv.ui.dialog.AboutDialog;
import com.fongmi.android.tv.ui.dialog.ConfigDialog;
import com.fongmi.android.tv.ui.dialog.HistoryDialog;
import com.fongmi.android.tv.ui.dialog.LiveDialog;
@@ -91,25 +101,10 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
@Override
protected void initView() {
mBinding.vodUrl.setText(VodConfig.getDesc());
mBinding.liveUrl.setText(LiveConfig.getDesc());
mBinding.wallUrl.setText(WallConfig.getDesc());
mBinding.versionText.setText(BuildConfig.VERSION_NAME);
// 设置开关的颜色为黄色
int accentColor = getResources().getColor(R.color.accent);
android.content.res.ColorStateList colorStateList = new android.content.res.ColorStateList(
new int[][]{
new int[]{-android.R.attr.state_checked},
new int[]{android.R.attr.state_checked}
},
new int[]{
0x66FFFFFF, // 未选中时的颜色
accentColor // 选中时的颜色
}
);
mBinding.incognitoSwitch.setThumbTintList(android.content.res.ColorStateList.valueOf(android.graphics.Color.WHITE));
mBinding.incognitoSwitch.setTrackTintList(colorStateList);
setSourceHintText(mBinding.vodUrl, VodConfig.getDesc(), R.string.source_hint_setting);
setSourceHintText(mBinding.liveUrl, LiveConfig.getDesc(), R.string.source_hint_live);
// setSourceHintText(mBinding.wallUrl, WallConfig.getDesc(), R.string.source_hint_wall); // 壁纸功能已移除
mBinding.versionText.setText(getString(R.string.setting_version) + " " + BuildConfig.VERSION_NAME);
setOtherText();
setCacheText();
@@ -122,7 +117,30 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
mBinding.dohText.setText(getDohList()[getDohIndex()]);
mBinding.proxyText.setText(getProxy(Setting.getProxy()));
mBinding.incognitoSwitch.setChecked(Setting.isIncognito());
mBinding.liveTabVisibleSwitch.setChecked(Setting.isLiveTabVisible());
mBinding.sizeText.setText((size = ResUtil.getStringArray(R.array.select_size))[Setting.getSize()]);
setLiveSettingsVisibility();
}
private void setLiveSettingsVisibility() {
boolean isLiveTabVisible = !Setting.isLiveTabVisible(); // 注意这里取反因为开关是"隐藏直播"
// 获取直播容器的布局参数
LinearLayout.LayoutParams liveContainerParams = (LinearLayout.LayoutParams) mBinding.liveContainer.getLayoutParams();
if (isLiveTabVisible) {
// 直播开关打开显示直播模块间距为12dp
mBinding.liveContainer.setVisibility(View.VISIBLE);
liveContainerParams.topMargin = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 12, getResources().getDisplayMetrics());
} else {
// 直播开关关闭隐藏直播模块间距为0dp这样视频模块和下一个模块之间会有正常间距
mBinding.liveContainer.setVisibility(View.GONE);
liveContainerParams.topMargin = 0;
}
// 应用布局参数
mBinding.liveContainer.setLayoutParams(liveContainerParams);
}
private void setCacheText() {
@@ -138,54 +156,84 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
protected void initEvent() {
mBinding.vod.setOnClickListener(this::onVod);
mBinding.live.setOnClickListener(this::onLive);
mBinding.wall.setOnClickListener(this::onWall);
// mBinding.wall.setOnClickListener(this::onWall); // 壁纸功能已移除
mBinding.proxy.setOnClickListener(this::onProxy);
mBinding.cache.setOnClickListener(this::onCache);
mBinding.backup.setOnClickListener(this::onBackup);
mBinding.player.setOnClickListener(this::onPlayer);
mBinding.restore.setOnClickListener(this::onRestore);
mBinding.version.setOnClickListener(this::onVersion);
mBinding.about.setOnClickListener(this::onAbout);
mBinding.vod.setOnLongClickListener(this::onVodEdit);
mBinding.vodHome.setOnClickListener(this::onVodHome);
mBinding.live.setOnLongClickListener(this::onLiveEdit);
mBinding.liveHome.setOnClickListener(this::onLiveHome);
mBinding.wall.setOnLongClickListener(this::onWallEdit);
// mBinding.wall.setOnLongClickListener(this::onWallEdit); // 壁纸功能已移除
mBinding.vodHistory.setOnClickListener(this::onVodHistory);
mBinding.version.setOnLongClickListener(this::onVersionDev);
mBinding.liveHistory.setOnClickListener(this::onLiveHistory);
mBinding.wallDefault.setOnClickListener(this::setWallDefault);
mBinding.wallRefresh.setOnClickListener(this::setWallRefresh);
mBinding.incognito.setOnClickListener(this::setIncognito);
// mBinding.wallDefault.setOnClickListener(this::setWallDefault); // 壁纸功能已移除
// mBinding.wallRefresh.setOnClickListener(this::setWallRefresh); // 壁纸功能已移除
mBinding.incognitoSwitch.setOnClickListener(this::setIncognito);
mBinding.liveTabVisibleSwitch.setOnClickListener(this::setLiveTabVisible);
mBinding.size.setOnClickListener(this::setSize);
mBinding.doh.setOnClickListener(this::setDoh);
}
@Override
public void setConfig(Config config) {
if (config.getUrl().startsWith("file") && !PermissionX.isGranted(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
PermissionX.init(this).permissions(Manifest.permission.WRITE_EXTERNAL_STORAGE).request((allGranted, grantedList, deniedList) -> load(config));
} else {
load(config);
// 添加Fragment状态检查防止在无效状态下执行
if (getActivity() == null || !isAdded() || isDetached()) return;
// 如果URL为空不进行任何操作
if (config == null || config.isEmpty()) return;
try {
if (config.getUrl().startsWith("file") && !PermissionX.isGranted(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
PermissionX.init(this).permissions(Manifest.permission.WRITE_EXTERNAL_STORAGE).request((allGranted, grantedList, deniedList) -> {
if (getActivity() != null && isAdded()) {
load(config);
}
});
} else {
load(config);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void load(Config config) {
switch (config.getType()) {
case 0:
Notify.progress(getActivity());
VodConfig.load(config, getCallback(0));
mBinding.vodUrl.setText(config.getDesc());
break;
case 1:
Notify.progress(getActivity());
LiveConfig.load(config, getCallback(1));
mBinding.liveUrl.setText(config.getDesc());
break;
case 2:
Notify.progress(getActivity());
WallConfig.load(config, getCallback(2));
mBinding.wallUrl.setText(config.getDesc());
break;
// 再次检查Fragment状态防止在异步回调中执行
if (getActivity() == null || !isAdded() || isDetached()) return;
try {
switch (config.getType()) {
case 0:
Notify.progress(getActivity());
VodConfig.load(config, getCallback(0));
if (mBinding != null && mBinding.vodUrl != null) {
mBinding.vodUrl.setText(config.getDesc());
}
break;
case 1:
Notify.progress(getActivity());
LiveConfig.load(config, getCallback(1));
if (mBinding != null && mBinding.liveUrl != null) {
mBinding.liveUrl.setText(config.getDesc());
}
break;
case 2:
Notify.progress(getActivity());
WallConfig.load(config, getCallback(2));
// if (mBinding != null && mBinding.wallUrl != null) { // 壁纸功能已移除
// mBinding.wallUrl.setText(config.getDesc());
// }
break;
}
} catch (Exception e) {
e.printStackTrace();
Notify.dismiss();
}
}
@@ -193,18 +241,35 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
return new Callback() {
@Override
public void success(String result) {
// 检查Fragment是否还在活动状态
if (getActivity() == null || !isAdded()) return;
Notify.show(result);
}
@Override
public void success() {
// 检查Fragment是否还在活动状态
if (getActivity() == null || !isAdded()) return;
setConfig(type);
}
@Override
public void error(String msg) {
// 检查Fragment是否还在活动状态
if (getActivity() == null || !isAdded()) return;
Notify.show(msg);
setConfig(type);
Notify.dismiss();
switch (type) {
case 0:
setSourceHintText(mBinding.vodUrl, VodConfig.getDesc(), R.string.source_hint_setting);
break;
case 1:
setSourceHintText(mBinding.liveUrl, LiveConfig.getDesc(), R.string.source_hint_live);
break;
case 2:
// setSourceHintText(mBinding.wallUrl, WallConfig.getDesc(), R.string.source_hint_wall); // 壁纸功能已移除
break;
}
}
};
}
@@ -216,24 +281,37 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
Notify.dismiss();
RefreshEvent.video();
RefreshEvent.config();
mBinding.vodUrl.setText(VodConfig.getDesc());
mBinding.liveUrl.setText(LiveConfig.getDesc());
mBinding.wallUrl.setText(WallConfig.getDesc());
setSourceHintText(mBinding.vodUrl, VodConfig.getDesc(), R.string.source_hint_setting);
setSourceHintText(mBinding.liveUrl, LiveConfig.getDesc(), R.string.source_hint_live);
// setSourceHintText(mBinding.wallUrl, WallConfig.getDesc(), R.string.source_hint_wall); // 壁纸功能已移除
break;
case 1:
setCacheText();
Notify.dismiss();
RefreshEvent.config();
mBinding.liveUrl.setText(LiveConfig.getDesc());
setSourceHintText(mBinding.liveUrl, LiveConfig.getDesc(), R.string.source_hint_live);
break;
case 2:
setCacheText();
Notify.dismiss();
mBinding.wallUrl.setText(WallConfig.getDesc());
// setSourceHintText(mBinding.wallUrl, WallConfig.getDesc(), R.string.source_hint_wall); // 壁纸功能已移除
break;
}
}
private void setSourceHintText(TextView textView, String desc, int hintStringRes) {
if (TextUtils.isEmpty(desc)) {
SpannableString spannable = new SpannableString(getString(hintStringRes));
spannable.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.white)), 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spannable.setSpan(new RelativeSizeSpan(0.8f), 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
int alpha = (int)(255 * 0.5f);
spannable.setSpan(new ForegroundColorSpan(android.graphics.Color.argb(alpha, 255, 255, 255)), 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(spannable);
} else {
textView.setText(desc);
}
}
@Override
public void setSite(Site item) {
VodConfig.get().setHome(item);
@@ -293,12 +371,16 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
}
private void onPlayer(View view) {
getRoot().change(2);
SettingPlayerActivity.start(requireActivity());
}
private void onVersion(View view) {
Updater.create().force().release().start(getActivity());
}
private void onAbout(View view) {
AboutDialog.show(this);
}
private boolean onVersionDev(View view) {
Updater.create().force().dev().start(getActivity());
@@ -323,7 +405,17 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
private void setIncognito(View view) {
boolean isChecked = !Setting.isIncognito();
Setting.putIncognito(isChecked);
mBinding.incognitoSwitch.setChecked(isChecked);
// 不需要再次调用 setChecked因为点击已经触发了状态变化
}
private void setLiveTabVisible(View view) {
boolean isChecked = !Setting.isLiveTabVisible();
Setting.putLiveTabVisible(isChecked);
// 发送刷新事件通知主界面更新导航栏
RefreshEvent.config();
// 更新直播设置项的可见性
setLiveSettingsVisibility();
// 不需要再次调用 setChecked因为点击已经触发了状态变化
}
private void setSize(View view) {
@@ -415,9 +507,9 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
@Override
public void onHiddenChanged(boolean hidden) {
if (hidden) return;
mBinding.vodUrl.setText(VodConfig.getDesc());
mBinding.liveUrl.setText(LiveConfig.getDesc());
mBinding.wallUrl.setText(WallConfig.getDesc());
setSourceHintText(mBinding.vodUrl, VodConfig.getDesc(), R.string.source_hint_setting);
setSourceHintText(mBinding.liveUrl, LiveConfig.getDesc(), R.string.source_hint_live);
// setSourceHintText(mBinding.wallUrl, WallConfig.getDesc(), R.string.source_hint_wall); // 壁纸功能已移除
setCacheText();
}
@@ -26,6 +26,8 @@ import com.fongmi.android.tv.R;
import com.fongmi.android.tv.Setting;
import com.fongmi.android.tv.api.config.VodConfig;
import com.fongmi.android.tv.bean.Class;
import com.fongmi.android.tv.bean.Config;
import com.fongmi.android.tv.bean.History;
import com.fongmi.android.tv.bean.Hot;
import com.fongmi.android.tv.bean.Result;
import com.fongmi.android.tv.bean.Site;
@@ -35,6 +37,7 @@ import com.fongmi.android.tv.event.CastEvent;
import com.fongmi.android.tv.event.RefreshEvent;
import com.fongmi.android.tv.event.StateEvent;
import com.fongmi.android.tv.impl.Callback;
import com.fongmi.android.tv.impl.ConfigCallback;
import com.fongmi.android.tv.impl.FilterCallback;
import com.fongmi.android.tv.impl.SiteCallback;
import com.fongmi.android.tv.model.SiteViewModel;
@@ -42,13 +45,17 @@ import com.fongmi.android.tv.ui.activity.CollectActivity;
import com.fongmi.android.tv.ui.activity.HistoryActivity;
import com.fongmi.android.tv.ui.activity.KeepActivity;
import com.fongmi.android.tv.ui.activity.VideoActivity;
import com.airbnb.lottie.LottieAnimationView;
import com.fongmi.android.tv.ui.adapter.TypeAdapter;
import com.fongmi.android.tv.ui.base.BaseFragment;
import com.fongmi.android.tv.ui.dialog.ConfigDialog;
import com.fongmi.android.tv.ui.dialog.FilterDialog;
import com.fongmi.android.tv.ui.dialog.LastWatchToast;
import com.fongmi.android.tv.ui.dialog.LinkDialog;
import com.fongmi.android.tv.ui.dialog.ReceiveDialog;
import com.fongmi.android.tv.ui.dialog.SiteDialog;
import com.fongmi.android.tv.utils.FileChooser;
import com.fongmi.android.tv.utils.Notify;
import com.fongmi.android.tv.utils.ResUtil;
import com.fongmi.android.tv.utils.UrlUtil;
import com.github.catvod.net.OkHttp;
@@ -68,7 +75,7 @@ import okhttp3.Call;
import okhttp3.Headers;
import okhttp3.Response;
public class VodFragment extends BaseFragment implements SiteCallback, FilterCallback, TypeAdapter.OnClickListener {
public class VodFragment extends BaseFragment implements SiteCallback, FilterCallback, TypeAdapter.OnClickListener, ConfigCallback {
private FragmentVodBinding mBinding;
private SiteViewModel mViewModel;
@@ -99,10 +106,37 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
EventBus.getDefault().register(this);
setRecyclerView();
setViewModel();
showProgress();
initStartupState(); // 根据是否已有配置来设置初始状态
setLogo();
initHot();
getHot();
// 检查是否需要显示上次播放弹窗
checkLastWatchDialog();
}
// 初始化启动状态区分已有配置和无配置的情况
private void initStartupState() {
// 检查是否已经有保存的配置添加空值检查
boolean hasExistingConfig = false;
try {
Config config = VodConfig.get().getConfig();
hasExistingConfig = config != null &&
config.getUrl() != null &&
!config.getUrl().isEmpty();
} catch (Exception e) {
// 如果获取配置时出错认为没有配置
hasExistingConfig = false;
}
if (hasExistingConfig) {
// 已有配置显示加载状态确保不显示添加源提示
showProgress();
mBinding.emptySourceHint.setVisibility(View.GONE);
} else {
// 无配置立即显示空源提示不显示加载状态
hideProgress();
checkEmptySource();
}
}
@Override
@@ -127,6 +161,23 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
});
}
// 添加检查上次播放历史并显示弹窗的方法
private void checkLastWatchDialog() {
if (App.isAppJustLaunched()) {
List<History> histories = History.get();
if (!histories.isEmpty()) {
App.setAppLaunched();
App.post(() -> {
if (getActivity() != null) {
LastWatchToast.create(getActivity(), histories.get(0)).show();
}
}, 1000);
} else {
App.setAppLaunched();
}
}
}
private void setRecyclerView() {
mBinding.type.setHasFixedSize(true);
mBinding.type.setItemAnimator(null);
@@ -173,10 +224,179 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
setFabVisible(0);
hideProgress();
checkRetry();
checkEmptySource(); // 添加检查是否显示空源提示
}
// 修改checkEmptySource方法增强鲁棒性
private void checkEmptySource() {
// 检查是否有基础配置文件添加空值检查
boolean hasBaseConfig = false;
try {
Config config = VodConfig.get().getConfig();
hasBaseConfig = config != null &&
config.getUrl() != null &&
!config.getUrl().isEmpty();
} catch (Exception e) {
hasBaseConfig = false;
}
// 检查是否有有效的站点配置
boolean hasValidSites = false;
boolean hasValidHome = false;
try {
hasValidSites = VodConfig.get().getSites().size() > 0;
Site site = getSite();
hasValidHome = site != null && site.getKey() != null && !site.getKey().isEmpty();
} catch (Exception e) {
hasValidSites = false;
hasValidHome = false;
}
// 只有在完全没有配置文件或配置文件无效时才显示空源提示
boolean isEmpty = !hasBaseConfig || (!hasValidSites || !hasValidHome);
if (mBinding.emptySourceHint != null) {
mBinding.emptySourceHint.setVisibility(isEmpty ? View.VISIBLE : View.GONE);
if (isEmpty) {
// 设置整个布局的点击事件
mBinding.emptySourceHint.setOnClickListener(this::onAddSource);
// 设置按钮的点击事件
if (mBinding.addSourceBtn != null) {
mBinding.addSourceBtn.setOnClickListener(this::onAddSource);
}
// 空源状态下隐藏所有悬浮按钮
hideFabButtons();
// 启动Lottie动画
try {
LottieAnimationView lottieView = mBinding.emptySourceHint.findViewById(R.id.lottieAnimation);
if (lottieView != null) {
lottieView.playAnimation();
}
} catch (Exception e) {
// 忽略错误
}
}
}
}
// 添加源按钮点击事件处理
private void onAddSource(View view) {
ConfigDialog.create(this).type(0).show();
}
// 实现ConfigCallback接口
@Override
public void setConfig(Config config) {
android.util.Log.d("VodFragment", "setConfig called with: " + (config != null ? config.toString() : "null"));
if (config == null || config.isEmpty()) {
android.util.Log.d("VodFragment", "Config is null or empty, returning");
return;
}
// 检查Fragment是否还在活动状态增强检查
if (!isValidFragmentState()) {
android.util.Log.d("VodFragment", "Fragment state invalid, returning");
return;
}
android.util.Log.d("VodFragment", "Fragment state valid, proceeding with config load");
// 安全地隐藏空源提示
try {
if (mBinding != null && mBinding.emptySourceHint != null) {
mBinding.emptySourceHint.setVisibility(View.GONE);
}
} catch (Exception e) {
e.printStackTrace();
}
Notify.progress(getActivity());
android.util.Log.d("VodFragment", "Calling VodConfig.load");
VodConfig.load(config, new Callback() {
@Override
public void success() {
android.util.Log.d("VodFragment", "VodConfig.load success callback");
// 双重检查Fragment是否还在活动状态
if (!isValidFragmentState()) {
android.util.Log.d("VodFragment", "Fragment state invalid in success callback");
return;
}
try {
android.util.Log.d("VodFragment", "Success: dismissing notify and refreshing");
Notify.dismiss();
RefreshEvent.config();
RefreshEvent.video();
homeContent();
} catch (Exception e) {
android.util.Log.e("VodFragment", "Error in success callback", e);
e.printStackTrace();
}
}
@Override
public void error(String msg) {
android.util.Log.e("VodFragment", "VodConfig.load error: " + msg);
// 双重检查Fragment是否还在活动状态
if (!isValidFragmentState()) {
android.util.Log.d("VodFragment", "Fragment state invalid in error callback");
return;
}
try {
Notify.dismiss();
Notify.show(msg);
// 加载失败时重新显示空源提示
checkEmptySource();
} catch (Exception e) {
android.util.Log.e("VodFragment", "Error in error callback", e);
e.printStackTrace();
}
}
});
}
// 添加Fragment状态检查方法
private boolean isValidFragmentState() {
return getActivity() != null &&
!getActivity().isFinishing() &&
!getActivity().isDestroyed() &&
isAdded() &&
!isDetached() &&
!isRemoving() &&
getView() != null &&
mBinding != null;
}
private void setFabVisible(int position) {
if (mAdapter.getItemCount() == 0) {
// 检查是否为空源状态 - 使用与checkEmptySource相同的逻辑添加空值检查
boolean hasBaseConfig = false;
boolean hasValidSites = false;
boolean hasValidHome = false;
try {
Config config = VodConfig.get().getConfig();
hasBaseConfig = config != null &&
config.getUrl() != null &&
!config.getUrl().isEmpty();
hasValidSites = VodConfig.get().getSites().size() > 0;
Site site = getSite();
hasValidHome = site != null && site.getKey() != null && !site.getKey().isEmpty();
} catch (Exception e) {
hasBaseConfig = false;
hasValidSites = false;
hasValidHome = false;
}
boolean isEmpty = !hasBaseConfig || (!hasValidSites || !hasValidHome);
if (isEmpty) {
// 空源状态下隐藏所有悬浮按钮
hideFabButtons();
} else if (mAdapter.getItemCount() == 0) {
mBinding.top.setVisibility(View.INVISIBLE);
mBinding.link.setVisibility(View.VISIBLE);
mBinding.filter.setVisibility(View.GONE);
@@ -190,6 +410,13 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
mBinding.link.show();
}
}
// 隐藏所有悬浮按钮的方法
private void hideFabButtons() {
mBinding.top.setVisibility(View.GONE);
mBinding.link.setVisibility(View.GONE);
mBinding.filter.setVisibility(View.GONE);
}
private void checkRetry() {
mBinding.retry.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
@@ -247,6 +474,14 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
private void homeContent() {
showProgress();
setFabVisible(0);
// 安全地隐藏空源提示
try {
if (mBinding != null && mBinding.emptySourceHint != null) {
mBinding.emptySourceHint.setVisibility(View.GONE);
}
} catch (Exception e) {
e.printStackTrace();
}
mAdapter.clear();
mViewModel.homeContent();
mBinding.pager.setAdapter(new PageAdapter(getChildFragmentManager()));
@@ -296,6 +531,7 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
switch (event.getType()) {
case EMPTY:
hideProgress();
checkEmptySource(); // 添加检查是否显示空源提示
break;
case PROGRESS:
showProgress();
@@ -59,7 +59,7 @@ public class Timer {
public void delay() {
cancel();
set(TimeUnit.MINUTES.toMillis(5) + tick);
set(TimeUnit.MINUTES.toMillis(15) + tick);
}
public void reset() {
+1 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/white" android:state_checked="true" />
<item android:color="@color/black" android:state_checked="true" />
<item android:color="@color/white" android:state_checked="false" />
</selector>
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/white" android:state_focused="true" android:state_selected="true" />
<item android:color="@color/green_400" android:state_selected="true" />
<item android:color="@color/primary" android:state_selected="true" />
<item android:color="@color/white" />
</selector>
+1 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/white" android:state_checked="true" />
<item android:color="@color/black" android:state_checked="true" />
<item android:color="@color/white" android:state_checked="false" />
</selector>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 703 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 890 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 869 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 B

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#1AFFFFFF" />
<corners android:radius="8dp" />
<size android:height="48dp" />
</shape>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 开启状态:黄色底 + 黑色圆在右边 -->
<item android:state_checked="true">
<layer-list>
<!-- 黄色轨道 -->
<item>
<shape android:shape="rectangle">
<solid android:color="#FFEB3B" />
<corners android:radius="15dp" />
</shape>
</item>
<!-- 黑色小圆(右边,精确定位 22dp) -->
<item android:top="4dp" android:bottom="4dp" android:right="4dp" android:left="24dp">
<shape android:shape="oval">
<solid android:color="#000000" />
</shape>
</item>
</layer-list>
</item>
<!-- 关闭状态:灰色底 + 白色圆在左边 -->
<item>
<layer-list>
<!-- 灰色轨道 -->
<item>
<shape android:shape="rectangle">
<solid android:color="#555555" />
<corners android:radius="15dp" />
</shape>
</item>
<!-- 白色小圆(左边,精确定位 22dp) -->
<item android:top="4dp" android:bottom="4dp" android:left="4dp" android:right="24dp">
<shape android:shape="oval">
<solid android:color="#FFFFFF" />
</shape>
</item>
</layer-list>
</item>
</selector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,595Q553,595 604.5,543.5Q656,492 656,419Q656,346 604.5,294.5Q553,243 480,243Q407,243 355.5,294.5Q304,346 304,419Q304,492 355.5,543.5Q407,595 480,595ZM480,420ZM480,760Q587,760 693,724.5Q799,689 896,619Q902,615 905,608.5Q908,602 908,595Q908,588 905,582Q902,576 896,572Q758,474 658,447.5Q558,421 480,421Q402,421 302,447.5Q202,474 64,572Q58,576 55,582Q52,588 52,595Q52,602 55,608.5Q58,615 64,619Q161,689 267,724.5Q373,760 480,760Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M312,720Q261,720 214.5,702Q168,684 131,649Q83,604 61.5,542.5Q40,481 40,415Q40,337 78,288.5Q116,240 189,240Q203,240 215.5,242.5Q228,245 241,250L480,339L719,250Q732,245 744.5,242.5Q757,240 771,240Q844,240 882,288.5Q920,337 920,415Q920,481 898.5,542.5Q877,604 829,649Q792,684 745.5,702Q699,720 648,720Q582,720 536,690Q490,660 490,660L470,660Q470,660 424,690Q378,720 312,720ZM312,640Q349,640 381,622.5Q413,605 440,580L520,580Q547,605 579,622.5Q611,640 648,640Q684,640 717.5,627.5Q751,615 777,589Q811,555 825.5,509Q840,463 840,415Q840,374 823,346.5Q806,319 769,320Q766,320 747,324L480,424L213,324Q208,322 202.5,321Q197,320 191,320Q154,320 137,347Q120,374 120,415Q120,464 134.5,510Q149,556 184,590Q210,615 243,627.5Q276,640 312,640ZM361,580Q398,580 419,563.5Q440,547 440,518Q440,469 375.5,424.5Q311,380 239,380Q202,380 181,396.5Q160,413 160,442Q160,491 224.5,535.5Q289,580 361,580ZM355,520Q317,520 272.5,495Q228,470 220,444Q225,442 231.5,440.5Q238,439 245,439Q283,439 327.5,464.5Q372,490 380,516Q375,518 368.5,519Q362,520 355,520ZM599,581Q671,581 735.5,536Q800,491 800,442Q800,413 779.5,396Q759,379 721,379Q649,379 584.5,424Q520,469 520,518Q520,547 541,564Q562,581 599,581ZM605,520Q598,520 592,519Q586,518 581,516Q589,490 633.5,465Q678,440 716,440Q723,440 729,441Q735,442 740,444Q732,470 687.5,495Q643,520 605,520ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,840Q363,840 281.5,758.5Q200,677 200,560Q200,483 225.5,405Q251,327 291.5,263.5Q332,200 382,160Q432,120 480,120Q529,120 578.5,160Q628,200 668.5,263.5Q709,327 734.5,405Q760,483 760,560Q760,677 678.5,758.5Q597,840 480,840ZM480,760Q563,760 621.5,701.5Q680,643 680,560Q680,503 660.5,440Q641,377 611.5,323.5Q582,270 547,235Q512,200 480,200Q449,200 413.5,235Q378,270 348.5,323.5Q319,377 299.5,440Q280,503 280,560Q280,643 338.5,701.5Q397,760 480,760ZM520,720Q537,720 548.5,708.5Q560,697 560,680Q560,663 548.5,651.5Q537,640 520,640Q470,640 435,605Q400,570 400,520Q400,503 388.5,491.5Q377,480 360,480Q343,480 331.5,491.5Q320,503 320,520Q320,603 378.5,661.5Q437,720 520,720ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M123,520Q122,510 121.5,500Q121,490 121,480Q121,405 149,339.5Q177,274 226,225.5Q275,177 340,148.5Q405,120 480,120Q555,120 620.5,148.5Q686,177 734.5,225.5Q783,274 811.5,339.5Q840,405 840,480Q840,490 839.5,500Q839,510 838,520L757,520Q759,510 759.5,500Q760,490 760,480Q760,470 759.5,460Q759,450 757,440L639,440Q640,450 640,460Q640,470 640,480Q640,490 640,500Q640,510 639,520L560,520Q560,512 560,503.5Q560,495 560,487Q560,475 559.5,463Q559,451 558,440L403,440Q402,451 401.5,463Q401,475 401,487Q401,495 401,503.5Q401,512 401,520L322,520Q321,510 321,500Q321,490 321,480Q321,470 321,460Q321,450 322,440L204,440Q202,450 201.5,460Q201,470 201,480Q201,490 201.5,500Q202,510 204,520L123,520ZM228,360L331,360Q339,317 351,282.5Q363,248 377,220Q329,238 290,274.5Q251,311 228,360ZM414,360L546,360Q536,317 521,276Q506,235 480,200Q454,235 438.5,276Q423,317 414,360ZM630,360L733,360Q710,311 670.5,274.5Q631,238 583,220Q597,250 609.5,283.5Q622,317 630,360ZM440,840L440,800Q440,750 405,715Q370,680 320,680L80,680L80,600L320,600Q368,600 409.5,621Q451,642 480,680Q509,642 550.5,621Q592,600 640,600L880,600L880,680L640,680Q590,680 555,715Q520,750 520,800L520,840L440,840Z"/>
</vector>
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M390,880L322,760L190,760L100,600L168,480L100,360L190,200L322,200L390,80L570,80L638,200L770,200L860,360L792,480L860,600L770,760L638,760L570,880L390,880ZM638,440L724,440L768,360L724,280L638,280L593,360L638,440ZM438,560L522,560L567,480L522,400L438,400L393,480L438,560ZM438,320L522,320L568,239L523,160L437,160L392,239L438,320ZM237,440L322,440L367,360L322,280L237,280L192,360L237,440ZM237,680L322,680L367,600L322,520L236,520L192,600L237,680ZM437,800L523,800L568,721L522,640L438,640L392,721L437,800ZM638,680L723,680L768,600L723,520L638,520L593,600L638,680Z"/>
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5s5,-2.24 5,-5S14.76,7 12,7L12,7zM17,15c0.75,-1.06 1.19,-2.36 1.19,-3.77c0,-3.61 -2.92,-6.54 -6.54,-6.54c-0.84,0 -1.65,0.16 -2.39,0.45L12,7.78L12,7.78c2.33,0 4.22,1.89 4.22,4.22c0,1.04 -0.38,1.99 -1,2.73V15zM2,4.27l2.28,2.28l0.46,0.46C3.08,8.3 1.78,10.02 1,12c1.73,4.39 6,7.5 11,7.5c1.55,0 3.03,-0.3 4.38,-0.84l0.42,0.42L19.73,22L21,20.73L3.27,3L2,4.27zM7.53,9.8l1.55,1.55c-0.05,0.21 -0.08,0.43 -0.08,0.65c0,1.66 1.34,3 3,3c0.22,0 0.44,-0.03 0.65,-0.08l1.55,1.55c-0.67,0.33 -1.41,0.53 -2.2,0.53c-2.76,0 -5,-2.24 -5,-5C7,11.21 7.2,10.47 7.53,9.8z"/>
</vector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
</vector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF0000"
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
</vector>
+13 -5
View File
@@ -1,10 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="960"
android:viewportHeight="960">
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M438,617L613,503Q627,494 627,478Q627,462 613,453L438,339Q423,329 407.5,337.37Q392,345.74 392,364L392,592Q392,610.26 407.5,618.63Q423,627 438,617ZM140,800Q116,800 98,782Q80,764 80,740L80,220Q80,196 98,178Q116,160 140,160L820,160Q844.75,160 862.38,178Q880,196 880,220L880,740Q880,764 862.38,782Q844.75,800 820,800L140,800ZM140,740L140,740Q140,740 140,740Q140,740 140,740L140,220Q140,220 140,220Q140,220 140,220L140,220Q140,220 140,220Q140,220 140,220L140,740Q140,740 140,740Q140,740 140,740ZM140,740L820,740Q820,740 820,740Q820,740 820,740L820,220Q820,220 820,220Q820,220 820,220L140,220Q140,220 140,220Q140,220 140,220L140,740Q140,740 140,740Q140,740 140,740Z" />
android:fillColor="#FF000000"
android:fillAlpha="0"
android:strokeWidth="1.5"
android:strokeColor="#FF000000"
android:pathData="M3,6C3,4.89543 3.89543,4 5,4H19C20.1046,4 21,4.89543 21,6V18C21,19.1046 20.1046,20 19,20H5C3.89543,20 3,19.1046 3,18V6Z"/>
<path
android:fillColor="#FF000000"
android:fillAlpha="0"
android:strokeWidth="1.5"
android:strokeColor="#FF000000"
android:pathData="M10,9l5,3l-5,3l0,-6z"/>
</vector>
@@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:strokeWidth="1.5"
android:strokeColor="#FF000000"
android:pathData="M3,6C3,4.89543 3.89543,4 5,4H19C20.1046,4 21,4.89543 21,6V18C21,19.1046 20.1046,20 19,20H5C3.89543,20 3,19.1046 3,18V6Z"/>
<path
android:fillColor="#FF000000"
android:strokeWidth="1.5"
android:strokeColor="#FF000000"
android:pathData="M10,9l5,3l-5,3l0,-6z"/>
</vector>
+15 -7
View File
@@ -1,10 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="960"
android:viewportHeight="960">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M546,880L414,880Q403,880 394.5,873Q386,866 384,855L368,754Q349,747 328,735Q307,723 291,710L198,753Q187,758 176,754.5Q165,751 159,740L93,623Q87,613 90,602Q93,591 102,584L188,521Q186,512 185.5,500.5Q185,489 185,480Q185,471 185.5,459.5Q186,448 188,439L102,376Q93,369 90,358Q87,347 93,337L159,220Q165,209 176,205.5Q187,202 198,207L291,250Q307,237 328,225Q349,213 368,207L384,105Q386,94 394.5,87Q403,80 414,80L546,80Q557,80 565.5,87Q574,94 576,105L592,206Q611,213 632.5,224.5Q654,236 669,250L762,207Q773,202 784,205.5Q795,209 801,220L867,336Q873,346 870.5,357.5Q868,369 858,376L772,437Q774,447 774.5,458.5Q775,470 775,480Q775,490 774.5,501Q774,512 772,522L858,584Q867,591 870,602Q873,613 867,623L801,740Q795,751 784,754.5Q773,758 762,753L669,710Q653,723 632.5,735.5Q612,748 592,754L576,855Q574,866 565.5,873Q557,880 546,880ZM480,610Q534,610 572,572Q610,534 610,480Q610,426 572,388Q534,350 480,350Q426,350 388,388Q350,426 350,480Q350,534 388,572Q426,610 480,610ZM480,550Q451,550 430.5,529.5Q410,509 410,480Q410,451 430.5,430.5Q451,410 480,410Q509,410 529.5,430.5Q550,451 550,480Q550,509 529.5,529.5Q509,550 480,550ZM480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480ZM436,820L524,820L538,708Q571,700 600.5,683Q630,666 654,642L760,688L800,616L706,547Q710,530 712.5,513.5Q715,497 715,480Q715,463 713,446.5Q711,430 706,413L800,344L760,272L654,318Q631,292 602,274.5Q573,257 538,252L524,140L436,140L422,252Q388,259 358.5,276Q329,293 306,318L200,272L160,344L254,413Q250,430 247.5,446.5Q245,463 245,480Q245,497 247.5,513.5Q250,530 254,547L160,616L200,688L306,642Q330,666 359.5,683Q389,700 422,708L436,820Z" />
android:fillColor="#FF000000"
android:fillAlpha="0"
android:strokeWidth="1.5"
android:strokeColor="#FF000000"
android:pathData="M12,12m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/>
<path
android:fillColor="#FF000000"
android:fillAlpha="0"
android:strokeWidth="1.5"
android:strokeColor="#FF000000"
android:pathData="M19.14,12.94C19.18,12.64 19.2,12.32 19.2,12C19.2,11.68 19.18,11.36 19.14,11.06L21.16,9.48C21.34,9.34 21.4,9.08 21.28,8.88L19.36,5.52C19.24,5.32 18.98,5.24 18.76,5.32L16.36,6.34C15.84,5.94 15.28,5.62 14.66,5.38L14.3,2.8C14.26,2.58 14.08,2.4 13.84,2.4H10.16C9.92,2.4 9.74,2.58 9.7,2.8L9.34,5.38C8.72,5.62 8.16,5.94 7.64,6.34L5.24,5.32C5.02,5.24 4.76,5.32 4.64,5.52L2.72,8.88C2.6,9.08 2.66,9.34 2.84,9.48L4.86,11.06C4.82,11.36 4.8,11.68 4.8,12C4.8,12.32 4.82,12.64 4.86,12.94L2.84,14.52C2.66,14.66 2.6,14.92 2.72,15.12L4.64,18.48C4.76,18.68 5.02,18.76 5.24,18.68L7.64,17.66C8.16,18.06 8.72,18.38 9.34,18.62L9.7,21.2C9.74,21.42 9.92,21.6 10.16,21.6H13.84C14.08,21.6 14.26,21.42 14.3,21.2L14.66,18.62C15.28,18.38 15.84,18.06 16.36,17.66L18.76,18.68C18.98,18.76 19.24,18.68 19.36,18.48L21.28,15.12C21.4,14.92 21.34,14.66 21.16,14.52L19.14,12.94Z"/>
</vector>
@@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:strokeWidth="1.5"
android:strokeColor="#FF000000"
android:pathData="M12,12m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/>
<path
android:fillColor="#FF000000"
android:strokeWidth="1.5"
android:strokeColor="#FF000000"
android:pathData="M19.14,12.94C19.18,12.64 19.2,12.32 19.2,12C19.2,11.68 19.18,11.36 19.14,11.06L21.16,9.48C21.34,9.34 21.4,9.08 21.28,8.88L19.36,5.52C19.24,5.32 18.98,5.24 18.76,5.32L16.36,6.34C15.84,5.94 15.28,5.62 14.66,5.38L14.3,2.8C14.26,2.58 14.08,2.4 13.84,2.4H10.16C9.92,2.4 9.74,2.58 9.7,2.8L9.34,5.38C8.72,5.62 8.16,5.94 7.64,6.34L5.24,5.32C5.02,5.24 4.76,5.32 4.64,5.52L2.72,8.88C2.6,9.08 2.66,9.34 2.84,9.48L4.86,11.06C4.82,11.36 4.8,11.68 4.8,12C4.8,12.32 4.82,12.64 4.86,12.94L2.84,14.52C2.66,14.66 2.6,14.92 2.72,15.12L4.64,18.48C4.76,18.68 5.02,18.76 5.24,18.68L7.64,17.66C8.16,18.06 8.72,18.38 9.34,18.62L9.7,21.2C9.74,21.42 9.92,21.6 10.16,21.6H13.84C14.08,21.6 14.26,21.42 14.3,21.2L14.66,18.62C15.28,18.38 15.84,18.06 16.36,17.66L18.76,18.68C18.98,18.76 19.24,18.68 19.36,18.48L21.28,15.12C21.4,14.92 21.34,14.66 21.16,14.52L19.14,12.94Z"/>
</vector>
@@ -1,7 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="960"
android:viewportHeight="960">
<path
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M140,160L214,312L344,312L270,160L359,160L433,312L563,312L489,160L578,160L652,312L782,312L708,160L820,160Q844,160 862,178Q880,196 880,220L880,740Q880,764 862,782Q844,800 820,800L140,800Q116,800 98,782Q80,764 80,740L80,220Q80,196 98,178Q116,160 140,160L140,160ZM140,372L140,740Q140,740 140,740Q140,740 140,740L820,740Q820,740 820,740Q820,740 820,740L820,372L140,372ZM140,372L140,372L140,740Q140,740 140,740Q140,740 140,740L140,740Q140,740 140,740Q140,740 140,740L140,372Z" />
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z" />
</vector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M20,3H4C2.9,3 2,3.9 2,5v14c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V5C22,3.9 21.1,3 20,3zM9,17H7v-5h2V17zM13,17h-2V7h2V17zM17,17h-2v-9h2V17z" />
</vector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4zM12,11.99h7c-0.53,4.12 -3.28,7.79 -7,8.94V12H5V6.3l7,-3.11v8.8z" />
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3,17v2h6v-2H3zM3,5v2h10V5H3zM13,21v-2h8v-2h-8v-2h-2v6H13zM7,9v2H3v2h4v2h2V9H7zM21,13v-2H11v2H21zM15,9h2V7h4V5h-4V3h-2V9z" />
</vector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z" />
</vector>
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,880Q346,880 253,787Q160,694 160,560L160,360Q160,238 256,159Q352,80 480,80Q608,80 704,159Q800,238 800,360L800,880L480,880ZM480,800L560,800Q541,775 530.5,744.5Q520,714 520,680L520,638Q510,639 500,639.5Q490,640 480,640Q413,640 350.5,616.5Q288,593 240,545L240,560Q240,660 310,730Q380,800 480,800ZM600,680Q600,730 635,765Q670,800 720,800L720,545Q694,571 664,589.5Q634,608 600,620L600,680ZM440,400Q440,334 395,289Q350,244 286,241Q264,265 252,295Q240,325 240,360Q240,449 312.5,504.5Q385,560 480,560Q575,560 647.5,504.5Q720,449 720,360Q720,325 708,294.5Q696,264 674,240Q610,242 565,288Q520,334 520,400L440,400ZM340,400Q323,400 311.5,388.5Q300,377 300,360Q300,343 311.5,331.5Q323,320 340,320Q357,320 368.5,331.5Q380,343 380,360Q380,377 368.5,388.5Q357,400 340,400ZM620,400Q603,400 591.5,388.5Q580,377 580,360Q580,343 591.5,331.5Q603,320 620,320Q637,320 648.5,331.5Q660,343 660,360Q660,377 648.5,388.5Q637,400 620,400ZM370,182Q404,196 432,219Q460,242 480,271Q500,242 527.5,219Q555,196 589,182Q564,171 536.5,165.5Q509,160 480,160Q451,160 423.5,165.5Q396,171 370,182ZM800,800L800,800L720,800Q670,800 635,800Q600,800 600,800L600,800Q580,800 560,800Q540,800 520,800L520,800Q520,800 578.5,800Q637,800 720,800L800,800ZM480,800Q380,800 310,730Q240,660 240,560L240,560Q240,660 310,730Q380,800 480,800Q540,800 550,800Q560,800 560,800L560,800Q560,800 560,800Q560,800 560,800L480,800ZM600,680L600,680Q600,730 635,765Q670,800 720,800L720,800Q670,800 635,765Q600,730 600,680ZM480,271Q480,271 480,271Q480,271 480,271Q480,271 480,271Q480,271 480,271Q480,271 480,271Q480,271 480,271Q480,271 480,271Q480,271 480,271Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M508,760L732,760Q725,786 708,802Q691,818 664,822L228,875Q195,880 168.5,859.5Q142,839 138,806L85,369Q81,336 101,310Q121,284 154,280L200,274L200,354L164,359Q164,359 164,359Q164,359 164,359L218,796Q218,796 218,796Q218,796 218,796L508,760ZM360,680Q327,680 303.5,656.5Q280,633 280,600L280,160Q280,127 303.5,103.5Q327,80 360,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,600Q880,633 856.5,656.5Q833,680 800,680L360,680ZM360,600L800,600Q800,600 800,600Q800,600 800,600L800,160Q800,160 800,160Q800,160 800,160L360,160Q360,160 360,160Q360,160 360,160L360,600Q360,600 360,600Q360,600 360,600ZM580,380Q580,380 580,380Q580,380 580,380L580,380Q580,380 580,380Q580,380 580,380L580,380Q580,380 580,380Q580,380 580,380L580,380Q580,380 580,380Q580,380 580,380ZM218,796L218,796L218,796L218,796L218,796Q218,796 218,796Q218,796 218,796ZM581,560Q649,560 696.5,513Q744,466 749,400Q681,400 632.5,447Q584,494 581,560ZM581,560Q578,494 529.5,447Q481,400 413,400Q418,466 465.5,513Q513,560 581,560ZM581,440Q598,440 609.5,428.5Q621,417 621,400L621,390L631,394Q646,400 661.5,397Q677,394 685,380Q694,365 691,348Q688,331 671,324L661,320L671,316Q688,309 690.5,291.5Q693,274 685,260Q676,245 661,242.5Q646,240 631,246L621,250L621,240Q621,223 609.5,211.5Q598,200 581,200Q564,200 552.5,211.5Q541,223 541,240L541,250L531,246Q516,240 501,242.5Q486,245 477,260Q469,274 471.5,291.5Q474,309 491,316L501,320L491,324Q474,331 471,348Q468,365 477,380Q485,394 500.5,397Q516,400 531,394L541,390L541,400Q541,417 552.5,428.5Q564,440 581,440ZM581,360Q564,360 552.5,348.5Q541,337 541,320Q541,303 552.5,291.5Q564,280 581,280Q598,280 609.5,291.5Q621,303 621,320Q621,337 609.5,348.5Q598,360 581,360Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M342,800L618,800Q618,800 618,800Q618,800 618,800L658,640L302,640L342,800Q342,800 342,800Q342,800 342,800ZM342,880Q314,880 293,863Q272,846 265,819L220,640L740,640L695,819Q688,846 667,863Q646,880 618,880L342,880ZM200,560L760,560Q760,560 760,560Q760,560 760,560L760,480L200,480L200,560Q200,560 200,560Q200,560 200,560ZM480,320Q480,220 550,150Q620,80 720,80Q720,170 663,236Q606,302 520,316L520,400L840,400L840,560Q840,593 816.5,616.5Q793,640 760,640L200,640Q167,640 143.5,616.5Q120,593 120,560L120,400L440,400L440,316Q354,302 297,236Q240,170 240,80Q340,80 410,150Q480,220 480,320Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M280,680L280,280L680,280L680,680L280,680ZM360,600L600,600L600,360L360,360L360,600ZM200,760L200,840Q167,840 143.5,816.5Q120,793 120,760L200,760ZM120,680L120,600L200,600L200,680L120,680ZM120,520L120,440L200,440L200,520L120,520ZM120,360L120,280L200,280L200,360L120,360ZM200,200L120,200Q120,167 143.5,143.5Q167,120 200,120L200,200ZM280,840L280,760L360,760L360,840L280,840ZM280,200L280,120L360,120L360,200L280,200ZM440,840L440,760L520,760L520,840L440,840ZM440,200L440,120L520,120L520,200L440,200ZM600,840L600,760L680,760L680,840L600,840ZM600,200L600,120L680,120L680,200L600,200ZM760,840L760,760L840,760Q840,793 816.5,816.5Q793,840 760,840ZM760,680L760,600L840,600L840,680L760,680ZM760,520L760,440L840,440L840,520L760,520ZM760,360L760,280L840,280L840,360L760,360ZM760,200L760,120Q793,120 816.5,143.5Q840,167 840,200L760,200Z"/>
</vector>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_nav_live_selected" android:state_checked="true" />
<item android:drawable="@drawable/ic_nav_live" android:state_checked="false" />
</selector>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_nav_setting_selected" android:state_checked="true" />
<item android:drawable="@drawable/ic_nav_setting" android:state_checked="false" />
</selector>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_nav_vod_selected" android:state_checked="true" />
<item android:drawable="@drawable/ic_nav_vod" android:state_checked="false" />
</selector>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/colorControlHighlight">
<item android:id="@android:id/background">
<shape android:shape="rectangle">
<solid android:color="@color/primary" />
<corners android:radius="8dp" />
<padding
android:bottom="14dp"
android:left="12dp"
android:right="12dp"
android:top="14dp" />
</shape>
</item>
</ripple>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="14dp" />
<solid android:color="#212121" />
</shape>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/colorControlHighlight">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<corners android:radius="8dp" />
<solid android:color="#f5f5f5" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@color/black_20" />
<corners android:radius="8dp" />
<padding
android:bottom="8dp"
android:left="12dp"
android:right="12dp"
android:top="8dp" />
</shape>
</item>
</ripple>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/colorControlHighlight">
<item android:id="@android:id/background">
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent" />
</shape>
</item>
</ripple>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#802196F3">
android:color="#80FFEB3B">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<solid android:color="#f5f5f5" />
@@ -3,7 +3,7 @@
android:color="?attr/colorControlHighlight">
<item android:id="@android:id/background">
<shape android:shape="rectangle">
<solid android:color="@color/yellow_500" />
<solid android:color="@color/primary" />
<corners android:radius="8dp" />
<padding
android:bottom="10dp"
@@ -3,7 +3,7 @@
android:color="?attr/colorControlHighlight">
<item android:id="@android:id/background">
<shape android:shape="rectangle">
<solid android:color="@color/yellow_500" />
<solid android:color="@color/primary" />
<corners android:radius="8dp" />
<padding
android:bottom="18dp"
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#8066BB6A">
android:color="#80FFEB3B">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<solid android:color="#f5f5f5" />
+1 -1
View File
@@ -4,7 +4,7 @@
<item android:id="@android:id/background">
<shape android:shape="rectangle">
<solid android:color="@color/black_20" />
<corners android:radius="8dp" />
<corners android:radius="12dp" />
<padding
android:bottom="14dp"
android:left="16dp"
@@ -3,7 +3,7 @@
android:color="?attr/colorControlHighlight">
<item android:id="@android:id/background">
<shape android:shape="rectangle">
<solid android:color="@color/yellow_500" />
<solid android:color="@color/primary" />
<corners android:radius="8dp" />
<padding
android:bottom="18dp"
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="14dp" />
<stroke
android:width="1dp"
android:color="@color/white_10" />
</shape>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/colorControlHighlight">
<item android:id="@android:id/background">
<shape android:shape="rectangle">
<solid android:color="@color/primary" />
<corners android:radius="8dp" />
<padding
android:bottom="12dp"
android:left="16dp"
android:right="16dp"
android:top="12dp" />
</shape>
</item>
</ripple>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/colorControlHighlight">
<item android:id="@android:id/background">
<shape android:shape="rectangle">
<solid android:color="@color/black_20" />
<corners android:radius="8dp" />
<padding
android:bottom="12dp"
android:left="16dp"
android:right="16dp"
android:top="12dp" />
</shape>
</item>
</ripple>
@@ -0,0 +1,258 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black_20">
<LinearLayout
android:id="@+id/top"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingStart="8dp"
android:paddingTop="8dp"
android:paddingEnd="8dp">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:singleLine="true"
android:textColor="@color/white"
android:textSize="14sp"
tools:text="慶餘年第一季:第一集" />
<TextView
android:id="@+id/size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="12sp"
tools:text="1920 x 1080" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/time_battery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="21:30" />
<ImageView
android:id="@+id/charging_indicator"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:layout_marginEnd="4dp"
android:src="@drawable/ic_charging_bolt"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/battery_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingEnd="8dp"
android:textColor="@color/white"
android:textSize="14sp"
android:visibility="gone"
tools:text="85%"
tools:visibility="visible" />
<ImageView
android:id="@+id/cast"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="8dp"
android:src="@drawable/ic_control_cast" />
<ImageView
android:id="@+id/info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="8dp"
android:src="@drawable/ic_control_info" />
<ImageView
android:id="@+id/keep"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="8dp"
android:src="@drawable/ic_control_keep_off" />
<ImageView
android:id="@+id/setting"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="8dp"
android:src="@drawable/ic_control_setting" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:orientation="horizontal">
<FrameLayout
android:id="@+id/prevRoot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_control"
android:visibility="gone"
tools:visibility="visible">
<ImageView
android:id="@+id/prev"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/exo_icon_previous" />
</FrameLayout>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="48dp"
android:layout_marginEnd="48dp"
android:background="@drawable/shape_control">
<ImageView
android:id="@+id/play"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/exo_icon_play" />
</FrameLayout>
<FrameLayout
android:id="@+id/nextRoot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_control"
android:visibility="gone"
tools:visibility="visible">
<ImageView
android:id="@+id/next"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/exo_icon_next" />
</FrameLayout>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/danmaku"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="8dp"
android:src="@drawable/ic_control_danmaku_on" />
</LinearLayout>
<include
android:id="@+id/right"
layout="@layout/view_control_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="8dp" />
<LinearLayout
android:id="@+id/bottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="8dp"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/parse"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="horizontal"
android:padding="8dp"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="5"
tools:listitem="@layout/adapter_parse_dark" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<com.fongmi.android.tv.ui.custom.CustomSeekView
android:id="@+id/seek"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<ImageView
android:id="@+id/full"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_control_full" />
</LinearLayout>
<include
android:id="@+id/action"
layout="@layout/view_control_vod_action"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</RelativeLayout>
@@ -154,5 +154,15 @@
app:spanCount="2"
tools:listitem="@layout/adapter_vod_rect" />
<!-- 搜索结果空状态Lottie动画 -->
<include
android:id="@+id/emptyLayout"
layout="@layout/view_empty_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginTop="-40dp"
android:visibility="gone" />
</RelativeLayout>
</LinearLayout>
+13 -3
View File
@@ -18,7 +18,7 @@
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:background="@drawable/shape_action_background"
android:src="@drawable/ic_back" />
<TextView
@@ -35,7 +35,7 @@
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:background="@drawable/shape_action_background"
android:src="@drawable/ic_action_sync" />
<ImageView
@@ -43,7 +43,7 @@
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:background="@drawable/shape_action_background"
android:src="@drawable/ic_action_delete"
android:visibility="gone"
tools:visibility="visible" />
@@ -65,5 +65,15 @@
android:paddingEnd="8dp"
android:paddingBottom="8dp" />
<!-- 空状态Lottie动画 -->
<include
android:id="@+id/emptyLayout"
layout="@layout/view_empty_lottie"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="-40dp"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>
+2 -2
View File
@@ -14,14 +14,14 @@
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/navigation"
android:layout_width="match_parent"
android:layout_height="68dp"
android:layout_height="70dp"
android:layout_alignParentBottom="true"
android:background="@color/transparent"
app:elevation="0dp"
app:itemIconSize="24dp"
app:itemIconTint="@color/nav"
app:itemTextColor="@color/nav"
app:labelVisibilityMode="labeled"
app:labelVisibilityMode="unlabeled"
app:itemActiveIndicatorStyle="@style/Indicator"
app:menu="@menu/menu_nav" />
+12 -3
View File
@@ -18,7 +18,7 @@
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:background="@drawable/shape_action_background"
android:src="@drawable/ic_back" />
<TextView
@@ -35,7 +35,7 @@
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:background="@drawable/shape_action_background"
android:src="@drawable/ic_action_sync" />
<ImageView
@@ -43,7 +43,7 @@
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:background="@drawable/shape_action_background"
android:src="@drawable/ic_action_delete"
android:visibility="gone"
tools:visibility="visible" />
@@ -65,5 +65,14 @@
android:paddingEnd="8dp"
android:paddingBottom="8dp" />
<include
android:id="@+id/emptyLayout"
layout="@layout/view_empty_keep"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="-40dp"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>
@@ -0,0 +1,367 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/back"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:background="@drawable/shape_action_background"
android:src="@drawable/ic_back" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/setting_player"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold" />
</LinearLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:overScrollMode="never">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:orientation="vertical"
android:paddingStart="24dp"
android:paddingTop="8dp"
android:paddingEnd="24dp"
android:paddingBottom="16dp">
<LinearLayout
android:id="@+id/render"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_item"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/player_render"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/renderText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="Surface" />
</LinearLayout>
<LinearLayout
android:id="@+id/scale"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/shape_item"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/player_scale"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/scaleText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="預設" />
</LinearLayout>
<LinearLayout
android:id="@+id/caption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/shape_item"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/player_caption"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/captionText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:gravity="end"
android:singleLine="true"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="預設" />
</LinearLayout>
<LinearLayout
android:id="@+id/buffer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/shape_item"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/player_buffer"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/bufferText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="@string/times"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/speed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/shape_item"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/player_speed"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/speedText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="@string/times"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/tunnel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/shape_item"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/player_tunnel"
android:textColor="@color/white"
android:textSize="16sp" />
<com.fongmi.android.tv.ui.custom.CustomSwitch
android:id="@+id/tunnelSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:id="@+id/audioDecode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/shape_item"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/player_audio_decode"
android:textColor="@color/white"
android:textSize="16sp" />
<com.fongmi.android.tv.ui.custom.CustomSwitch
android:id="@+id/audioDecodeSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:id="@+id/aac"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/shape_item"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/player_aac"
android:textColor="@color/white"
android:textSize="16sp" />
<com.fongmi.android.tv.ui.custom.CustomSwitch
android:id="@+id/aacSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:id="@+id/danmakuLoad"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/shape_item"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/player_danmaku_load"
android:textColor="@color/white"
android:textSize="16sp" />
<com.fongmi.android.tv.ui.custom.CustomSwitch
android:id="@+id/danmakuLoadSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:id="@+id/background"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/shape_item"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/player_background"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/backgroundText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="畫中畫" />
</LinearLayout>
<LinearLayout
android:id="@+id/ua"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/shape_item"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/player_ua"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/uaText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:gravity="end"
android:singleLine="true"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="okhttp/4.11.0" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
@@ -4,11 +4,14 @@
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:layout_marginStart="6dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="6dp"
android:layout_marginBottom="8dp"
android:background="@drawable/shape_item"
android:gravity="center"
android:padding="8dp"
android:singleLine="true"
android:textColor="@color/text"
android:textSize="14sp"
android:textSize="12sp"
tools:text="泥巴" />
+29 -9
View File
@@ -6,17 +6,37 @@
android:gravity="center_vertical"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/text"
style="@style/Widget.App.Button.OutlinedButton.SiteDialog"
<FrameLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="middle"
android:singleLine="true"
android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp"
tools:text="https://fongmi.github.io/cat.json" />
android:layout_weight="1">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_site_dialog"
android:gravity="center"
android:ellipsize="middle"
android:singleLine="true"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="14dp"
android:paddingBottom="14dp"
android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp"
tools:text="https://fongmi.github.io/cat.json" />
</FrameLayout>
<ImageView
android:id="@+id/copy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitCenter"
android:src="@drawable/ic_setting_copy" />
<ImageView
android:id="@+id/delete"
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="关于"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="开发说明"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="本项目仅用于学习Android开发,代码改自FongMi/TV (https://github.com/FongMi/TV)。"
android:textColor="@color/white"
android:textSize="14sp"
android:layout_marginTop="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="免责声明"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="1.本项目仅供学习交流使用,不得用于商业用途\n2.项目中的内容均来自网络,如有侵权请联系删除\n3.使用本项目产生的一切后果由使用者自行承担"
android:textColor="@color/white"
android:textSize="14sp"
android:layout_marginTop="8dp" />
<Button
android:id="@+id/github"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:background="@drawable/shape_about_button"
android:text="我的GitHub"
android:textColor="@color/black"
android:textSize="14sp"
android:padding="12dp"/>
</LinearLayout>
+4 -3
View File
@@ -15,9 +15,10 @@
android:stepSize="1"
android:valueFrom="1"
android:valueTo="10"
app:thumbColor="@color/yellow_500"
app:thumbColor="@color/accent"
app:thumbRadius="9dp"
app:tickVisible="false"
app:trackColorActive="@color/yellow_500"
app:trackColorInactive="@color/yellow_50" />
app:trackColorActive="@color/accent"
app:trackColorInactive="@color/white_30" />
</LinearLayout>
+27 -8
View File
@@ -8,12 +8,30 @@
android:padding="16dp"
tools:background="@color/white">
<TextView
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/control_speed"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp" />
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/control_speed"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp" />
<TextView
android:id="@+id/speedValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1.0x"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<com.google.android.material.slider.Slider
android:id="@+id/speed"
@@ -24,10 +42,11 @@
android:stepSize="0.25"
android:valueFrom="0.5"
android:valueTo="5"
app:thumbColor="@color/yellow_500"
app:thumbColor="@color/accent"
app:thumbRadius="9dp"
app:tickVisible="false"
app:trackColorActive="@color/yellow_500"
app:trackColorInactive="@color/yellow_50" />
app:trackColorActive="@color/accent"
app:trackColorInactive="@color/white_30" />
<TextView
android:id="@+id/parseText"
+3 -1
View File
@@ -15,8 +15,10 @@
android:stepSize="0.5"
android:valueFrom="2"
android:valueTo="5"
app:thumbColor="@color/accent"
app:thumbRadius="9dp"
app:tickVisible="false"
app:trackColorActive="@color/accent"
app:trackColorInactive="@color/yellow_50" />
app:trackColorInactive="@color/white_30" />
</LinearLayout>
+64 -18
View File
@@ -29,50 +29,80 @@
<TextView
android:id="@+id/time1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="56dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:background="@drawable/shape_accent"
android:background="@drawable/shape_accent_no_border"
android:tag="5"
android:text="@string/timer_5"
android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp" />
android:textSize="14sp"
android:gravity="center" />
<TextView
android:id="@+id/time2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="56dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:background="@drawable/shape_accent"
android:background="@drawable/shape_accent_no_border"
android:tag="15"
android:text="@string/timer_15"
android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp" />
android:textSize="14sp"
android:gravity="center" />
<TextView
android:id="@+id/time3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="56dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:background="@drawable/shape_accent"
android:background="@drawable/shape_accent_no_border"
android:tag="30"
android:text="@string/timer_30"
android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp" />
android:textSize="14sp"
android:gravity="center" />
<TextView
android:id="@+id/time4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="56dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:background="@drawable/shape_accent"
android:background="@drawable/shape_accent_no_border"
android:tag="60"
android:text="@string/timer_60"
android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp" />
android:textSize="14sp"
android:gravity="center" />
<TextView
android:id="@+id/time5"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:background="@drawable/shape_accent_no_border"
android:tag="120"
android:text="@string/timer_120"
android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp"
android:gravity="center" />
<TextView
android:id="@+id/time6"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:background="@drawable/shape_accent_no_border"
android:tag="180"
android:text="@string/timer_180"
android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp"
android:gravity="center" />
</LinearLayout>
@@ -96,25 +126,41 @@
android:textStyle="bold"
tools:text="5:00" />
<com.google.android.material.button.MaterialButton
<TextView
android:id="@+id/delay"
style="?attr/materialButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:background="@drawable/shape_timer_delay_button"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:paddingStart="60dp"
android:paddingEnd="60dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:singleLine="true"
android:text="@string/timer_delay"
android:textColor="@color/white" />
android:textColor="@color/black"
android:textSize="14sp" />
<com.google.android.material.button.MaterialButton
<TextView
android:id="@+id/reset"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:background="@drawable/shape_timer_reset_button"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:paddingStart="60dp"
android:paddingEnd="60dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:singleLine="true"
android:text="@string/timer_cancel"
android:textColor="?android:attr/textColorPrimary" />
android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
+340 -229
View File
@@ -44,6 +44,7 @@
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:fillViewport="true"
android:overScrollMode="never"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
@@ -58,15 +59,31 @@
android:paddingBottom="24dp">
<!-- 源管理分组 -->
<TextView
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginBottom="8dp"
android:text="@string/setting_source"
android:textColor="@color/white"
android:textSize="14sp"
android:alpha="0.7" />
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/setting_source"
android:textColor="@color/white"
android:textSize="14sp"
android:alpha="0.7" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="(单击视频输入框可加源)"
android:textColor="@color/white"
android:textSize="12sp"
android:alpha="0.5" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
@@ -77,10 +94,11 @@
<LinearLayout
android:id="@+id/vod"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_height="56dp"
android:layout_marginEnd="12dp"
android:layout_weight="1"
android:background="@drawable/shape_item"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
@@ -112,7 +130,8 @@
android:background="@drawable/shape_item"
android:padding="16dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_setting_home" />
android:src="@drawable/potted_plant_24px"
android:tint="@color/white" />
<ImageView
android:id="@+id/vodHistory"
@@ -121,11 +140,13 @@
android:background="@drawable/shape_item"
android:padding="16dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_setting_history" />
android:src="@drawable/ic_m3_list_alt"
android:tint="@color/white" />
</LinearLayout>
<LinearLayout
android:id="@+id/liveContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
@@ -135,10 +156,11 @@
<LinearLayout
android:id="@+id/live"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_height="56dp"
android:layout_marginEnd="12dp"
android:layout_weight="1"
android:background="@drawable/shape_item"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
@@ -170,7 +192,8 @@
android:background="@drawable/shape_item"
android:padding="16dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_setting_home" />
android:src="@drawable/potted_plant_24px"
android:tint="@color/white" />
<ImageView
android:id="@+id/liveHistory"
@@ -179,67 +202,11 @@
android:background="@drawable/shape_item"
android:padding="16dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_setting_history" />
android:src="@drawable/ic_m3_list_alt"
android:tint="@color/white" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/wall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:layout_weight="1"
android:background="@drawable/shape_item"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/setting_wall"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/wallUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:gravity="end"
android:singleLine="true"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="https://" />
</LinearLayout>
<ImageView
android:id="@+id/wallDefault"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginEnd="12dp"
android:background="@drawable/shape_item"
android:padding="16dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_setting_switch" />
<ImageView
android:id="@+id/wallRefresh"
android:layout_width="56dp"
android:layout_height="56dp"
android:background="@drawable/shape_item"
android:padding="16dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_setting_refresh" />
</LinearLayout>
<!-- 应用设置分组 -->
<TextView
@@ -253,74 +220,136 @@
android:textSize="14sp"
android:alpha="0.7" />
<!-- 将三个设置项放在一个共同的背景容器中 -->
<LinearLayout
android:id="@+id/player"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_item"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/setting_player"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/incognito"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:background="@drawable/shape_item"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/setting_incognito"
android:textColor="@color/white"
android:textSize="16sp" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/incognitoSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/M3SwitchStyle" />
</LinearLayout>
<LinearLayout
android:id="@+id/size"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:background="@drawable/shape_item"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/setting_size"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/sizeText"
android:orientation="vertical">
<!-- 播放器设置 -->
<LinearLayout
android:id="@+id/player"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="Medium" />
android:orientation="horizontal"
android:paddingTop="16dp"
android:paddingBottom="12dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:src="@drawable/hive_24px"
android:tint="@color/white" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/setting_player"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
<!-- 无痕模式 -->
<LinearLayout
android:id="@+id/incognito"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:src="@drawable/domino_mask_24px"
android:tint="@color/white" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/setting_incognito"
android:textColor="@color/white"
android:textSize="16sp" />
<com.fongmi.android.tv.ui.custom.CustomSwitch
android:id="@+id/incognitoSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<!-- 显示直播 -->
<LinearLayout
android:id="@+id/liveTabVisible"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_nav_live"
android:tint="@color/white" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/setting_live_tab_visible"
android:textColor="@color/white"
android:textSize="16sp" />
<com.fongmi.android.tv.ui.custom.CustomSwitch
android:id="@+id/liveTabVisibleSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<!-- 图片尺寸 -->
<LinearLayout
android:id="@+id/size"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="12dp"
android:paddingBottom="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:src="@drawable/select_all_24px"
android:tint="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/setting_size"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/sizeText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="Medium" />
</LinearLayout>
</LinearLayout>
<!-- 网络设置分组 -->
@@ -335,84 +364,117 @@
android:textSize="14sp"
android:alpha="0.7" />
<!-- 将三个网络设置项放在一个共同的背景容器中 -->
<LinearLayout
android:id="@+id/doh"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_item"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/setting_doh"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/dohText"
android:orientation="vertical">
<!-- DoH设置 -->
<LinearLayout
android:id="@+id/doh"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="Google" />
android:orientation="horizontal"
android:paddingTop="16dp"
android:paddingBottom="16dp">
</LinearLayout>
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:src="@drawable/globe_book_24px"
android:tint="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/setting_doh"
android:textColor="@color/white"
android:textSize="16sp" />
<LinearLayout
android:id="@+id/proxy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:background="@drawable/shape_item"
android:orientation="horizontal">
<TextView
android:id="@+id/dohText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="Google" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/setting_proxy"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/proxyText"
</LinearLayout>
<!-- 代理设置 -->
<LinearLayout
android:id="@+id/proxy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="http" />
android:orientation="horizontal"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_fab_link"
android:tint="@color/white" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/setting_proxy"
android:textColor="@color/white"
android:textSize="16sp" />
<LinearLayout
android:id="@+id/cache"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:background="@drawable/shape_item"
android:orientation="horizontal">
<TextView
android:id="@+id/proxyText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="http" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/setting_cache"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/cacheText"
</LinearLayout>
<!-- 缓存设置 -->
<LinearLayout
android:id="@+id/cache"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="1.0 MB" />
android:orientation="horizontal"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:src="@drawable/owl_24px"
android:tint="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/setting_cache"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/cacheText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="1.0 MB" />
</LinearLayout>
</LinearLayout>
<!-- 备份与恢复分组 -->
@@ -434,62 +496,111 @@
android:orientation="horizontal"
android:padding="0dp">
<TextView
android:id="@+id/backup"
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="?android:attr/selectableItemBackground"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp"
android:text="@string/setting_backup"
android:textColor="@color/white"
android:textSize="16sp" />
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:src="@drawable/photo_prints_24px"
android:tint="@color/white" />
<TextView
android:id="@+id/backup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:text="@string/setting_backup"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
<TextView
android:id="@+id/restore"
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="?android:attr/selectableItemBackground"
android:gravity="end"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp"
android:text="@string/setting_restore"
android:textColor="@color/white"
android:textSize="16sp" />
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/restore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp"
android:text="@string/setting_restore"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/version"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:background="@drawable/shape_item"
android:layout_marginTop="12dp"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
<LinearLayout
android:id="@+id/version"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/setting_version"
android:textColor="@color/white"
android:textSize="16sp" />
android:layout_weight="1"
android:layout_marginEnd="12dp"
android:background="@drawable/shape_item"
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_setting_github"
android:tint="@color/white" />
<TextView
android:id="@+id/versionText"
android:layout_width="match_parent"
<TextView
android:id="@+id/versionText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/setting_version"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="版本 3.0.3" />
</LinearLayout>
<LinearLayout
android:id="@+id/about"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="1.2.1" />
android:layout_weight="1"
android:background="@drawable/shape_item"
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:src="@drawable/egg_24px"
android:tint="@color/white" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="关于"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
@@ -55,324 +55,407 @@
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:orientation="vertical"
android:padding="16dp">
android:paddingStart="24dp"
android:paddingTop="16dp"
android:paddingEnd="24dp"
android:paddingBottom="16dp">
<!-- 播放器基本设置模块 -->
<LinearLayout
android:id="@+id/render"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_item"
android:orientation="horizontal">
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/player_render"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/renderText"
<!-- 渲染方式 -->
<LinearLayout
android:id="@+id/render"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="Surface" />
android:orientation="horizontal"
android:padding="16dp"
android:background="?attr/selectableItemBackground">
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/player_render"
android:textColor="@color/white"
android:textSize="16sp" />
<LinearLayout
android:id="@+id/scale"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/shape_item"
android:orientation="horizontal">
<TextView
android:id="@+id/renderText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="Surface" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/player_scale"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:background="#22FFFFFF" />
<TextView
android:id="@+id/scaleText"
<!-- 缩放比例 -->
<LinearLayout
android:id="@+id/scale"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="預設" />
android:orientation="horizontal"
android:padding="16dp"
android:background="?attr/selectableItemBackground">
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/player_scale"
android:textColor="@color/white"
android:textSize="16sp" />
<LinearLayout
android:id="@+id/caption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/shape_item"
android:orientation="horizontal">
<TextView
android:id="@+id/scaleText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="預設" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/player_caption"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:background="#22FFFFFF" />
<TextView
android:id="@+id/captionText"
<!-- 字幕样式 -->
<LinearLayout
android:id="@+id/caption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:gravity="end"
android:singleLine="true"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="預設" />
android:orientation="horizontal"
android:padding="16dp"
android:background="?attr/selectableItemBackground">
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/player_caption"
android:textColor="@color/white"
android:textSize="16sp" />
<LinearLayout
android:id="@+id/buffer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/shape_item"
android:orientation="horizontal">
<TextView
android:id="@+id/captionText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:gravity="end"
android:singleLine="true"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="預設" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/player_buffer"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:background="#22FFFFFF" />
<TextView
android:id="@+id/bufferText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="@string/times"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/speed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/shape_item"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/player_speed"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/speedText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="@string/times"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/tunnel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/shape_item"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/player_tunnel"
android:textColor="@color/white"
android:textSize="16sp" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/tunnelSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/M3SwitchStyle" />
</LinearLayout>
<LinearLayout
android:id="@+id/audioDecode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/shape_item"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/player_audio_decode"
android:textColor="@color/white"
android:textSize="16sp" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/audioDecodeSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/M3SwitchStyle" />
</LinearLayout>
<LinearLayout
android:id="@+id/aac"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/shape_item"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/player_aac"
android:textColor="@color/white"
android:textSize="16sp" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/aacSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/M3SwitchStyle" />
</LinearLayout>
<LinearLayout
android:id="@+id/danmakuLoad"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/shape_item"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/player_danmaku_load"
android:textColor="@color/white"
android:textSize="16sp" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/danmakuLoadSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/M3SwitchStyle" />
</LinearLayout>
<LinearLayout
android:id="@+id/background"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/shape_item"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/player_background"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/backgroundText"
<!-- 缓冲时间 -->
<LinearLayout
android:id="@+id/buffer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="畫中畫" />
android:orientation="horizontal"
android:padding="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/player_buffer"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/bufferText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="@string/times"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:background="#22FFFFFF" />
<!-- 长按倍速 -->
<LinearLayout
android:id="@+id/speed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/player_speed"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/speedText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="@string/times"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:background="#22FFFFFF" />
<!-- 后台播放 -->
<LinearLayout
android:id="@+id/background"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/player_background"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/backgroundText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="畫中畫" />
</LinearLayout>
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:background="#22FFFFFF" />
<!-- User-Agent -->
<LinearLayout
android:id="@+id/ua"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/player_ua"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/uaText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:gravity="end"
android:singleLine="true"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="okhttp/4.11.0" />
</LinearLayout>
</LinearLayout>
<!-- 开关设置模块 -->
<LinearLayout
android:id="@+id/ua"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/shape_item"
android:orientation="horizontal">
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/player_ua"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/uaText"
<!-- 隧道模式 -->
<LinearLayout
android:id="@+id/tunnel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:gravity="end"
android:singleLine="true"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="okhttp/4.11.0" />
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/player_tunnel"
android:textColor="@color/white"
android:textSize="16sp" />
<com.fongmi.android.tv.ui.custom.CustomSwitch
android:id="@+id/tunnelSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:background="#22FFFFFF" />
<!-- 音频软解 -->
<LinearLayout
android:id="@+id/audioDecode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/player_audio_decode"
android:textColor="@color/white"
android:textSize="16sp" />
<com.fongmi.android.tv.ui.custom.CustomSwitch
android:id="@+id/audioDecodeSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:background="#22FFFFFF" />
<!-- AAC优化 -->
<LinearLayout
android:id="@+id/aac"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/player_aac"
android:textColor="@color/white"
android:textSize="16sp" />
<com.fongmi.android.tv.ui.custom.CustomSwitch
android:id="@+id/aacSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:background="#22FFFFFF" />
<!-- 弹幕加载 -->
<LinearLayout
android:id="@+id/danmakuLoad"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/player_danmaku_load"
android:textColor="@color/white"
android:textSize="16sp" />
<com.fongmi.android.tv.ui.custom.CustomSwitch
android:id="@+id/danmakuLoadSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
+43 -4
View File
@@ -28,7 +28,7 @@
android:id="@+id/logo"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:background="@drawable/shape_action_background"
android:src="@drawable/ic_logo" />
<LinearLayout
@@ -66,14 +66,14 @@
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="12dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:background="@drawable/shape_action_background"
android:src="@drawable/ic_action_keep" />
<ImageView
android:id="@+id/history"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:background="@drawable/shape_action_background"
android:src="@drawable/ic_action_history" />
</LinearLayout>
@@ -100,12 +100,51 @@
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<!-- 新增空源提示文本 -->
<LinearLayout
android:id="@+id/empty_source_hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp"
android:visibility="gone"
tools:visibility="visible">
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottieAnimation"
android:layout_width="180dp"
android:layout_height="180dp"
app:lottie_fileName="lottie_empty_1.json"
app:lottie_loop="true"
app:lottie_autoPlay="true" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/source_hint"
android:textColor="@color/white"
android:textSize="14sp" />
<Button
android:id="@+id/add_source_btn"
android:layout_width="200dp"
android:layout_height="48dp"
android:layout_marginTop="16dp"
android:backgroundTint="@color/primary"
android:text="@string/add_source"
android:textColor="@color/black"
android:textSize="16sp" />
</LinearLayout>
<ImageView
android:id="@+id/retry"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_gravity="center"
android:background="?attr/selectableItemBackgroundBorderless"
android:background="@drawable/shape_action_background"
android:src="@drawable/ic_action_retry"
android:visibility="gone" />
@@ -23,6 +23,8 @@
android:layout_marginEnd="8dp"
android:layout_weight="1"
app:bar_height="2dp"
app:scrubber_enabled_size="14dp"
app:scrubber_disabled_size="14dp"
app:played_color="#FFEB3B"
app:scrubber_color="#FFEB3B"
app:buffered_color="#80FFEB3B" />
@@ -47,6 +47,40 @@
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/time_battery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="21:30" />
<ImageView
android:id="@+id/charging_indicator"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:layout_marginEnd="2dp"
android:src="@drawable/ic_charging_bolt"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/battery_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingEnd="8dp"
android:textColor="@color/white"
android:textSize="16sp"
android:visibility="gone"
tools:text="85%"
tools:visibility="visible" />
<ImageView
android:id="@+id/cast"
android:layout_width="wrap_content"
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/ic_empty" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textColor="@color/white"
android:textSize="16sp"
android:text="@string/error_no_live" />
</LinearLayout>
+3 -1
View File
@@ -43,7 +43,9 @@
<string name="timer_15">15 分钟</string>
<string name="timer_30">30 分钟</string>
<string name="timer_60">1 小时</string>
<string name="timer_delay">延长 5 分钟</string>
<string name="timer_120">2 小时</string>
<string name="timer_180">3 小时</string>
<string name="timer_delay">延长 15 分钟</string>
<string name="timer_cancel">取消定时器</string>
<!-- Hint -->
+3 -1
View File
@@ -41,7 +41,9 @@
<string name="timer_15">15 分鐘</string>
<string name="timer_30">30 分鐘</string>
<string name="timer_60">1 小時</string>
<string name="timer_delay">延長 5 分鐘</string>
<string name="timer_120">2 小時</string>
<string name="timer_180">3 小時</string>
<string name="timer_delay">延長 15 分鐘</string>
<string name="timer_cancel">取消定時器</string>
<!-- Hint -->
+3
View File
@@ -5,5 +5,8 @@
<color name="accent">#FFEB3B</color>
<color name="indicator">@color/white_80</color>
<color name="yellow_50">#4DFFEB3B</color>
<color name="white_50">#80FFFFFF</color>
<color name="black_50">#80000000</color>
<color name="white_30">#4DFFFFFF</color>
</resources>
+3 -1
View File
@@ -43,7 +43,9 @@
<string name="timer_15">15 minutes</string>
<string name="timer_30">30 minutes</string>
<string name="timer_60">1 hour</string>
<string name="timer_delay">Add 5 minutes</string>
<string name="timer_120">2 hours</string>
<string name="timer_180">3 hours</string>
<string name="timer_delay">Add 15 minutes</string>
<string name="timer_cancel">Cancel timer</string>
<!-- Hint -->
+16 -18
View File
@@ -13,10 +13,11 @@
<item name="android:windowTranslucentStatus">false</item>
</style>
<style name="BaseTheme" parent="Theme.Material3.DayNight.NoActionBar">
<style name="BaseTheme" parent="Theme.Material3.Dark.NoActionBar">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primaryDark</item>
<item name="colorAccent">@color/accent</item>
<item name="colorControlHighlight">@color/white_20</item>
<item name="android:windowBackground">@null</item>
<item name="android:windowDisablePreview">true</item>
<item name="android:navigationBarColor">@color/transparent</item>
@@ -30,17 +31,13 @@
<style name="Control.Action">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:background">?attr/selectableItemBackgroundBorderless</item>
<item name="android:background">@drawable/shape_action_background</item>
<item name="android:padding">8dp</item>
<item name="android:shadowColor">@color/grey_200</item>
<item name="android:shadowDx">1</item>
<item name="android:shadowDy">1</item>
<item name="android:shadowRadius">0.5</item>
<item name="android:textColor">@color/white</item>
<item name="android:textSize">14sp</item>
</style>
<style name="BottomSheetDialog" parent="Theme.Material3.DayNight.BottomSheetDialog">
<style name="BottomSheetDialog" parent="Theme.Material3.Dark.BottomSheetDialog">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primaryDark</item>
<item name="colorAccent">@color/accent</item>
@@ -60,7 +57,8 @@
</style>
<style name="Indicator" parent="Widget.Material3.BottomNavigationView.ActiveIndicator">
<item name="android:color">@color/indicator</item>
<item name="android:color">@color/primary</item>
<item name="android:height">42dp</item>
</style>
<style name="BottomNavigationView.TextAppearance" parent="TextAppearance.AppCompat">
@@ -69,20 +67,20 @@
</style>
<style name="BottomNavigation" parent="Widget.Material3.BottomNavigationView">
<item name="itemPaddingTop">8dp</item>
<item name="itemPaddingBottom">8dp</item>
<item name="itemPaddingTop">4dp</item>
<item name="itemPaddingBottom">4dp</item>
<item name="itemTextAppearanceActive">@style/BottomNavigationView.TextAppearance</item>
<item name="itemTextAppearanceInactive">@style/BottomNavigationView.TextAppearance</item>
<item name="itemRippleColor">@android:color/transparent</item>
<item name="android:background">@null</item>
</style>
<!-- M3 Switch Style -->
<style name="M3SwitchStyle" parent="Widget.AppCompat.CompoundButton.Switch">
<item name="android:thumb">@drawable/m3_switch_thumb</item>
<item name="track">@drawable/m3_switch_track</item>
<item name="android:switchMinWidth">52dp</item>
<item name="android:switchPadding">8dp</item>
<item name="android:colorControlActivated">#FFEB3B</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
<!-- M3 Switch Style (Material Design) - 使用CustomSwitch完全自定义 -->
<style name="M3SwitchStyle" parent="Widget.AppCompat.CompoundButton.CheckBox">
<item name="android:button">@null</item>
<item name="android:background">@drawable/custom_switch_bg</item>
<item name="android:minHeight">30dp</item>
<item name="android:minWidth">50dp</item>
</style>
<!-- 自定义数据源按钮样式 -->
+5 -5
View File
@@ -1,7 +1,7 @@
plugins {
id 'com.android.application' version '8.8.2' apply false
id 'com.android.library' version '8.8.2' apply false
id 'com.chaquo.python' version '15.0.1' apply false
id 'com.android.application' version '8.12.0' apply false
id 'com.android.library' version '8.12.0' apply false
id 'com.chaquo.python' version '16.1.0' apply false
}
tasks.register('clean', Delete) {
@@ -10,6 +10,6 @@ tasks.register('clean', Delete) {
project.ext {
gsonVersion = '2.11.0'
media3Version = '1.6.1'
okhttpVersion = '5.0.0-alpha.14'
media3Version = '1.4.1'
okhttpVersion = '4.12.0'
}
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
echo "=== 构建v8a手机测试版 ==="
# 清理之前的构建
echo "清理之前的构建..."
./gradlew clean
# 构建mobile arm64-v8a debug版本
echo "构建mobile arm64-v8a debug版本..."
./gradlew assembleMobileArm64_v8aDebug
# 检查构建结果
if [ $? -eq 0 ]; then
echo "=== 构建成功 ==="
# 查找生成的APK文件
APK_PATH=$(find app/build/outputs/apk/mobile/arm64-v8a/debug -name "*.apk" 2>/dev/null | head -1)
if [ -n "$APK_PATH" ]; then
echo "APK文件位置: $APK_PATH"
echo "文件大小: $(ls -lh "$APK_PATH" | awk '{print $5}')"
echo "文件信息:"
ls -la "$APK_PATH"
# 显示APK详细信息
echo ""
echo "=== APK详细信息 ==="
aapt dump badging "$APK_PATH" | grep -E "(package|application-label|native-code|sdkVersion|targetSdkVersion)"
else
echo "未找到生成的APK文件"
find app/build/outputs -name "*.apk" 2>/dev/null
fi
else
echo "=== 构建失败 ==="
echo "请检查错误信息"
fi
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
echo "========================================="
echo " 构建 XMBOX Mobile ARM64-V8A Release "
echo "========================================="
echo ""
cd /Users/chen/Desktop/XMBOX
echo "=== 1. 清理旧的构建文件 ==="
./gradlew clean
echo ""
echo "=== 2. 构建 Release APK ==="
./gradlew assembleMobileArm64_v8aRelease
echo ""
echo "=== 3. 验证构建结果 ==="
if [ -f "app/build/outputs/apk/mobileArm64_v8a/release/mobile-arm64_v8a.apk" ]; then
echo "✅ Release APK 构建成功!"
echo ""
echo "文件信息:"
ls -lh app/build/outputs/apk/mobileArm64_v8a/release/mobile-arm64_v8a.apk
echo ""
echo "APK详细信息:"
echo "---"
unzip -l app/build/outputs/apk/mobileArm64_v8a/release/mobile-arm64_v8a.apk | grep "assets/jar"
echo ""
echo "签名信息:"
jarsigner -verify -verbose -certs app/build/outputs/apk/mobileArm64_v8a/release/mobile-arm64_v8a.apk | grep -A 3 "Signed by"
echo ""
echo "=== 构建完成! ==="
echo "APK路径: app/build/outputs/apk/mobileArm64_v8a/release/mobile-arm64_v8a.apk"
else
echo "❌ 构建失败!"
exit 1
fi
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
cd /Users/chen/Desktop/XMBOX
./gradlew clean assembleMobileArm64_v8aDebug
+7 -5
View File
@@ -5,21 +5,23 @@ plugins {
android {
namespace 'com.github.catvod.crawler'
compileSdk 35
compileSdk {
version = release(36)
}
defaultConfig {
minSdk 21
minSdk 24
targetSdk 28
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
dependencies {
api 'androidx.annotation:annotation:1.6.0'
api 'androidx.annotation:annotation:1.9.1'
api 'androidx.preference:preference:1.2.1'
api 'com.google.code.gson:gson:' + gsonVersion
api 'com.google.net.cronet:cronet-okhttp:0.1.0'
@@ -1,18 +1,124 @@
package com.github.catvod.utils;
import android.os.SystemClock;
import com.github.catvod.net.OkHttp;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class Github {
public static final String URL = "https://github.com/Tosencen/XMBOX";
public static final String URL = "https://raw.githubusercontent.com/Tosencen/XMBOX-Release/main";
// 国内镜像地址 - 使用Gitee作为镜像
public static final String CN_URL = "https://gitee.com/ochenoktochen/XMBOX-Release/raw/main";
// 存储测速结果
private static Boolean useCnMirror = null;
private static long lastCheckTime = 0;
private static final long CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24小时
private static String getUrl(String path, String name) {
return URL + "/" + path + "/" + name;
}
private static String getCnUrl(String path, String name) {
return CN_URL + "/" + path + "/" + name;
}
public static String getJson(boolean dev, String name) {
return getUrl("apk/" + (dev ? "dev" : "release"), name + ".json");
if (useCnMirror()) {
return getCnUrl("apk/" + (dev ? "dev" : "release"), name + ".json");
} else {
return getUrl("apk/" + (dev ? "dev" : "release"), name + ".json");
}
}
public static String getApk(boolean dev, String name) {
return getUrl("apk/" + (dev ? "dev" : "release"), name + ".apk");
if (useCnMirror()) {
return getCnUrl("apk/" + (dev ? "dev" : "release"), name + ".apk");
} else {
return getUrl("apk/" + (dev ? "dev" : "release"), name + ".apk");
}
}
// 智能检测是否使用国内镜像
public static boolean useCnMirror() {
// 如果已经测试过并且在24小时内直接返回上次的结果
long currentTime = SystemClock.elapsedRealtime();
if (useCnMirror != null && (currentTime - lastCheckTime < CHECK_INTERVAL)) {
return useCnMirror;
}
// 进行网络测速
useCnMirror = testMirrorSpeed();
lastCheckTime = currentTime;
return useCnMirror;
}
// 测试镜像速度
private static boolean testMirrorSpeed() {
try {
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS) // 增加超时时间
.readTimeout(5, TimeUnit.SECONDS)
.build();
// 测试国际源
long startTime = System.currentTimeMillis();
boolean intlSuccess = testUrl(client, URL + "/README.md");
long intlTime = System.currentTimeMillis() - startTime;
Logger.d("Github: International mirror test - success: " + intlSuccess + ", time: " + intlTime + "ms");
// 测试国内源
startTime = System.currentTimeMillis();
boolean cnSuccess = testUrl(client, CN_URL + "/README.md");
long cnTime = System.currentTimeMillis() - startTime;
Logger.d("Github: Chinese mirror test - success: " + cnSuccess + ", time: " + cnTime + "ms");
// 如果两个都成功选择更快的
if (intlSuccess && cnSuccess) {
boolean useCn = cnTime < intlTime;
Logger.d("Github: Both mirrors work, choosing " + (useCn ? "Chinese" : "International") + " mirror");
return useCn;
}
// 如果只有一个成功选择成功的那个
if (intlSuccess) {
Logger.d("Github: Only international mirror works, using it");
return false;
}
if (cnSuccess) {
Logger.d("Github: Only Chinese mirror works, using it");
return true;
}
// 如果都失败默认国际源
Logger.e("Github: Both mirrors failed, defaulting to international");
return false;
} catch (Exception e) {
Logger.e("Github: Mirror test exception: " + e.getMessage());
return false; // 出错时默认使用国际源
}
}
private static boolean testUrl(OkHttpClient client, String url) {
Request request = new Request.Builder().url(url).build();
Call call = client.newCall(request);
try {
Response response = call.execute();
boolean success = response.isSuccessful();
response.close();
return success;
} catch (IOException e) {
return false;
}
}
}
@@ -0,0 +1,35 @@
package com.github.catvod.utils;
import android.util.Log;
public class Logger {
private static final String TAG = "XMBOX";
public static void d(String msg) {
Log.d(TAG, msg);
}
public static void e(String msg) {
Log.e(TAG, msg);
}
public static void e(String msg, Throwable tr) {
Log.e(TAG, msg, tr);
}
public static void i(String msg) {
Log.i(TAG, msg);
}
public static void v(String msg) {
Log.v(TAG, msg);
}
public static void w(String msg) {
Log.w(TAG, msg);
}
public static void w(String msg, Throwable tr) {
Log.w(TAG, msg, tr);
}
}
@@ -1,5 +1,6 @@
package com.github.catvod.utils;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.preference.PreferenceManager;
+89
View File
@@ -0,0 +1,89 @@
#!/bin/bash
# WebP颜色修改脚本
# 需要先安装 ImageMagick: brew install imagemagick
echo "=== WebP 颜色修改工具 ==="
echo ""
# 检查ImageMagick是否安装
if ! command -v convert &> /dev/null; then
echo "❌ 未检测到 ImageMagick"
echo "请先安装: brew install imagemagick"
exit 1
fi
# 示例:修改颜色(色相旋转)
# 参数说明:
# -modulate brightness,saturation,hue
# 例如:-modulate 100,100,150 (色相旋转150度)
SOURCE_DIR="/Users/chen/Desktop/XMBOX/app/src/main/res/mipmap-hdpi"
OUTPUT_DIR="/Users/chen/Desktop/XMBOX/app/src/main/res/mipmap-hdpi/modified"
# 创建输出目录
mkdir -p "$OUTPUT_DIR"
echo "处理目录: $SOURCE_DIR"
echo "输出目录: $OUTPUT_DIR"
echo ""
# 示例1: 色相旋转(改变整体颜色)
echo "方式1: 色相旋转"
echo " convert input.webp -modulate 100,100,180 output.webp # 色相旋转180度"
echo ""
# 示例2: 颜色替换
echo "方式2: 颜色替换"
echo " convert input.webp -fuzz 20% -fill '#新颜色' -opaque '#旧颜色' output.webp"
echo ""
# 示例3: 调整色调/饱和度/亮度
echo "方式3: HSL调整"
echo " convert input.webp -modulate brightness,saturation,hue output.webp"
echo " brightness: 亮度 (100=不变)"
echo " saturation: 饱和度 (100=不变, 0=灰度)"
echo " hue: 色相 (100=不变)"
echo ""
# 交互式处理
read -p "请选择处理方式 (1/2/3): " choice
case $choice in
1)
read -p "输入色相旋转角度 (0-200, 100=不变): " hue
for file in "$SOURCE_DIR"/*.webp; do
filename=$(basename "$file")
echo "处理: $filename (色相旋转 ${hue}度)"
convert "$file" -modulate 100,100,$hue "$OUTPUT_DIR/$filename"
done
;;
2)
read -p "输入要替换的颜色 (HEX, 例如 #FF0000): " old_color
read -p "输入新颜色 (HEX, 例如 #00FF00): " new_color
for file in "$SOURCE_DIR"/*.webp; do
filename=$(basename "$file")
echo "处理: $filename ($old_color -> $new_color)"
convert "$file" -fuzz 20% -fill "$new_color" -opaque "$old_color" "$OUTPUT_DIR/$filename"
done
;;
3)
read -p "亮度 (100=不变): " brightness
read -p "饱和度 (100=不变, 0=灰度): " saturation
read -p "色相 (100=不变): " hue
for file in "$SOURCE_DIR"/*.webp; do
filename=$(basename "$file")
echo "处理: $filename (亮度:$brightness 饱和度:$saturation 色相:$hue)"
convert "$file" -modulate $brightness,$saturation,$hue "$OUTPUT_DIR/$filename"
done
;;
*)
echo "无效选择"
exit 1
;;
esac
echo ""
echo "✅ 处理完成!"
echo "处理后的文件保存在: $OUTPUT_DIR"
+51
View File
@@ -0,0 +1,51 @@
plugins {
id 'com.android.library'
id 'com.chaquo.python'
}
android {
namespace 'com.fongmi.chaquo'
compileSdk {
version = release(36)
}
defaultConfig {
minSdk 24
targetSdk 28
python {
version "3.8"
pip {
install("-r", "requirements.txt")
}
}
}
flavorDimensions = ["abi"]
productFlavors {
arm64_v8a {
dimension "abi"
ndk { abiFilters "arm64-v8a" }
}
armeabi_v7a {
dimension "abi"
ndk { abiFilters "armeabi-v7a" }
}
}
sourceSets {
main {
python.srcDirs = ["src/main/python"]
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
dependencies {
implementation project(':catvod')
}
+8
View File
@@ -0,0 +1,8 @@
lxml
ujson
pyquery
requests
jsonpath
cachetools
pycryptodome
beautifulsoup4
+3
View File
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
echo "=== 检查应用日志 ==="
echo ""
echo "1. 检查custom_spider.jar是否被加载:"
echo "---"
adb logcat -d | grep -i "custom_spider" | tail -20
echo ""
echo "2. 检查JarLoader相关日志:"
echo "---"
adb logcat -d | grep "JarLoader" | tail -20
echo ""
echo "3. 检查Spider初始化日志:"
echo "---"
adb logcat -d | grep -E "Spider|Init\.init" | tail -30
echo ""
echo "4. 检查是否有错误:"
echo "---"
adb logcat -d | grep -E "Error|Exception|Failed" | grep -i "fongmi\|spider\|jar" | tail -30
echo ""
echo "5. 检查DexNative相关问题:"
echo "---"
adb logcat -d | grep -i "DexNative" | tail -20
echo ""
echo "6. 检查APK中的jar文件:"
echo "---"
echo "检查已安装APK的assets目录..."
adb shell run-as com.fongmi.android.tv ls -la /data/data/com.fongmi.android.tv/cache/jar/ 2>/dev/null || echo "无权限访问,使用pull检查..."
echo ""
echo "=== 实时监控日志(按Ctrl+C停止)==="
adb logcat -c
adb logcat | grep -E "custom_spider|Spider|JarLoader|Init|fongmi" --color=always
+18
View File
@@ -0,0 +1,18 @@
#!/bin/bash
# GitHub CLI 创建 Release 脚本
# 使用前请先运行: gh auth login
echo "创建 XMBOX v3.0.8 Release..."
gh release create v3.0.8 \
--title "XMBOX v3.0.8 - UI交互体验全面优化" \
--notes-file RELEASE_NOTES_v3.0.8.md \
--draft \
~/Desktop/mobile-arm64_v8a-v3.0.8.apk \
~/Desktop/mobile-armeabi_v7a-v3.0.8.apk \
~/Desktop/leanback-arm64_v8a-v3.0.8.apk \
~/Desktop/leanback-armeabi_v7a-v3.0.8.apk
echo "Release 创建完成(草稿状态)"
echo "请在 GitHub 上检查并发布"
+265
View File
@@ -0,0 +1,265 @@
# 广告拦截功能说明
## 📌 功能概述
XMBOX内置了强大的广告拦截系统,可以有效过滤视频播放过程中弹出的各类广告,特别是常见的赌博广告(如澳门新葡京、皇冠、金沙等)。
## 🎯 拦截类型
### 1. 赌博类广告 ⭐⭐⭐⭐⭐
**最常见的视频中途弹窗广告**
包括但不限于:
- 澳门新葡京、皇冠、金沙、威尼斯人、永利等博彩品牌
- 各类赌场、扑克、投注网站
- 使用 `xpj``wnsr``js[数字]``vn[数字]` 等域名的赌博网站
**特征**:通常在视频播放到一半时突然弹出,覆盖整个播放器
### 2. 广告联盟
- Google广告联盟(DoubleClick、AdSense等)
- 百度广告联盟
- 淘宝/阿里广告联盟
- 腾讯广告联盟
### 3. 视频平台广告
- 优酷、爱奇艺、腾讯视频等平台的贴片广告
- 芒果TV、乐视等平台广告
### 4. 弹窗广告
- 页面加载时的弹窗
- 点击播放时的诱导弹窗
### 5. 跟踪统计
- 百度统计、Google Analytics
- CNZZ、友盟等统计代码
## 🔧 使用方法
### 方式一:内置拦截(推荐)✅
**无需任何配置**,应用已内置200+常见广告域名,包括:
```java
// 已内置的赌博广告域名示例
".*\\..*葡京.*" // 拦截所有包含"葡京"的域名
".*\\..*皇冠.*" // 拦截所有包含"皇冠"的域名
".*xpj.*\\..*" // 拦截新葡京相关域名
"wan.51img1.com" // 常见广告CDN
"vip.ffzyad.com" // 视频广告域名
```
### 方式二:自定义拦截
在配置文件中添加 `ads` 字段:
```json
{
"spider": "your_spider_url",
"sites": [...],
"ads": [
"精确域名匹配",
"example-ad.com",
"another-ad-domain.net",
"通配符匹配",
".*\\.advertisement\\..*",
".*\\.ad-server\\..*",
"关键词匹配",
".*赌博.*",
".*博彩.*"
]
}
```
### 方式三:片头片尾跳过 🎬
对于**嵌入视频流中的广告**(无法通过域名拦截),使用片头片尾跳过功能:
#### 设置片头跳过
1. 播放视频
2. 在片头广告结束后(前5分钟内),点击 **【片头】** 按钮
3. 当前时间点会被记录
4. 下次播放时自动从这个时间点开始
#### 设置片尾跳过
1. 播放到片尾广告开始前(后5分钟内),点击 **【片尾】** 按钮
2. 当前时间点会被记录
3. 下次播放时会在这个时间点停止
#### 重置设置
长按 **【片头】** 或 **【片尾】** 按钮即可重置
## 📝 工作原理
### WebView层拦截
```java
// 在WebView加载资源时拦截
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String host = request.getUrl().getHost();
// 检查是否为广告域名
if (isAd(host)) {
// 返回空响应,阻止广告加载
return emptyResponse;
}
return super.shouldInterceptRequest(view, request);
}
```
### 拦截流程
```
视频请求 → 检查域名 → 内置黑名单? → 是 → 拦截 ❌
用户自定义黑名单? → 是 → 拦截 ❌
允许加载 ✅
```
## 🛠️ 高级配置
### 1. 正则表达式匹配
```json
{
"ads": [
".*\\.ad.*\\.com", // 匹配 sub.adserver.com
".*gambling.*", // 匹配任何包含gambling的域名
"^https?://[^/]*ad[^/]*/" // 匹配URL中包含ad的路径
]
}
```
### 2. 分类管理
建议按类型组织广告域名:
```json
{
"ads": [
"// === 赌博广告 ===",
".*葡京.*",
".*casino.*",
"// === 视频平台广告 ===",
"atm.youku.com",
"cupid.iqiyi.com",
"// === 弹窗广告 ===",
"mimg.0c1q0l.cn",
"www.92424.cn"
]
}
```
## 📊 效果统计
启用广告拦截后,您可以看到:
- ✅ 视频播放更流畅
- ✅ 无中途弹窗干扰
- ✅ 隐私数据不被跟踪
- ✅ 节省流量消耗
## ❓ 常见问题
### Q1: 为什么有些广告还是会出现?
**A:** 可能的原因:
1. 广告直接嵌入视频流(使用片头片尾跳过)
2. 广告使用了新的域名(可添加到自定义黑名单)
3. 广告通过JavaScript动态生成(内容层广告,较难拦截)
### Q2: 会不会误拦截正常内容?
**A:** 内置域名库经过精心筛选,主要针对已知广告域名。误拦截概率极低。如遇到问题,可以:
- 关闭内置拦截(需修改代码)
- 反馈给开发者更新黑名单
### Q3: 如何找到广告的域名?
**A:** 方法:
1. 使用Chrome浏览器开发者工具(F12)查看Network请求
2. 查看应用日志输出的URL
3. 参考其他用户分享的广告域名列表
### Q4: 拦截会影响性能吗?
**A:** 影响极小:
- 仅在WebView解析阶段生效
- 字符串匹配操作非常快速
- 反而会因为减少网络请求而提升性能
### Q5: 数据会被上传吗?
**A:** 完全不会:
- 所有拦截在本地进行
- 不联网、不上传
- 完全保护您的隐私
## 🔍 调试方法
### 查看拦截日志
启用调试模式可以看到被拦截的域名:
```bash
# 连接设备
adb logcat | grep "WebView\|AdBlocker"
# 查看拦截记录
adb logcat | grep "blocked"
```
### 测试广告拦截
1. 访问包含广告的视频网站
2. 观察是否弹出广告
3. 检查日志中的拦截记录
## 📚 域名库维护
### 添加新的广告域名
编辑 `AdBlocker.java`
```java
private static final List<String> GAMBLING_ADS = Arrays.asList(
// 添加新发现的广告域名
"new-casino-ad.com",
".*new-gambling.*"
);
```
### 提交贡献
如果您发现新的广告域名,欢迎:
1. 提交Issue报告
2. 提交Pull Request更新域名库
3. 在社区分享
## 💡 最佳实践
1. **优先使用内置拦截** - 已覆盖大部分常见广告
2. **补充自定义规则** - 针对特定视频源的广告
3. **善用片头片尾跳过** - 处理嵌入式广告
4. **定期更新** - 保持应用最新版本以获取最新域名库
## 🎉 总结
XMBOX的广告拦截系统通过**三层防护**:
1. 🛡️ **内置域名库** - 200+常见广告(含澳门新葡京等赌博广告)
2. 🛡️ **自定义黑名单** - 用户针对性添加
3. 🛡️ **片头片尾跳过** - 处理嵌入式广告
让您享受**纯净、流畅、安全**的视频观看体验!
+498
View File
@@ -0,0 +1,498 @@
#!/bin/bash
# 设置颜色输出
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}开始修复项目依赖...${NC}"
# 修复 app/build.gradle
echo -e "${YELLOW}修复 app 模块依赖...${NC}"
cat > app/build.gradle << 'EOF'
plugins {
id 'com.android.application'
}
android {
namespace 'com.fongmi.android.tv'
compileSdk 33
defaultConfig {
applicationId "com.fongmi.android.tv"
minSdk 21
targetSdk 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
buildFeatures {
viewBinding true
}
flavorDimensions "mode", "abi"
productFlavors {
mobile {
dimension "mode"
applicationIdSuffix ".mobile"
}
leanback {
dimension "mode"
applicationIdSuffix ".leanback"
}
all32 {
dimension "abi"
ndk {
abiFilters 'armeabi-v7a'
}
}
all64 {
dimension "abi"
ndk {
abiFilters 'arm64-v8a'
}
}
}
lintOptions {
checkReleaseBuilds false
abortOnError false
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.aar"])
implementation project(':catvod')
implementation project(':quickjs')
implementation project(':forcetech')
implementation project(':hook')
implementation project(':jianpian')
implementation project(':thunder')
implementation project(':tvbus')
implementation project(':zlive')
// AndroidX
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core:1.10.1'
implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.recyclerview:recyclerview:1.3.0'
// Google
implementation 'com.google.android.material:material:1.7.0'
// OkHttp
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
// Room
implementation 'androidx.room:room-runtime:2.5.2'
annotationProcessor 'androidx.room:room-compiler:2.5.2'
// Jsoup
implementation 'org.jsoup:jsoup:1.15.4'
// Coil
implementation 'io.coil-kt:coil:2.2.2'
// Other
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
}
EOF
# 修复 catvod/build.gradle
echo -e "${YELLOW}修复 catvod 模块依赖...${NC}"
cat > catvod/build.gradle << 'EOF'
plugins {
id 'com.android.library'
}
android {
namespace 'com.github.catvod'
compileSdk 33
defaultConfig {
minSdk 21
targetSdk 33
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
// OkHttp
api 'com.squareup.okhttp3:okhttp:4.11.0'
api 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0'
api 'com.squareup.okhttp3:logging-interceptor:4.11.0'
// Gson
api 'com.google.code.gson:gson:2.10.1'
// Guava
api 'com.google.guava:guava:31.1-android'
// Logger
api 'com.orhanobut:logger:2.2.0'
// JSoup
api 'org.jsoup:jsoup:1.15.4'
// Other
api 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3'
}
EOF
# 修复 quickjs/build.gradle
echo -e "${YELLOW}修复 quickjs 模块依赖...${NC}"
cat > quickjs/build.gradle << 'EOF'
plugins {
id 'com.android.library'
}
android {
namespace 'com.fongmi.quickjs'
compileSdk 33
defaultConfig {
minSdk 21
targetSdk 33
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
repositories {
maven { url 'https://jitpack.io' }
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation project(':catvod')
// QuickJS
implementation 'io.github.taoweiji.quickjs:quickjs-android:0.9.0'
implementation 'com.github.whl1729:quickjs-android:3.2.0'
// Concurrent
implementation 'net.sourceforge.streamsupport:streamsupport:1.7.4'
implementation 'net.sourceforge.streamsupport:android-retrofuture:1.7.4'
}
EOF
# 修复 thunder/build.gradle
echo -e "${YELLOW}修复 thunder 模块依赖...${NC}"
cat > thunder/build.gradle << 'EOF'
plugins {
id 'com.android.library'
}
android {
namespace 'com.xunlei.downloadlib'
compileSdk 33
defaultConfig {
minSdk 21
targetSdk 33
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation project(':catvod')
}
EOF
# 修复 forcetech/build.gradle
echo -e "${YELLOW}修复 forcetech 模块依赖...${NC}"
cat > forcetech/build.gradle << 'EOF'
plugins {
id 'com.android.library'
}
android {
namespace 'com.forcetech'
compileSdk 33
defaultConfig {
minSdk 21
targetSdk 33
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation project(':catvod')
}
EOF
# 修复 hook/build.gradle
echo -e "${YELLOW}修复 hook 模块依赖...${NC}"
cat > hook/build.gradle << 'EOF'
plugins {
id 'com.android.library'
}
android {
namespace 'com.fongmi.hook'
compileSdk 33
defaultConfig {
minSdk 21
targetSdk 33
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
}
EOF
# 修复 jianpian/build.gradle
echo -e "${YELLOW}修复 jianpian 模块依赖...${NC}"
cat > jianpian/build.gradle << 'EOF'
plugins {
id 'com.android.library'
}
android {
namespace 'com.p2p.jianpian'
compileSdk 33
defaultConfig {
minSdk 21
targetSdk 33
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation project(':catvod')
}
EOF
# 修复 tvbus/build.gradle
echo -e "${YELLOW}修复 tvbus 模块依赖...${NC}"
cat > tvbus/build.gradle << 'EOF'
plugins {
id 'com.android.library'
}
android {
namespace 'com.tvbus'
compileSdk 33
defaultConfig {
minSdk 21
targetSdk 33
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation project(':catvod')
}
EOF
# 修复 zlive/build.gradle
echo -e "${YELLOW}修复 zlive 模块依赖...${NC}"
cat > zlive/build.gradle << 'EOF'
plugins {
id 'com.android.library'
}
android {
namespace 'com.zlive'
compileSdk 33
defaultConfig {
minSdk 21
targetSdk 33
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation project(':catvod')
}
EOF
# 修改settings.gradle添加jitpack仓库
echo -e "${YELLOW}修改 settings.gradle 添加 jitpack 仓库...${NC}"
cat > settings.gradle << 'EOF'
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url "https://jitpack.io" }
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url "https://jitpack.io" }
}
}
include ':app'
include ':catvod'
include ':quickjs'
include ':forcetech'
include ':hook'
include ':jianpian'
include ':thunder'
include ':tvbus'
include ':zlive'
rootProject.name = "TV"
EOF
echo -e "${GREEN}依赖修复完成!${NC}"
echo -e "${YELLOW}现在您可以尝试构建项目:./gradlew clean${NC}"
+6 -2
View File
@@ -6,7 +6,7 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 --add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 --add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
@@ -21,4 +21,8 @@ android.enableJetifier=true
# thereby reducing the size of the R class for that library
android.useFullClasspathForDexingTransform=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
android.nonFinalResIds=false
# Library versions
media3Version=1.3.0
okhttpVersion=4.11.0
+25
View File
@@ -0,0 +1,25 @@
# 配置Java路径,使用Java 17而不是默认的Java 21
org.gradle.java.home=/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home
# 增加构建内存
org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=1024m -XX:+HeapDumpOnOutOfMemoryError
# 启用并行构建
org.gradle.parallel=true
org.gradle.caching=true
# 配置网络设置
systemProp.https.protocols=TLSv1.2,TLSv1.3
systemProp.https.proxyPort=0
systemProp.https.nonProxyHosts=localhost
# Android相关配置
android.useAndroidX=true
android.enableJetifier=true
android.jetifier.ignorelist=bcprov-jdk15on,annotation-experimental-1.4.1.aar,activity-1.8.0.aar,nextlib-media3ext-0.8.4.aar,sardine-android-0.9.aar,bcprov-jdk18on-1.79.jar
# 允许高版本的SDK
android.suppressUnsupportedCompileSdk=35
# 禁用增量编译以解决某些兼容性问题
android.enableBuildIncremental=false
+1 -1
View File
@@ -1,6 +1,6 @@
#Wed Mar 29 12:54:35 CST 2023
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
+1 -1
View File
@@ -499,6 +499,6 @@ public class Hook extends PackageManager {
@Override
public boolean canRequestPackageInstalls() {
return false;
return true; // 允许请求安装包权限
}
}
+28
View File
@@ -0,0 +1,28 @@
gradle.beforeProject { project ->
File localProperties = new File(project.projectDir, 'gradle.properties.local')
if (localProperties.exists()) {
Properties props = new Properties()
localProperties.withInputStream { props.load(it) }
props.each { key, val -> project.ext[key] = val }
}
}
allprojects {
// 使
buildscript {
repositories {
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
google()
mavenCentral()
}
}
repositories {
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/google' }
google()
mavenCentral()
}
}
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
echo "=== 检查模拟器状态 ==="
adb devices
echo ""
echo "=== 安装APK到模拟器 ==="
adb install -r /Users/chen/Desktop/XMBOX/app/build/outputs/apk/mobileArm64_v8a/debug/mobile-arm64_v8a.apk
echo ""
echo "=== 启动应用 ==="
adb shell am start -n com.fongmi.android.tv/.ui.activity.HomeActivity
echo ""
echo "=== 等待应用启动... ==="
sleep 3
echo ""
echo "=== 查看应用日志 ==="
echo "按 Ctrl+C 停止日志监控"
adb logcat -c
adb logcat | grep -E "custom_spider|Spider|JarLoader|fongmi" --color=always
Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

+69
View File
@@ -0,0 +1,69 @@
{
"name": "广告过滤配置示例",
"description": "演示如何配置广告域名黑名单,阻止视频中途弹出的广告(如澳门新葡京等博彩广告)",
"说明": {
"内置拦截": "应用已内置常见广告域名库,包括:澳门新葡京、皇冠、金沙等博彩广告;Google、百度、淘宝等广告联盟;优酷、爱奇艺等视频平台广告",
"自定义拦截": "可以在配置文件中添加ads字段,补充需要拦截的广告域名",
"支持正则": "域名支持正则表达式匹配,使用 .* 作为通配符"
},
"配置示例": {
"spider": "your_spider_url",
"sites": [],
"ads": [
"注释: 以下是自定义广告域名列表,会与内置域名库合并使用",
"注释: 精确匹配 - 直接写完整域名",
"mimg.0c1q0l.cn",
"www.92424.cn",
"vip.ffzyad.com",
"注释: 模糊匹配 - 使用通配符",
".*\\.doubleclick\\.net",
".*\\.googlesyndication\\.com",
"注释: 关键词匹配 - 拦截包含特定关键词的域名",
".*葡京.*",
".*皇冠.*",
".*金沙.*",
".*casino.*",
".*bet.*",
"注释: 特定平台的广告",
"wan.51img1.com",
"k.jinxiuzhilv.com",
"ssl.kdd.cc"
]
},
"常见问题": {
"Q1": "为什么配置了还是有广告?",
"A1": "1. 检查广告域名是否正确;2. 某些广告可能直接嵌入视频流,无法通过域名拦截;3. 尝试使用片头片尾跳过功能",
"Q2": "如何找到广告的域名?",
"A2": "1. 使用浏览器开发者工具查看网络请求;2. 查看应用日志中的URL;3. 参考其他用户分享的广告域名列表",
"Q3": "会不会误拦截正常内容?",
"A3": "内置域名库经过筛选,主要针对已知广告。如有误拦截,可以反馈给开发者"
},
"片头片尾跳过": {
"说明": "对于嵌入视频流中的广告,可以使用片头片尾跳过功能",
"使用方法": [
"1. 播放视频时,在片头(前5分钟内)按【片头】按钮,记录当前时间点",
"2. 在片尾(后5分钟内)按【片尾】按钮,记录结束前的时间点",
"3. 下次播放相同视频时,会自动跳过片头,并在片尾前停止",
"4. 如需重置,长按对应按钮即可"
]
},
"技术说明": {
"拦截层级": "WebView网络请求层拦截",
"拦截方式": "返回空响应,阻止广告内容加载",
"性能影响": "极小,仅在WebView解析时生效",
"隐私保护": "所有拦截在本地进行,不上传任何数据"
}
}
+8 -10
View File
@@ -5,26 +5,24 @@ plugins {
android {
namespace 'com.fongmi.android.tv.quickjs'
compileSdk 35
compileSdk {
version = release(36)
}
defaultConfig {
minSdk 21
minSdk 24
targetSdk 28
}
lint {
disable 'UnsafeOptInUsageError'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
dependencies {
implementation project(':catvod')
implementation 'wang.harlon.quickjs:wrapper-java:3.2.0'
implementation 'wang.harlon.quickjs:wrapper-android:3.2.0'
implementation 'wang.harlon.quickjs:wrapper-java:3.2.3'
implementation 'wang.harlon.quickjs:wrapper-android:3.2.3'
implementation 'net.sourceforge.streamsupport:android-retrofuture:1.7.4'
}
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
echo "=== 替换Spider Jar文件 ==="
# 删除旧的fm.jar
echo "删除旧的fm.jar..."
rm -f /Users/chen/Desktop/XMBOX/app/src/main/assets/jar/fm.jar
# 复制新的custom_spider.jar
echo "复制custom_spider.jar..."
cp /Users/chen/Desktop/custom_spider.jar /Users/chen/Desktop/XMBOX/app/src/main/assets/jar/custom_spider.jar
# 验证
echo ""
echo "=== 验证结果 ==="
ls -lh /Users/chen/Desktop/XMBOX/app/src/main/assets/jar/
echo ""
md5 /Users/chen/Desktop/XMBOX/app/src/main/assets/jar/custom_spider.jar
echo ""
echo "=== 清理并重新构建 ==="
cd /Users/chen/Desktop/XMBOX
./gradlew clean assembleMobileArm64_v8aDebug
echo ""
echo "=== 完成! ==="