63 Commits

Author SHA1 Message Date
您的名字 59a4096b37 WebDAV同步功能重大改进:修复编码问题和双向同步
主要改进:
1. 修复观看记录key中站点名称的编码问题(电视版乱码修复)
2. 实现智能合并策略,支持时间和进度比较
3. 自动修复过期时间戳,确保记录能正常显示
4. 上传和下载都使用findAllRecent(0),确保完整同步
5. 添加详细日志,方便调试定位问题

技术细节:
- 新增fixHistoryKey()方法,单独修复key中的站点名称部分
- 改进合并算法,考虑时间相近、进度领先等多种情况
- 修复createTime超过60天被过滤的问题
- 统一本地和远程记录的编码处理

删除的文件:
- other/sample/* - 示例配置文件
- other/image/* - 示例图片
- .vscode/settings.json - 编辑器配置
2025-11-18 10:22:38 +08:00
您的名字 156cecc848 项目清理:删除临时文件和示例目录,增强项目规范
- 删除 .vscode/ 编辑器配置
- 删除 other/ 示例和工具目录
- 增强 .gitignore,防止IDE配置和临时文件被提交
- 添加 CONTRIBUTING.md 贡献指南
- 添加 clean_project.sh 项目清理脚本
- 清理所有 .DS_Store 系统文件
2025-11-18 10:22:25 +08:00
您的名字 98da628aee docs: 更新README.md,添加v3.1.1版本说明和WebDAV同步功能介绍 2025-11-10 19:06:51 +08:00
您的名字 e0aee44d5a feat: 同步本地代码到远程仓库
- 新增WebDAV同步功能相关文件
- 新增CustomSwitch自定义开关组件
- 新增SyncCodeManager、UpdateInstaller、WebDAVSyncManager工具类
- 新增build_all_release.sh构建脚本
- 更新多个Dialog和Activity文件
- 更新字符串资源文件
- 删除apk/release目录下的旧文件
2025-11-10 19:05:21 +08:00
Tochen e7e215628b Update README.md 2025-11-10 17:17:52 +08:00
您的名字 a0888f7930 chore: 清理项目并规范化
- 删除重复的 XMBOX-Release 目录(与 apk/ 目录功能重复)
- 删除临时清理脚本和无关工具文件(bfg.jar, cleaner.bat)
- 从 Git 中移除本地配置文件(gradle.properties.local)
- 删除空的 local-repo 目录和 .DS_Store 文件
- 更新项目名称:rootProject.name 从 'TV' 改为 'XMBOX'
- 更新通知渠道名称从 'TV' 改为 'XMBOX'
- 更新关于对话框文本,移除 GitHub 链接
- 更新 .gitignore,添加更多忽略规则
- 从 settings.gradle 中注释掉未使用的 chaquo 模块
2025-11-09 18:22:19 +08:00
您的名字 3441bbc8f0 fix: 恢复 JSON 方式检测更新,GitHub API 作为备用
- 优先使用 XMBOX-Release 仓库的 JSON 文件检测更新
- JSON 检测失败时回退到 GitHub Releases API
- 修复 v3.0.9 无法检测更新的问题
- 提升更新检测的兼容性和稳定性
2025-10-28 20:25:56 +08:00
您的名字 13bc801b12 chore: 添加 v3.1.0 版本的 APK 文件
- mobile-arm64_v8a-v3.1.0.apk (34.4MB)
- mobile-armeabi_v7a-v3.1.0.apk (30.4MB)
- leanback-arm64_v8a-v3.1.0.apk (34.5MB)
- leanback-armeabi_v7a-v3.1.0.apk (30.5MB)
2025-10-28 20:25:56 +08:00
您的名字 9f3b631dfb chore: 发布 v3.1.0 版本 APK
- 添加 v3.1.0 版本所有架构的 APK 文件
- 更新版本信息 JSON 文件
- 手机版: mobile-arm64_v8a (34.4MB), mobile-armeabi_v7a (30.4MB)
- TV版: leanback-arm64_v8a (34.5MB), leanback-armeabi_v7a (30.5MB)
2025-10-28 20:25:49 +08:00
您的名字 8cfea9ef79 chore: 发布 v3.1.0 版本
- 更新 README.md 到 v3.1.0
- 添加 v3.1.0 版本的 JSON 配置文件
- 更新根目录的版本信息 JSON
2025-10-28 19:53:50 +08:00
您的名字 4a8a84a4eb feat: 升级到v3.1.0
- 实现定时按钮倒计时显示功能
- 适配pixel主题化图标展示
- 优化TimerDialog按钮宽度设计
2025-10-28 19:49:49 +08:00
您的名字 56251db9e7 feat: 优化播放进度条交互体验
- 修复拖拽时圆球消失问题
- 添加动态轨道高度变化效果(按住时4dp,松开时2dp)
- 优化圆球大小设置(固定14dp)
- 添加ProGuard规则保护DefaultTimeBar反射字段
- 改进触摸事件处理逻辑
- 增强拖拽体验的流畅性

修复内容:
- CustomSeekView: 重构触摸事件处理和动态高度调整
- 布局文件: 统一设置圆球大小为14dp
- ProGuard: 保护Media3 DefaultTimeBar字段不被混淆
2025-10-28 19:49:49 +08:00
Tochen 8357ebefcf Update README.md 2025-10-24 15:31:56 +08:00
Tochen b16cb4d193 Update README.md 2025-10-24 15:28:27 +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
323 changed files with 13021 additions and 1544 deletions
+68 -7
View File
@@ -1,8 +1,69 @@
.idea # Gradle files
.gradle .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 *.jks
lib-*.aar *.keystore
*build
/media* # Google Services (e.g. APIs or Firebase)
/Release google-services.json
/local.properties
# Android Profiling
*.hprof
# APK files
apk/release/*.apk
# Local configuration files
gradle.properties.local
local.properties
local-repo/
# Temporary files
cleanup_project.sh
*.tmp
*.temp
# System files
.DS_Store
Thumbs.db
*.swp
*~
# Editor directories and files
.vscode/
.vs/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Duplicate release directories
XMBOX-Release/
# Tools and utilities (not part of the project)
other/
tools/
+77
View File
@@ -0,0 +1,77 @@
# 贡献指南
## 项目结构
这是一个Android应用项目,主要使用Java开发。
### 语言组成
- **Java (78.4%)** - 主要应用代码
- **JavaScript (9.9%)** - WebView内嵌脚本和爬虫引擎
- **CSS (7.3%)** - WebView样式文件
- **GLSL (2.5%)** - Media3视频渲染着色器
- **Shell (1.1%)** - 构建和部署脚本
- **HTML (0.8%)** - WebView页面
### 目录说明
```
XMBOX/
├── app/ # 主应用模块
│ ├── src/main/ # 通用代码
│ ├── src/leanback/ # 电视版UI代码
│ └── src/mobile/ # 手机版UI代码
├── catvod/ # 爬虫核心库
├── quickjs/ # JavaScript引擎
├── thunder/ # 迅雷下载模块
├── forcetech/ # P2P模块
├── jianpian/ # 减片模块
├── tvbus/ # TVBus模块
├── zlive/ # 直播模块
└── docs/ # 文档
```
## 代码规范
### Java代码
- 遵循Android开发规范
- 使用驼峰命名法
- 类名首字母大写
- 方法和变量名首字母小写
- 常量全大写,用下划线分隔
### 资源文件
- JavaScript/CSS/HTML位于 `app/src/main/assets/`
- 这些文件用于WebView解析和内容抓取,不可删除
### GLSL着色器
- 由Media3库提供,用于视频渲染
- 自动生成,不需要手动修改
## 清理项目
运行清理脚本:
```bash
./clean_project.sh
```
这将清理:
- 构建产物(build目录)
- 临时文件
- 系统文件(.DS_Store等)
- IDE配置文件
## 提交代码
1. 清理项目:`./clean_project.sh`
2. 查看改动:`git status`
3. 添加文件:`git add .`
4. 提交代码:`git commit -m "描述"`
5. 推送代码:`git push`
## 注意事项
1. **不要删除assets目录**中的JS/CSS/HTML文件,这些是应用必需的
2. **不要删除GLSL文件**,这些是视频播放器需要的
3. 提交前运行 `./gradlew clean` 清理构建产物
4. 确保新增的临时文件已添加到 `.gitignore`
+293 -214
View File
@@ -1,276 +1,355 @@
# XMBOX <h1 align="center"> 📱 XMBOX - Android视频播放器
</h1>
<div align="center">
各模块说明: ![Version](https://img.shields.io/badge/version-3.1.1-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)
- app - 主要的应用程序代码 一个操作方便、界面简洁的Android视频播放器盒子,需自行添源,支持TV和手机双平台。
- catvod - 视频点播相关功能
- forcetech - 强制技术相关功能
- hook - 钩子功能
- jianpian - 剪片相关功能
- quickjs - JavaScript 引擎
- thunder - 迅雷下载相关功能
- tvbus - TV 总线功能
- zlive - 直播相关功能
一个简单的视频播放器应用,支持以下功能: [下载APK](https://github.com/Tosencen/XMBOX-Release/tree/main/apk/release) • [功能特性](#-功能特性) • [构建指南](#-构建指南) • [API文档](#-api文档)
<img width="1920" height="864" alt="Group 15" src="https://github.com/user-attachments/assets/e69741bd-a21d-417e-ad85-e747032f6daf" />
## 主要功能 </div>
- 视频播放:支持多种格式视频播放
- 直播观看:支持直播源播放
- 收藏管理:可收藏喜欢的视频和直播源
- 设置中心:自定义应用配置
## 技术特点 ## 🎯 功能特性
- 基于 Android 原生开发
- 使用 ExoPlayer 作为播放内核
- 支持 TV 和手机双平台
- Material Design 界面设计
## 开发说明 ### 📺 多平台支持
本项目仅用于学习 Android 开发,代码改自 [FongMi/TV](https://github.com/FongMi/TV)。 - **Android TV版本** - 针对电视、盒子优化的遥控器界面
- **手机版本** - 触屏友好的移动端界面
- **多架构支持** - ARM64-V8A 和 ARM V7A 双架构
## 免责声明 ### 🎬 强大的播放功能
1. 本项目仅供学习交流使用,不得用于商业用途 - 🎵 **多格式支持** - 支持主流视频格式播放
2. 项目中的内容均来自网络,如有侵权请联系删除 - 📡 **直播观看** - 支持各种直播源协议
3. 使用本项目产生的一切后果由使用者自行承担 - 🔍 **智能搜索** - 全局搜索和换源功能
- 📚 **收藏管理** - 视频收藏和历史记录
- 🎨 **自定义界面** - 丰富的主题和布局选项
## 许可证 ### ⚡ 技术特色
GPL-3.0 license - 🚀 **高性能播放** - 基于ExoPlayer播放内核
- 🔧 **模块化架构** - 清晰的模块分层设计
- 🛡️ **稳定可靠** - 完善的错误处理和崩溃防护
- 🌐 **网络优化** - 智能代理和DNS解析
- 📱 **Material Design** - 现代化UI设计
- ☁️ **WebDAV同步** - 支持观看记录和设置云端同步,支持账号模式和同步码模式
# 影視 ## 📥 下载安装
### 基於 CatVod 項目 ### 最新版本: v3.1.1
https://github.com/CatVodTVOfficial/CatVodTVJarLoader | 平台 | ARM64-V8A | ARM V7A |
|------|-----------|---------|
| **📱 手机版** | [下载 (34.4MB)](https://github.com/Tosencen/XMBOX-Release/raw/main/apk/release/v3.1.0/mobile-arm64_v8a-v3.1.0.apk) | [下载 (30.4MB)](https://github.com/Tosencen/XMBOX-Release/raw/main/apk/release/v3.1.0/mobile-armeabi_v7a-v3.1.0.apk) |
| **📺 TV版** | [下载 (34.5MB)](https://github.com/Tosencen/XMBOX-Release/raw/main/apk/release/v3.1.0/leanback-arm64_v8a-v3.1.0.apk) | [下载 (30.5MB)](https://github.com/Tosencen/XMBOX-Release/raw/main/apk/release/v3.1.0/leanback-armeabi_v7a-v3.1.0.apk) |
### 點播欄位 ### 📁 版本历史
- **v3.1.1**: [查看v3.1.1版本](https://github.com/Tosencen/XMBOX-Release/tree/main/apk/release/v3.1.1) - 新增WebDAV同步功能和更新安装器
- **v3.1.0**: [查看v3.1.0版本](https://github.com/Tosencen/XMBOX-Release/tree/main/apk/release/v3.1.0) - 定时器优化和画中画修复版本
- **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交互体验全面优化
| 欄位名稱 | 預設值 | 說明 | 其他 | ### 📦 下载说明
|------------|------|------|------------| - **最新版本**: 根目录的 `mobile.json``leanback.json` 包含最新版本信息
| searchable | 1 | 是否搜索 | 0:關閉;1:啟用 | - **历史版本**: 每个版本都有独立的文件夹,包含完整的APK文件和版本信息
| changeable | 1 | 是否換源 | 0:關閉;1:啟用 | - **文件结构**: 按版本号组织,便于管理和下载
| quickserch | 1 | 是否快搜 | 0:關閉;1:啟用 | - **签名保护**: 所有APK均使用v1/v2/v3/v4多重签名保护
| indexs | 0 | 是否聚搜 | 0:關閉;1:啟用 |
| hide | 0 | 是否隱藏 | 0:顯示;1:隱藏 |
| timeout | 15 | 播放超時 | 單位:秒 |
| header | none | 請求標頭 | 格式:json |
| click | none | 點擊js | javascript |
### 直播欄位 TV版基于 [FongMi/TV](https://github.com/FongMi/TV) 原项目就改了些配色,想要嘿稳定的可去原项目体验
### 📋 系统要求
- Android 5.0 (API 21) 及以上
- ARM64-V8A: 推荐新设备使用,性能更优
- ARM V7A: 兼容老设备,适配性更强
| 欄位名稱 | 預設值 | 說明 | 其他 | ## 🏗️ 构建指南
|----------|-------|-------|------------|
| 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 | 時區 | |
### 樣式 ### 📋 环境要求
- Android Studio Arctic Fox 或更高版本
- JDK 11 或更高版本
- Android SDK API 35
- Gradle 8.10.2
| 欄位名稱 | 值 | 說明 | ### 🔧 快速开始
|-------|------|-----|
| type | rect | 矩形 |
| | oval | 橢圓 |
| | list | 列表 |
| ratio | 0.75 | 34 |
| | 1.33 | 43 |
直式 1. **克隆项目**
```bash
```json git clone https://github.com/yourusername/XMBOX.git
{ cd XMBOX
"style": {
"type": "rect"
}
}
``` ```
橫式 2. **配置签名** (可选)
```bash
```json # 将你的签名文件放到 keystore/ 目录
{ # 或修改 app/build.gradle 中的签名配置
"style": {
"type": "rect",
"ratio": 1.33
}
}
``` ```
正方 3. **构建项目**
```bash
# 构建所有版本
./gradlew assembleRelease
```json # 构建特定版本
{ ./gradlew assembleMobileArm64_v8aRelease # 手机版 ARM64
"style": { ./gradlew assembleLeanbackArm64_v8aRelease # TV版 ARM64
"type": "rect", ./gradlew assembleMobileArmeabi_v7aRelease # 手机版 ARM V7A
"ratio": 1 ./gradlew assembleLeanbackArmeabi_v7aRelease # TV版 ARM V7A
}
}
``` ```
正圓 4. **生成的APK位置**
```
```json app/build/outputs/apk/
{ ├── mobileArm64_v8a/release/mobile-arm64_v8a.apk
"style": { ├── leanbackArm64_v8a/release/leanback-arm64_v8a.apk
"type": "oval" ├── mobileArmeabi_v7a/release/mobile-armeabi_v7a.apk
} └── leanbackArmeabi_v7a/release/leanback-armeabi_v7a.apk
}
``` ```
橢圓 ## 🏛️ 项目架构
```json ### 📂 模块说明
{ ```
"style": { XMBOX/
"type": "oval", ├── app/ # 主应用模块
"ratio": 1.1 │ ├── 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
刷新詳情 ## 📝 更新日志
``` ### v3.1.1 (2025-11-10)
http://127.0.0.1:9978/action?do=refresh&type=detail
#### ✨ 新功能
* **WebDAV同步功能** - 新增WebDAV云端同步功能,支持观看记录和设置的多设备同步
* 支持账号模式:使用WebDAV服务器账号密码进行同步
* 支持同步码模式:无需账号,使用同步码即可实现多设备数据共享
* 自动合并本地和远程数据,确保数据完整性
* **更新安装器** - 新增UpdateInstaller工具类,优化应用更新安装体验
* **自定义开关组件** - 新增CustomSwitch组件,提供更灵活的UI控制
#### 🎨 UI优化
* **WebDAV配置界面** - 新增WebDAV配置对话框,支持账号模式和同步码模式切换
* **设置页面优化** - 优化设置页面布局和交互体验
#### 🔧 技术改进
* **代码同步管理** - 新增SyncCodeManager和WebDAVSyncManager,完善同步功能架构
* **构建脚本优化** - 新增build_all_release.sh脚本,支持一键构建所有版本
* **稳定性增强** - 优化同步逻辑,防止重复同步和数据冲突
### v3.1.0 (2025-10-28)
#### ✨ 新功能
* **定时倒计时显示** - 实现定时按钮倒计时显示功能,用户可以实时看到剩余时间
* **主题化图标适配** - 适配pixel主题化图标展示,提升视觉体验
#### 🎨 UI优化
* **TimerDialog优化** - 优化TimerDialog按钮宽度设计,界面更加协调
* **进度条交互改进** - 优化播放进度条交互体验,操作更流畅
* **视觉一致性提升** - 改进界面视觉一致性,整体更加统一
#### 🐛 修复
* **更新链接修复** - 修复更新跳转链接,现在可以正确跳转到具体版本页面
#### 🔧 技术改进
* **定时功能优化** - 提升定时功能用户体验
* **内存优化** - 进一步优化内存使用
* **稳定性增强** - 提升播放稳定性
### v3.0.9 (2025-10-24)
#### ✨ 新功能
* **直播开关控制** - 新增直播tab显示/隐藏开关,用户可根据需要控制直播功能
* **实时倍速显示** - 播放控制对话框新增实时倍速数值显示,提升用户体验
#### 🎨 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!**
``` Made with ❤️ by XMBOX Team
proxy://
```
```
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)
</div>
Binary file not shown.
-5
View File
@@ -1,5 +0,0 @@
{
"name": "v3.0.1",
"code": 301,
"desc": "1. 优化项目结构,重新组织各功能模块\n2. 更新移动端界面,优化用户体验\n3. 强制使用深色模式,提供更好的观看体验"
}
Binary file not shown.
-5
View File
@@ -1,5 +0,0 @@
{
"name": "v3.0.1",
"code": 301,
"desc": "1. 优化项目结构,重新组织各功能模块\n2. 更新移动端界面,优化用户体验\n3. 强制使用深色模式,提供更好的观看体验"
}
+37 -16
View File
@@ -5,7 +5,7 @@ plugins {
android { android {
namespace 'com.fongmi.android.tv' namespace 'com.fongmi.android.tv'
compileSdk 35 compileSdk 36
flavorDimensions = ["mode", "abi"] flavorDimensions = ["mode", "abi"]
signingConfigs { signingConfigs {
@@ -14,16 +14,24 @@ android {
storePassword "xmbox123" storePassword "xmbox123"
keyAlias "xmbox" keyAlias "xmbox"
keyPassword "xmbox123" keyPassword "xmbox123"
// 同时启用v1、v2、v3、v4签名以确保最佳兼容性
enableV1Signing true
enableV2Signing true
enableV3Signing true
enableV4Signing true
} }
} }
defaultConfig { defaultConfig {
applicationId "com.fongmi.android.tv" applicationId "com.fongmi.android.tv"
minSdk 21 minSdk 24
//noinspection ExpiredTargetSdkVersion //noinspection ExpiredTargetSdkVersion
targetSdk 28 targetSdk 28
versionCode 302 versionCode 311
versionName "3.0.2" versionName "3.1.1"
// GitHub Token (可选,用于提高API请求限制)
def githubToken = project.findProperty("GITHUB_TOKEN") ?: ""
buildConfigField "String", "GITHUB_TOKEN", "\"${githubToken}\""
javaCompileOptions { javaCompileOptions {
annotationProcessorOptions { annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString(), "eventBusIndex": "com.fongmi.android.tv.event.EventIndex"] arguments = ["room.schemaLocation": "$projectDir/schemas".toString(), "eventBusIndex": "com.fongmi.android.tv.event.EventIndex"]
@@ -67,6 +75,17 @@ android {
exclude 'META-INF/beans.xml' exclude 'META-INF/beans.xml'
exclude 'META-INF/versions/9/OSGI-INF/MANIFEST.MF' 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 -> android.applicationVariants.configureEach { variant ->
@@ -87,15 +106,15 @@ android {
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_11 sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_17
} }
} }
dependencies { dependencies {
implementation fileTree(dir: "libs", include: ["*.aar"]) implementation fileTree(dir: "libs", include: ["*.aar"])
implementation project(':catvod') implementation project(':catvod')
//implementation project(':chaquo') // implementation project(':chaquo') // 移除Python支持减少8-10MB体积
implementation project(':quickjs') implementation project(':quickjs')
implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.media:media:1.7.0' implementation 'androidx.media:media:1.7.0'
@@ -114,9 +133,9 @@ dependencies {
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:' + media3Version implementation 'androidx.media3:media3-exoplayer-smoothstreaming:' + media3Version
implementation 'androidx.media3:media3-extractor:' + media3Version implementation 'androidx.media3:media3-extractor:' + media3Version
implementation 'androidx.media3:media3-ui:' + 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 '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.bassaer:materialdesigncolors:1.0.0'
implementation 'com.github.bumptech.glide:glide:4.16.0' implementation 'com.github.bumptech.glide:glide:4.16.0'
implementation 'com.github.bumptech.glide:annotations:4.16.0' implementation 'com.github.bumptech.glide:annotations:4.16.0'
@@ -124,12 +143,12 @@ dependencies {
implementation 'com.github.bumptech.glide:okhttp3-integration:4.16.0' implementation 'com.github.bumptech.glide:okhttp3-integration:4.16.0'
implementation 'com.github.jahirfiquitiva:TextDrawable:1.0.3' implementation 'com.github.jahirfiquitiva:TextDrawable:1.0.3'
implementation 'com.github.thegrizzlylabs:sardine-android:0.9' 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.android.material:material:1.12.0'
implementation 'com.google.zxing:core:3.5.3' implementation 'com.google.zxing:core:3.5.3'
implementation 'com.guolindev.permissionx:permissionx:1.8.0' implementation 'com.guolindev.permissionx:permissionx:1.7.1'
implementation 'com.hierynomus:smbj:0.14.0' implementation 'com.hierynomus:smbj:0.13.0'
implementation 'io.antmedia:rtmp-client:3.2.0' implementation 'io.antmedia:rtmp-client:3.1.0'
implementation 'javax.servlet:javax.servlet-api:3.1.0' implementation 'javax.servlet:javax.servlet-api:3.1.0'
implementation 'org.aomedia.avif.android:avif:1.1.1.14d8e3c4' implementation 'org.aomedia.avif.android:avif:1.1.1.14d8e3c4'
implementation 'org.eclipse.jetty:jetty-client:8.1.21.v20160908' implementation 'org.eclipse.jetty:jetty-client:8.1.21.v20160908'
@@ -146,8 +165,10 @@ dependencies {
mobileImplementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' mobileImplementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
mobileImplementation 'com.google.android.flexbox:flexbox:3.0.0' mobileImplementation 'com.google.android.flexbox:flexbox:3.0.0'
mobileImplementation('com.journeyapps:zxing-android-embedded:4.3.0') { transitive = false } mobileImplementation('com.journeyapps:zxing-android-embedded:4.3.0') { transitive = false }
annotationProcessor 'androidx.room:room-compiler:2.7.1' annotationProcessor 'androidx.room:room-compiler:2.6.1'
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0' // annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.3.1' 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.** { *; } -keep class fi.iki.elonen.** { *; }
# NewPipeExtractor # 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.** { *; } -keep class org.mozilla.javascript.** { *; }
-keep class org.mozilla.javascript.engine.** { *; } -keep class org.mozilla.javascript.engine.** { *; }
-keep class javax.script.** { *; } -keep class org.mozilla.classfile.ClassFileWriter
-keep class jdk.dynalink.** { *; } -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.JavaToJSONConverters
-dontwarn org.mozilla.javascript.tools.** -dontwarn org.mozilla.javascript.tools.**
-dontwarn javax.script.** -dontwarn javax.script.**
@@ -90,5 +92,12 @@
-keep class com.sun.jna.** { *; } -keep class com.sun.jna.** { *; }
-keep class com.east.android.zlive.** { *; } -keep class com.east.android.zlive.** { *; }
# Media3 DefaultTimeBar - 保护反射访问的字段
-keep class androidx.media3.ui.DefaultTimeBar {
int barHeight;
int scrubberEnabledSize;
int scrubberDisabledSize;
}
# Zxing # Zxing
-keep class com.google.zxing.** { *; } -keep class com.google.zxing.** { *; }
@@ -1,21 +1,27 @@
package com.fongmi.android.tv; package com.fongmi.android.tv;
import android.app.Activity; import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import com.fongmi.android.tv.App;
import com.fongmi.android.tv.databinding.DialogUpdateBinding; import com.fongmi.android.tv.databinding.DialogUpdateBinding;
import com.fongmi.android.tv.utils.Download; import com.fongmi.android.tv.utils.Download;
import com.fongmi.android.tv.utils.FileUtil; import com.fongmi.android.tv.utils.FileUtil;
import com.fongmi.android.tv.utils.Notify; import com.fongmi.android.tv.utils.Notify;
import com.fongmi.android.tv.utils.ResUtil; import com.fongmi.android.tv.utils.ResUtil;
import com.fongmi.android.tv.utils.UpdateInstaller;
import com.github.catvod.net.OkHttp; import com.github.catvod.net.OkHttp;
import com.github.catvod.utils.Github; import com.github.catvod.utils.Github;
import com.github.catvod.utils.Logger;
import com.github.catvod.utils.Path; import com.github.catvod.utils.Path;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import java.io.File; import java.io.File;
@@ -24,20 +30,34 @@ import java.util.Locale;
public class Updater implements Download.Callback { public class Updater implements Download.Callback {
private DialogUpdateBinding binding; private DialogUpdateBinding binding;
private final Download download; private Download download;
private AlertDialog dialog; private AlertDialog dialog;
private boolean dev; private boolean dev;
private boolean forceCheck; // 是否为手动检查
private boolean autoShow; // 是否自动显示更新对话框(用于自动检查)
private String latestVersion; // 存储检测到的最新版本
private String releaseApkUrl; // 从 GitHub Release 获取的 APK 下载链接(jsDelivr CDN
private String fallbackApkUrl; // 备用下载链接(GitHub原始URL)
// 静态变量:记录上次检查时间(用于时间间隔限制)
private static long lastCheckTime = 0;
private static final long CHECK_INTERVAL = 60 * 60 * 1000; // 1小时(毫秒)
private File getFile() { private File getFile() {
return Path.cache("update.apk"); // Android 10+ 无法直接访问外部存储的Download目录
} // 使用应用的cache目录,FileProvider可以正常访问
return Path.cache("XMBOX-update.apk");
private String getJson() {
return Github.getJson(dev, BuildConfig.FLAVOR_mode);
} }
private String getApk() { private String getApk() {
return Github.getApk(dev, BuildConfig.FLAVOR_mode + "-" + BuildConfig.FLAVOR_abi); // 使用从 GitHub Release 获取的 APK URLjsDelivr CDN
if (releaseApkUrl != null && !releaseApkUrl.isEmpty()) {
Logger.d("APK download URL from Release (jsDelivr): " + releaseApkUrl);
return releaseApkUrl;
}
// 如果没有获取到URL,返回空(不应该发生)
Logger.e("Updater: 未找到APK下载链接");
return "";
} }
public static Updater create() { public static Updater create() {
@@ -45,12 +65,24 @@ public class Updater implements Download.Callback {
} }
public Updater() { public Updater() {
this.download = Download.create(getApk(), getFile(), this); this.forceCheck = false;
this.autoShow = false;
// download对象将在需要时创建
} }
public Updater force() { public Updater force() {
Notify.show(R.string.update_check); Notify.show(R.string.update_check);
Setting.putUpdate(true); Setting.putUpdate(true);
this.forceCheck = true; // 标记为手动检查
return this;
}
/**
* 设置自动检查模式(应用启动时自动检查)
*/
public Updater auto() {
this.forceCheck = false;
this.autoShow = true; // 自动显示更新对话框
return this; return this;
} }
@@ -70,6 +102,16 @@ public class Updater implements Download.Callback {
} }
public void start(Activity activity) { public void start(Activity activity) {
// 如果是自动检查,检查时间间隔
if (autoShow && !forceCheck) {
long currentTime = System.currentTimeMillis();
long timeSinceLastCheck = currentTime - lastCheckTime;
// 1小时内只检查一次
if (lastCheckTime > 0 && timeSinceLastCheck < CHECK_INTERVAL) {
Logger.d("Updater: 距离上次检查仅 " + (timeSinceLastCheck / 1000 / 60) + " 分钟,跳过本次检查");
return;
}
}
App.execute(() -> doInBackground(activity)); App.execute(() -> doInBackground(activity));
} }
@@ -78,14 +120,204 @@ public class Updater implements Download.Callback {
} }
private void doInBackground(Activity activity) { private void doInBackground(Activity activity) {
Logger.d("Updater: Starting update check...");
lastCheckTime = System.currentTimeMillis(); // 更新检查时间
// 直接使用 GitHub Releases API 检查更新
checkViaGitHubAPI(activity);
}
private void checkViaGitHubAPI(Activity activity) {
try { try {
JSONObject object = new JSONObject(OkHttp.string(getJson())); String releasesUrl = "https://api.github.com/repos/Tosencen/XMBOX/releases/latest";
String name = object.optString("name"); Logger.d("Updater: Trying GitHub Releases API: " + releasesUrl);
String desc = object.optString("desc");
int code = object.optInt("code"); // 检查是否有GitHub Token
if (need(code, name)) App.post(() -> show(activity, name, desc)); String githubToken = BuildConfig.GITHUB_TOKEN;
String response;
if (githubToken != null && !githubToken.isEmpty()) {
// 使用token进行认证请求(5000次/小时)
java.util.Map<String, String> headers = new java.util.HashMap<>();
headers.put("Authorization", "Bearer " + githubToken);
headers.put("Accept", "application/vnd.github.v3+json");
Logger.d("Updater: Using GitHub Token for authenticated request");
response = OkHttp.string(releasesUrl, headers);
} else {
// 使用未认证请求(60次/小时)
Logger.d("Updater: Using unauthenticated request (60 requests/hour limit)");
response = OkHttp.string(releasesUrl);
}
// 检查响应是否为空(可能是网络错误、VPN问题等)
if (response == null || response.isEmpty()) {
Logger.e("Updater: 网络请求失败,响应为空。可能是网络连接问题或VPN配置问题");
if (forceCheck) {
// 手动检查时,显示错误提示
App.post(() -> {
Notify.show("检查更新失败:网络连接异常,请检查网络设置或VPN配置");
showVersionInfo(activity, BuildConfig.VERSION_NAME, "");
});
} else {
Logger.w("Updater: 自动检查失败,网络不可用");
}
return;
}
if (response.contains("rate limit exceeded") || response.contains("API rate limit exceeded")) {
Logger.e("Updater: Rate limit exceeded");
if (forceCheck) {
// 手动检查时,显示版本信息弹窗(不显示错误提示)
App.post(() -> showVersionInfo(activity, BuildConfig.VERSION_NAME, ""));
}
return;
}
if (response.contains("Not Found") || response.contains("404")) {
Logger.e("Updater: Release not found");
if (forceCheck) {
// 手动检查时,显示版本信息弹窗(不显示错误提示)
App.post(() -> showVersionInfo(activity, BuildConfig.VERSION_NAME, ""));
}
return;
}
JSONObject release = new JSONObject(response);
String tagName = release.optString("tag_name");
String body = release.optString("body");
String version = tagName.startsWith("v") ? tagName.substring(1) : tagName;
Logger.d("Updater: GitHub API Remote version: " + version);
// 从 assets 中查找 APK
JSONArray assets = release.optJSONArray("assets");
if (assets != null) {
String mode = BuildConfig.FLAVOR_mode;
String abi = BuildConfig.FLAVOR_abi;
// 尝试多种文件名格式
String[] possibleNames = {
mode + "-" + abi + "-v" + version + ".apk", // leanback-arm64_v8a-v3.1.0.apk
mode + "-" + abi + "-release.apk", // leanback-arm64_v8a-release.apk
mode + "-" + abi + ".apk", // leanback-arm64_v8a.apk
mode + "-" + abi + "-" + version + ".apk" // leanback-arm64_v8a-3.1.0.apk
};
boolean found = false;
for (int i = 0; i < assets.length() && !found; i++) {
JSONObject asset = assets.getJSONObject(i);
String assetName = asset.optString("name");
// 检查是否匹配任何可能的文件名格式
for (String targetName : possibleNames) {
if (targetName.equals(assetName)) {
String githubUrl = asset.optString("browser_download_url");
// jsDelivr无法访问GitHub Release文件,直接使用GitHub Release URL
this.releaseApkUrl = githubUrl;
this.fallbackApkUrl = githubUrl;
Logger.d("Updater: 找到匹配的APK: " + assetName);
Logger.d("Updater: APK URL (GitHub Release): " + this.releaseApkUrl);
found = true;
break;
}
}
}
// 如果精确匹配失败,尝试模糊匹配(包含mode和abi的APK文件)
if (!found) {
Logger.w("Updater: 未找到精确匹配的APK,尝试模糊匹配...");
for (int i = 0; i < assets.length(); i++) {
JSONObject asset = assets.getJSONObject(i);
String assetName = asset.optString("name");
// 检查文件名是否包含mode和abi,且是APK文件
if (assetName.endsWith(".apk") &&
assetName.contains(mode) &&
assetName.contains(abi.replace("_", "-"))) {
String githubUrl = asset.optString("browser_download_url");
// jsDelivr无法访问GitHub Release文件,直接使用GitHub Release URL
this.releaseApkUrl = githubUrl;
this.fallbackApkUrl = githubUrl;
Logger.d("Updater: 找到模糊匹配的APK: " + assetName);
Logger.d("Updater: APK URL (GitHub Release): " + this.releaseApkUrl);
found = true;
break;
}
}
}
if (!found) {
Logger.e("Updater: 在Release中未找到匹配的APK文件");
Logger.e("Updater: 期望的格式: " + mode + "-" + abi + "-v" + version + ".apk");
Logger.e("Updater: 可用的assets:");
for (int i = 0; i < assets.length(); i++) {
JSONObject asset = assets.getJSONObject(i);
String assetName = asset.optString("name");
if (assetName.endsWith(".apk")) {
Logger.e("Updater: - " + assetName);
}
}
}
} else {
Logger.e("Updater: Release中没有assets数组");
}
if (needUpdate(version)) {
this.latestVersion = version;
// 有新版本时,自动显示或手动显示更新对话框
App.post(() -> show(activity, version, body));
} else {
// 没有新版本
if (forceCheck) {
// 手动检查时,显示版本信息弹窗
App.post(() -> showVersionInfo(activity, version, body));
} else if (autoShow) {
// 自动检查时,不显示任何内容(静默检查)
Logger.d("Updater: 自动检查完成,当前已是最新版本");
}
}
} catch (Exception e) { } catch (Exception e) {
Logger.e("Updater: GitHub API check failed: " + e.getMessage());
e.printStackTrace(); e.printStackTrace();
if (forceCheck) {
// 手动检查时,显示错误提示
String errorMsg = e.getMessage();
if (errorMsg != null && (errorMsg.contains("network") || errorMsg.contains("timeout") || errorMsg.contains("connect"))) {
App.post(() -> {
Notify.show("检查更新失败:网络连接异常,请检查网络设置或VPN配置");
showVersionInfo(activity, BuildConfig.VERSION_NAME, "");
});
} else {
App.post(() -> showVersionInfo(activity, BuildConfig.VERSION_NAME, ""));
}
} else {
Logger.w("Updater: 自动检查失败: " + e.getMessage());
}
}
}
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;
} }
} }
@@ -98,19 +330,55 @@ public class Updater implements Download.Callback {
binding.desc.setText(desc); binding.desc.setText(desc);
} }
/**
* 显示版本信息弹窗(无更新时)
*/
private void showVersionInfo(Activity activity, String remoteVersion, String desc) {
binding = DialogUpdateBinding.inflate(LayoutInflater.from(activity));
// 先设置所有内容,再显示对话框
binding.version.setText("最新版本");
binding.desc.setText(BuildConfig.VERSION_NAME); // 只显示当前版本号,不使用远程信息
binding.confirm.setVisibility(View.GONE);
binding.cancel.setText("确定");
binding.cancel.setOnClickListener(v -> {
if (dialog != null) dialog.dismiss();
});
check().create(activity).show();
}
private AlertDialog create(Activity activity) { private AlertDialog create(Activity activity) {
return dialog = new MaterialAlertDialogBuilder(activity).setView(binding.getRoot()).setCancelable(false).create(); dialog = new MaterialAlertDialogBuilder(activity).setView(binding.getRoot()).setCancelable(false).create();
// 设置对话框背景为透明,让布局的深色背景显示
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
dialog.getWindow().setDimAmount(0);
return dialog;
} }
private void cancel(View view) { private void cancel(View view) {
Setting.putUpdate(false); Setting.putUpdate(false);
if (download != null) {
download.cancel(); download.cancel();
dismiss(); }
dialog.dismiss();
} }
private void confirm(View view) { private void confirm(View view) {
binding.confirm.setEnabled(false); // 开始下载更新(使用jsDelivr CDN,失败时回退到GitHub
download.start(); String downloadUrl = getApk();
String fallbackUrl = this.fallbackApkUrl;
// 检查URL是否为空
if (downloadUrl == null || downloadUrl.isEmpty()) {
Logger.e("Updater: 下载URL为空,无法下载");
Notify.show("无法获取下载链接,请稍后重试或手动下载");
return;
}
Logger.d("Updater: 开始下载,URL: " + downloadUrl);
// 创建带回退URL的下载对象
this.download = Download.create(downloadUrl, getFile(), fallbackUrl, this);
this.download.start();
} }
private void dismiss() { private void dismiss() {
@@ -133,7 +401,30 @@ public class Updater implements Download.Callback {
@Override @Override
public void success(File file) { public void success(File file) {
FileUtil.openFile(file); // 使用UpdateInstaller处理安装,包括权限检查和请求
UpdateInstaller installer = UpdateInstaller.get();
// 检查安装权限
if (!installer.hasInstallPermission()) {
// 没有权限,请求权限并保存待安装的文件
Logger.d("Updater: 没有安装权限,请求权限");
installer.requestInstallPermission();
// 保存待安装的文件,将在权限授予后自动安装
installer.install(file, true); // checkPermission=true会保存文件
Notify.show("请授予安装权限以完成更新");
dismiss();
return;
}
// 有权限,直接安装
boolean success = installer.install(file, false);
if (success) {
Logger.d("Updater: 已启动安装程序");
dismiss();
} else {
Logger.e("Updater: 启动安装程序失败");
Notify.show("无法启动安装程序,请检查文件是否完整");
dismiss(); dismiss();
} }
} }
}
@@ -1,5 +1,9 @@
package com.fongmi.android.tv.ui.activity; 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.appcompat.app.AlertDialog;
import androidx.viewbinding.ViewBinding; import androidx.viewbinding.ViewBinding;
@@ -14,22 +18,37 @@ import cat.ereza.customactivityoncrash.CustomActivityOnCrash;
public class CrashActivity extends BaseActivity { public class CrashActivity extends BaseActivity {
private ActivityCrashBinding mBinding; private ActivityCrashBinding mBinding;
private String errorDetails;
@Override @Override
protected ViewBinding getBinding() { protected ViewBinding getBinding() {
return mBinding = ActivityCrashBinding.inflate(getLayoutInflater()); return mBinding = ActivityCrashBinding.inflate(getLayoutInflater());
} }
@Override
protected void initView() {
errorDetails = CustomActivityOnCrash.getAllErrorDetailsFromIntent(this, getIntent());
mBinding.error.setText(errorDetails);
}
@Override @Override
protected void initEvent() { 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())))); 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() { private void showError() {
new AlertDialog.Builder(this) new AlertDialog.Builder(this)
.setTitle(R.string.crash_details_title) .setTitle(R.string.crash_details_title)
.setMessage(CustomActivityOnCrash.getAllErrorDetailsFromIntent(this, getIntent())) .setMessage(errorDetails)
.setPositiveButton(R.string.crash_details_close, null) .setPositiveButton(R.string.crash_details_close, null)
.show(); .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.CustomRowPresenter;
import com.fongmi.android.tv.ui.custom.CustomSelector; import com.fongmi.android.tv.ui.custom.CustomSelector;
import com.fongmi.android.tv.ui.custom.CustomTitleView; 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.dialog.SiteDialog;
import com.fongmi.android.tv.ui.presenter.FuncPresenter; import com.fongmi.android.tv.ui.presenter.FuncPresenter;
import com.fongmi.android.tv.ui.presenter.HeaderPresenter; import com.fongmi.android.tv.ui.presenter.HeaderPresenter;
@@ -244,7 +245,24 @@ public class HomeActivity extends BaseActivity implements CustomTitleView.Listen
} }
private void getHistory(boolean renew) { private void getHistory(boolean renew) {
List<History> items = History.get(); // 获取所有视频源的观看记录(最近60天)
List<History> items = History.getAll();
com.github.catvod.utils.Logger.d("HomeActivity: 获取观看记录,共 " + items.size() + "");
// 对比一下数据库中所有记录
List<com.fongmi.android.tv.bean.History> allInDb = com.fongmi.android.tv.db.AppDatabase.get().getHistoryDao().findAllRecent(0);
com.github.catvod.utils.Logger.d("HomeActivity: 数据库总记录数: " + allInDb.size() + " 条(包含所有时间)");
if (items.size() < allInDb.size()) {
com.github.catvod.utils.Logger.w("HomeActivity: 有 " + (allInDb.size() - items.size()) + " 条记录因为时间过滤被隐藏");
}
for (History h : items) {
com.github.catvod.utils.Logger.d("HomeActivity: 记录 - " + h.getVodName() +
" (cid=" + h.getCid() +
", createTime=" + h.getCreateTime() + ")");
}
int historyIndex = getHistoryIndex(); int historyIndex = getHistoryIndex();
int recommendIndex = getRecommendIndex(); int recommendIndex = getRecommendIndex();
boolean exist = recommendIndex - historyIndex == 2; boolean exist = recommendIndex - historyIndex == 2;
@@ -252,6 +270,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.removeItems(historyIndex, 1);
if ((!items.isEmpty() && !exist) || (renew && exist)) mAdapter.add(historyIndex, new ListRow(mHistoryAdapter)); if ((!items.isEmpty() && !exist) || (renew && exist)) mAdapter.add(historyIndex, new ListRow(mHistoryAdapter));
mHistoryAdapter.setItems(items, null); 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) { private void setHistoryDelete(boolean delete) {
@@ -3,6 +3,7 @@ package com.fongmi.android.tv.ui.activity;
import android.Manifest; import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.text.TextUtils;
import android.view.View; import android.view.View;
import androidx.viewbinding.ViewBinding; import androidx.viewbinding.ViewBinding;
@@ -35,11 +36,13 @@ import com.fongmi.android.tv.ui.dialog.LiveDialog;
import com.fongmi.android.tv.ui.dialog.ProxyDialog; import com.fongmi.android.tv.ui.dialog.ProxyDialog;
import com.fongmi.android.tv.ui.dialog.RestoreDialog; import com.fongmi.android.tv.ui.dialog.RestoreDialog;
import com.fongmi.android.tv.ui.dialog.SiteDialog; import com.fongmi.android.tv.ui.dialog.SiteDialog;
import com.fongmi.android.tv.ui.dialog.WebDAVDialog;
import com.fongmi.android.tv.utils.FileChooser; import com.fongmi.android.tv.utils.FileChooser;
import com.fongmi.android.tv.utils.FileUtil; import com.fongmi.android.tv.utils.FileUtil;
import com.fongmi.android.tv.utils.Notify; import com.fongmi.android.tv.utils.Notify;
import com.fongmi.android.tv.utils.ResUtil; import com.fongmi.android.tv.utils.ResUtil;
import com.fongmi.android.tv.utils.UrlUtil; import com.fongmi.android.tv.utils.UrlUtil;
import com.fongmi.android.tv.utils.WebDAVSyncManager;
import com.github.catvod.bean.Doh; import com.github.catvod.bean.Doh;
import com.github.catvod.net.OkHttp; import com.github.catvod.net.OkHttp;
import com.github.catvod.utils.Path; import com.github.catvod.utils.Path;
@@ -99,8 +102,40 @@ public class SettingActivity extends BaseActivity implements ConfigCallback, Sit
mBinding.dohText.setText(getDohList()[getDohIndex()]); mBinding.dohText.setText(getDohList()[getDohIndex()]);
mBinding.proxyText.setText(getProxy(Setting.getProxy())); mBinding.proxyText.setText(getProxy(Setting.getProxy()));
mBinding.incognitoText.setText(getSwitch(Setting.isIncognito())); 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.sizeText.setText((size = ResUtil.getStringArray(R.array.select_size))[Setting.getSize()]);
mBinding.qualityText.setText((quality = ResUtil.getStringArray(R.array.select_quality))[Setting.getQuality()]); mBinding.qualityText.setText((quality = ResUtil.getStringArray(R.array.select_quality))[Setting.getQuality()]);
setWebDAVStatus();
setLiveSettingsVisibility();
}
private void setWebDAVStatus() {
WebDAVSyncManager manager = WebDAVSyncManager.get();
if (manager.isConfigured()) {
// 显示账号昵称(用户名)
String username = Setting.getWebDAVUsername();
if (!TextUtils.isEmpty(username)) {
// 如果用户名是邮箱,只显示@前面的部分
String displayName = username;
if (username.contains("@")) {
displayName = username.substring(0, username.indexOf("@"));
}
String status = Setting.isWebDAVAutoSync() ? displayName + "(自动同步)" : displayName;
mBinding.webdavStatusText.setText(status);
} else {
String status = Setting.isWebDAVAutoSync() ? "已配置(自动同步)" : "已配置";
mBinding.webdavStatusText.setText(status);
}
} else {
mBinding.webdavStatusText.setText("未配置");
}
}
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() { private void setCacheText() {
@@ -134,9 +169,11 @@ public class SettingActivity extends BaseActivity implements ConfigCallback, Sit
mBinding.wallDefault.setOnClickListener(this::setWallDefault); mBinding.wallDefault.setOnClickListener(this::setWallDefault);
mBinding.wallRefresh.setOnClickListener(this::setWallRefresh); mBinding.wallRefresh.setOnClickListener(this::setWallRefresh);
mBinding.incognito.setOnClickListener(this::setIncognito); mBinding.incognito.setOnClickListener(this::setIncognito);
mBinding.liveTabVisible.setOnClickListener(this::setLiveTabVisible);
mBinding.quality.setOnClickListener(this::setQuality); mBinding.quality.setOnClickListener(this::setQuality);
mBinding.size.setOnClickListener(this::setSize); mBinding.size.setOnClickListener(this::setSize);
mBinding.doh.setOnClickListener(this::setDoh); mBinding.doh.setOnClickListener(this::setDoh);
mBinding.webdav.setOnClickListener(this::onWebDAV);
} }
@Override @Override
@@ -304,6 +341,15 @@ public class SettingActivity extends BaseActivity implements ConfigCallback, Sit
mBinding.incognitoText.setText(getSwitch(Setting.isIncognito())); 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) { private void setQuality(View view) {
int index = Setting.getQuality(); int index = Setting.getQuality();
Setting.putQuality(index = index == quality.length - 1 ? 0 : ++index); Setting.putQuality(index = index == quality.length - 1 ? 0 : ++index);
@@ -386,12 +432,33 @@ public class SettingActivity extends BaseActivity implements ConfigCallback, Sit
})); }));
} }
private void onWebDAV(View view) {
WebDAVDialog.create(this).show();
}
private void initConfig() { private void initConfig() {
WallConfig.get().init(); WallConfig.get().init();
LiveConfig.get().init().load(); LiveConfig.get().init().load();
VodConfig.get().init().load(getCallback(0)); VodConfig.get().init().load(getCallback(0));
} }
@Override
public void onRefreshEvent(RefreshEvent event) {
super.onRefreshEvent(event);
if (event.getType() == RefreshEvent.Type.CONFIG) {
setWebDAVStatus();
}
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
// 当Activity重新获得焦点时,更新WebDAV状态(例如从对话框返回后)
setWebDAVStatus();
}
}
@Override @Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
@@ -20,6 +20,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import androidx.leanback.widget.ArrayObjectAdapter; import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.ItemBridgeAdapter; import androidx.leanback.widget.ItemBridgeAdapter;
@@ -427,8 +428,11 @@ public class VideoActivity extends BaseActivity implements CustomKeyDownVod.List
private void setDetail(Result result) { private void setDetail(Result result) {
if (result.getList().isEmpty()) setEmpty(result.hasMsg()); if (result.getList().isEmpty()) setEmpty(result.hasMsg());
else setDetail(result.getList().get(0)); else setDetail(result.getList().get(0));
// 只在有错误或重要消息时显示提示
if (result.hasMsg() && result.getList().isEmpty()) {
Notify.show(result.getMsg()); Notify.show(result.getMsg());
} }
}
private void setEmpty(boolean finish) { private void setEmpty(boolean finish) {
if (isFromCollect() || finish) { if (isFromCollect() || finish) {
@@ -481,7 +485,7 @@ public class VideoActivity extends BaseActivity implements CustomKeyDownVod.List
private void setText(TextView view, int resId, String text) { private void setText(TextView view, int resId, String text) {
view.setText(getSpan(resId, text), TextView.BufferType.SPANNABLE); view.setText(getSpan(resId, text), TextView.BufferType.SPANNABLE);
view.setVisibility(text.isEmpty() ? View.GONE : View.VISIBLE); 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); CustomMovement.bind(view);
view.setTag(text); view.setTag(text);
} }
@@ -1224,7 +1228,10 @@ public class VideoActivity extends BaseActivity implements CustomKeyDownVod.List
private void nextSite() { private void nextSite() {
if (mQuickAdapter.size() == 0) return; if (mQuickAdapter.size() == 0) return;
Vod item = (Vod) mQuickAdapter.get(0); Vod item = (Vod) mQuickAdapter.get(0);
// 只在真正需要切换时显示提示(即当前站点已经失败的情况下)
if (mBroken.contains(getId())) {
Notify.show(getString(R.string.play_switch_site, item.getSiteName())); Notify.show(getString(R.string.play_switch_site, item.getSiteName()));
}
mQuickAdapter.removeItems(0, 1); mQuickAdapter.removeItems(0, 1);
mBroken.add(getId()); mBroken.add(getId());
setInitAuto(false); setInitAuto(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;
}
}
}
@@ -73,6 +73,8 @@ public class ConfigDialog implements DialogInterface.OnDismissListener {
params.width = (int) (ResUtil.getScreenWidth() * 0.55f); params.width = (int) (ResUtil.getScreenWidth() * 0.55f);
dialog.getWindow().setAttributes(params); dialog.getWindow().setAttributes(params);
dialog.getWindow().setDimAmount(0); dialog.getWindow().setDimAmount(0);
// 设置对话框背景为透明,让布局的深色背景显示
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
dialog.setOnDismissListener(this); dialog.setOnDismissListener(this);
dialog.show(); dialog.show();
} }
@@ -5,7 +5,9 @@ import android.view.LayoutInflater;
import android.widget.TextView; import android.widget.TextView;
import androidx.appcompat.app.AlertDialog; 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.databinding.DialogDescBinding;
import com.fongmi.android.tv.ui.custom.CustomMovement; import com.fongmi.android.tv.ui.custom.CustomMovement;
import com.github.bassaer.library.MDColor; import com.github.bassaer.library.MDColor;
@@ -21,13 +23,15 @@ public class DescDialog {
DialogDescBinding binding = DialogDescBinding.inflate(LayoutInflater.from(activity)); DialogDescBinding binding = DialogDescBinding.inflate(LayoutInflater.from(activity));
AlertDialog dialog = new MaterialAlertDialogBuilder(activity).setView(binding.getRoot()).create(); AlertDialog dialog = new MaterialAlertDialogBuilder(activity).setView(binding.getRoot()).create();
dialog.getWindow().setDimAmount(0); dialog.getWindow().setDimAmount(0);
initView(binding.text, desc); // 设置对话框背景为透明,让布局的深色背景显示
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
initView(binding.text, desc, activity);
dialog.show(); 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.setText(desc, TextView.BufferType.SPANNABLE);
view.setLinkTextColor(MDColor.BLUE_500); view.setLinkTextColor(ContextCompat.getColor(activity, R.color.primary));
CustomMovement.bind(view); CustomMovement.bind(view);
} }
} }
@@ -55,6 +55,8 @@ public class DohDialog implements DohAdapter.OnClickListener {
params.width = (int) (ResUtil.getScreenWidth() * 0.4f); params.width = (int) (ResUtil.getScreenWidth() * 0.4f);
dialog.getWindow().setAttributes(params); dialog.getWindow().setAttributes(params);
dialog.getWindow().setDimAmount(0); dialog.getWindow().setDimAmount(0);
// 设置对话框背景为透明,让布局的深色背景显示
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
dialog.show(); dialog.show();
} }
@@ -56,6 +56,8 @@ public class HistoryDialog implements ConfigAdapter.OnClickListener {
params.width = (int) (ResUtil.getScreenWidth() * 0.4f); params.width = (int) (ResUtil.getScreenWidth() * 0.4f);
dialog.getWindow().setAttributes(params); dialog.getWindow().setAttributes(params);
dialog.getWindow().setDimAmount(0); dialog.getWindow().setDimAmount(0);
// 设置对话框背景为透明,让布局的深色背景显示
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
dialog.show(); dialog.show();
} }
@@ -57,6 +57,8 @@ public class LiveDialog implements LiveAdapter.OnClickListener {
params.width = (int) (ResUtil.getScreenWidth() * 0.4f); params.width = (int) (ResUtil.getScreenWidth() * 0.4f);
dialog.getWindow().setAttributes(params); dialog.getWindow().setAttributes(params);
dialog.getWindow().setDimAmount(0); dialog.getWindow().setDimAmount(0);
// 设置对话框背景为透明,让布局的深色背景显示
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
dialog.show(); dialog.show();
} }
@@ -54,6 +54,8 @@ public class ProxyDialog implements DialogInterface.OnDismissListener {
params.width = (int) (ResUtil.getScreenWidth() * 0.55f); params.width = (int) (ResUtil.getScreenWidth() * 0.55f);
dialog.getWindow().setAttributes(params); dialog.getWindow().setAttributes(params);
dialog.getWindow().setDimAmount(0); dialog.getWindow().setDimAmount(0);
// 设置对话框背景为透明,让布局的深色背景显示
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
dialog.setOnDismissListener(this); dialog.setOnDismissListener(this);
dialog.show(); dialog.show();
} }
@@ -52,6 +52,8 @@ public class RestoreDialog implements RestoreAdapter.OnClickListener {
params.width = (int) (ResUtil.getScreenWidth() * 0.4f); params.width = (int) (ResUtil.getScreenWidth() * 0.4f);
dialog.getWindow().setAttributes(params); dialog.getWindow().setAttributes(params);
dialog.getWindow().setDimAmount(0); dialog.getWindow().setDimAmount(0);
// 设置对话框背景为透明,让布局的深色背景显示
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
dialog.show(); dialog.show();
} }
@@ -55,6 +55,7 @@ public class SiteDialog implements SiteAdapter.OnClickListener {
setType(type); setType(type);
initView(); initView();
initEvent(); initEvent();
setDialog();
} }
private boolean list() { private boolean list() {
@@ -94,7 +95,13 @@ public class SiteDialog implements SiteAdapter.OnClickListener {
if (decoration != null) binding.recycler.removeItemDecoration(decoration); if (decoration != null) binding.recycler.removeItemDecoration(decoration);
binding.recycler.addItemDecoration(decoration = new SpaceItemDecoration(getCount(), 16)); binding.recycler.addItemDecoration(decoration = new SpaceItemDecoration(getCount(), 16));
binding.recycler.setLayoutManager(new GridLayoutManager(dialog.getContext(), getCount())); 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() { private void setDialog() {
@@ -103,6 +110,8 @@ public class SiteDialog implements SiteAdapter.OnClickListener {
params.width = (int) (ResUtil.getScreenWidth() * getWidth()); params.width = (int) (ResUtil.getScreenWidth() * getWidth());
dialog.getWindow().setAttributes(params); dialog.getWindow().setAttributes(params);
dialog.getWindow().setDimAmount(0); dialog.getWindow().setDimAmount(0);
// 设置对话框背景为透明,让布局的深色背景显示
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
dialog.show(); dialog.show();
} }
@@ -55,6 +55,8 @@ public class UaDialog implements DialogInterface.OnDismissListener {
params.width = (int) (ResUtil.getScreenWidth() * 0.55f); params.width = (int) (ResUtil.getScreenWidth() * 0.55f);
dialog.getWindow().setAttributes(params); dialog.getWindow().setAttributes(params);
dialog.getWindow().setDimAmount(0); dialog.getWindow().setDimAmount(0);
// 设置对话框背景为透明让布局的深色背景显示
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
dialog.setOnDismissListener(this); dialog.setOnDismissListener(this);
dialog.show(); dialog.show();
} }
@@ -0,0 +1,637 @@
package com.fongmi.android.tv.ui.dialog;
import android.content.DialogInterface;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.Editable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentActivity;
import com.fongmi.android.tv.App;
import com.fongmi.android.tv.R;
import com.fongmi.android.tv.Setting;
import com.fongmi.android.tv.databinding.DialogWebdavBinding;
import com.fongmi.android.tv.event.RefreshEvent;
import com.fongmi.android.tv.utils.Notify;
import com.fongmi.android.tv.utils.ResUtil;
import com.fongmi.android.tv.utils.WebDAVSyncManager;
import com.github.catvod.utils.Logger;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
public class WebDAVDialog {
// 预设的WebDAV服务提供商
private static final String[] PROVIDERS = {
"坚果云",
"Nextcloud",
"ownCloud",
"自定义"
};
private static final String[] PROVIDER_URLS = {
"https://dav.jianguoyun.com/dav/XMBOX/", // 坚果云添加XMBOX子目录方便在网页版查看
"", // Nextcloud需要用户输入
"", // ownCloud需要用户输入
"" // 自定义需要用户输入
};
private final DialogWebdavBinding binding;
private final FragmentActivity activity;
private AlertDialog dialog;
private WebDAVSyncManager syncManager;
private int selectedProvider = 0; // 默认选择坚果云
private boolean isInitializing = false; // 标记是否正在初始化防止初始化时触发监听器
private Handler statusHandler = new Handler(Looper.getMainLooper());
private Runnable hideStatusRunnable; // 用于延迟隐藏状态消息
public static WebDAVDialog create(FragmentActivity activity) {
return new WebDAVDialog(activity);
}
public WebDAVDialog(FragmentActivity activity) {
this.activity = activity;
this.binding = DialogWebdavBinding.inflate(LayoutInflater.from(activity));
this.syncManager = WebDAVSyncManager.get();
}
public void show() {
initDialog();
initView();
initEvent();
}
private void initDialog() {
dialog = new MaterialAlertDialogBuilder(activity)
.setView(binding.getRoot())
.create();
// 设置对话框大小适合TV屏幕
WindowManager.LayoutParams params = dialog.getWindow().getAttributes();
params.width = (int) (ResUtil.getScreenWidth() * 0.45f);
dialog.getWindow().setAttributes(params);
dialog.getWindow().setDimAmount(0);
// 设置对话框背景为透明让布局的深色背景显示
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
dialog.show();
}
private void initView() {
isInitializing = true; // 标记开始初始化
// 加载已保存的配置
String url = Setting.getWebDAVUrl();
String username = Setting.getWebDAVUsername();
String password = Setting.getWebDAVPassword();
boolean autoSync = Setting.isWebDAVAutoSync();
int interval = Setting.getWebDAVSyncInterval();
// 根据保存的URL判断是哪个服务提供商
selectedProvider = getProviderIndexByUrl(url);
binding.providerText.setText(PROVIDERS[selectedProvider]);
// 根据选择的服务提供商决定是否显示URL输入框
if (selectedProvider == PROVIDERS.length - 1) {
// 自定义显示URL输入框
binding.urlInput.setVisibility(View.VISIBLE);
binding.urlText.setText(url);
if (!TextUtils.isEmpty(url)) {
binding.urlText.setSelection(url.length());
}
} else if (selectedProvider == 0) {
// 坚果云永远隐藏输入框有预设URL
binding.urlInput.setVisibility(View.GONE);
} else {
// Nextcloud或ownCloud需要用户输入URL
binding.urlInput.setVisibility(View.VISIBLE);
binding.urlText.setText(url);
if (!TextUtils.isEmpty(url)) {
binding.urlText.setSelection(url.length());
}
}
binding.usernameText.setText(username);
binding.passwordText.setText(password);
binding.autoSyncSwitch.setChecked(autoSync);
binding.syncIntervalText.setText(String.valueOf(interval));
// 根据自动同步开关显示/隐藏同步间隔
updateSyncIntervalVisibility(autoSync);
isInitializing = false; // 初始化完成
}
/**
* 根据URL判断是哪个服务提供商
*/
private int getProviderIndexByUrl(String url) {
if (TextUtils.isEmpty(url)) {
return 0; // 默认坚果云
}
if (url.contains("jianguoyun.com")) {
return 0; // 坚果云
}
if (url.contains("nextcloud")) {
return 1; // Nextcloud
}
if (url.contains("owncloud")) {
return 2; // ownCloud
}
return PROVIDERS.length - 1; // 自定义
}
/**
* 获取当前选择的服务提供商的URL
*/
private String getProviderUrl() {
if (selectedProvider < PROVIDER_URLS.length && !TextUtils.isEmpty(PROVIDER_URLS[selectedProvider])) {
return PROVIDER_URLS[selectedProvider];
}
return "";
}
private void initEvent() {
// 服务提供商选择
binding.providerText.setOnClickListener(v -> onSelectProvider());
// 自动同步开关监听立即保存状态
// 使用setOnClickListener而不是setOnCheckedChangeListener避免覆盖CustomSwitch内部的动画监听器
// AppCompatCheckBox会自动处理状态切换我们只需要在状态切换后获取新状态
binding.autoSyncSwitch.setOnClickListener(v -> {
// 防止初始化时触发监听器
if (isInitializing) {
return;
}
// 使用post()确保在状态切换后获取新状态
binding.autoSyncSwitch.post(() -> {
boolean newState = binding.autoSyncSwitch.isChecked();
// 立即保存自动同步状态
Setting.putWebDAVAutoSync(newState);
// 更新同步间隔的可见性
updateSyncIntervalVisibility(newState);
});
});
// 测试连接按钮
binding.testButton.setOnClickListener(v -> onTestConnection());
// 立即同步按钮
binding.syncButton.setOnClickListener(v -> onSyncNow());
// 同步间隔点击弹出选择对话框
binding.syncIntervalContainer.setOnClickListener(v -> onSelectInterval());
// 保存按钮
binding.positive.setOnClickListener(v -> onPositive(null, 0));
// 取消按钮
binding.negative.setOnClickListener(v -> onNegative(null, 0));
// 密码输入框回车键
binding.passwordText.setOnEditorActionListener((textView, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE) {
binding.positive.performClick();
return true;
}
return false;
});
// 监听输入框内容变化清除状态提示
TextWatcher clearStatusWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
clearStatus();
}
@Override
public void afterTextChanged(Editable s) {}
};
binding.urlText.addTextChangedListener(clearStatusWatcher);
binding.usernameText.addTextChangedListener(clearStatusWatcher);
binding.passwordText.addTextChangedListener(clearStatusWatcher);
}
private void onSelectProvider() {
AlertDialog providerDialog = new MaterialAlertDialogBuilder(activity)
.setTitle("选择服务提供商")
.setSingleChoiceItems(PROVIDERS, selectedProvider, (dialog, which) -> {
selectedProvider = which;
binding.providerText.setText(PROVIDERS[which]);
// 如果是自定义显示URL输入框
if (which == PROVIDERS.length - 1) {
binding.urlInput.setVisibility(View.VISIBLE);
String currentUrl = binding.urlText.getText().toString().trim();
if (TextUtils.isEmpty(currentUrl)) {
binding.urlText.setText("");
}
} else {
// 使用预设的URL
binding.urlInput.setVisibility(View.GONE);
String providerUrl = getProviderUrl();
if (!TextUtils.isEmpty(providerUrl)) {
// URL会在保存时自动填充
} else {
// Nextcloud或ownCloud需要用户输入URL
binding.urlInput.setVisibility(View.VISIBLE);
binding.urlText.setHint("请输入" + PROVIDERS[which] + "服务器地址");
}
}
dialog.dismiss();
})
.setNegativeButton("取消", null)
.create();
// 设置对话框深色背景
providerDialog.getWindow().setBackgroundDrawableResource(R.color.black_90);
providerDialog.getWindow().setDimAmount(0);
providerDialog.show();
// 设置标题和按钮文字颜色为白色
setDialogTextColor(providerDialog, R.color.white);
// 设置列表项文字颜色为白色使用 post 确保在列表渲染后设置
android.widget.ListView listView = providerDialog.getListView();
if (listView != null) {
listView.post(() -> {
for (int i = 0; i < listView.getChildCount(); i++) {
View itemView = listView.getChildAt(i);
setTextViewColorRecursive(itemView, R.color.white);
}
});
// 监听列表滚动确保新显示的项目也是白色
listView.setOnScrollListener(new android.widget.AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(android.widget.AbsListView view, int scrollState) {
if (scrollState == android.widget.AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
for (int i = 0; i < view.getChildCount(); i++) {
setTextViewColorRecursive(view.getChildAt(i), R.color.white);
}
}
}
@Override
public void onScroll(android.widget.AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
});
}
}
private void updateSyncIntervalVisibility(boolean visible) {
binding.syncIntervalContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
}
/**
* 递归设置 View 及其子 View 中所有 TextView 的文字颜色
*/
private void setTextViewColorRecursive(View view, int colorResId) {
if (view == null) return;
if (view instanceof android.widget.TextView) {
((android.widget.TextView) view).setTextColor(activity.getResources().getColor(colorResId));
} else if (view instanceof android.view.ViewGroup) {
android.view.ViewGroup group = (android.view.ViewGroup) view;
for (int i = 0; i < group.getChildCount(); i++) {
setTextViewColorRecursive(group.getChildAt(i), colorResId);
}
}
}
/**
* 设置对话框中的标题和按钮文字颜色
*/
private void setDialogTextColor(AlertDialog dialog, int colorResId) {
if (dialog == null) return;
int color = activity.getResources().getColor(colorResId);
// 设置标题文字颜色
int titleId = activity.getResources().getIdentifier("alertTitle", "id", "android");
if (titleId != 0) {
View titleView = dialog.findViewById(titleId);
if (titleView instanceof android.widget.TextView) {
((android.widget.TextView) titleView).setTextColor(color);
}
}
// 使用 post 延迟设置按钮文字颜色按钮可能在显示后才创建
dialog.getWindow().getDecorView().post(() -> {
android.widget.Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
if (negativeButton != null) {
negativeButton.setTextColor(color);
}
});
}
private void onTestConnection() {
String url = getServerUrl();
String username = binding.usernameText.getText().toString().trim();
String password = binding.passwordText.getText().toString().trim();
if (TextUtils.isEmpty(url)) {
showStatus("请选择服务提供商或输入服务器地址", false);
return;
}
if (TextUtils.isEmpty(username)) {
showStatus("请输入用户名", false);
return;
}
if (TextUtils.isEmpty(password)) {
showStatus("请输入密码", false);
return;
}
// 临时保存配置用于测试
Setting.putWebDAVUrl(url);
Setting.putWebDAVUsername(username);
Setting.putWebDAVPassword(password);
syncManager.reloadConfig();
showStatus("正在测试连接...", true);
binding.testButton.setEnabled(false);
App.execute(() -> {
WebDAVSyncManager.TestResult result = syncManager.testConnectionWithMessage();
App.post(() -> {
// 检查对话框是否还存在
if (binding == null || dialog == null || !dialog.isShowing()) {
return;
}
binding.testButton.setEnabled(true);
showStatus(result.message, result.success);
if (!result.success) {
// 显示详细错误信息
Logger.e("WebDAV测试连接失败: " + result.message);
}
});
});
}
private void onSyncNow() {
// 先临时保存当前配置用于测试同步
String url = getServerUrl();
String username = binding.usernameText.getText().toString().trim();
String password = binding.passwordText.getText().toString().trim();
// 验证输入
if (TextUtils.isEmpty(url)) {
showStatus("请选择服务提供商或输入服务器地址", false);
return;
}
if (TextUtils.isEmpty(username)) {
showStatus("请输入用户名", false);
return;
}
if (TextUtils.isEmpty(password)) {
showStatus("请输入密码", false);
return;
}
// 临时保存配置用于同步
Setting.putWebDAVUrl(url);
Setting.putWebDAVUsername(username);
Setting.putWebDAVPassword(password);
syncManager.reloadConfig();
if (!syncManager.isConfigured()) {
showStatus("配置无效,无法同步", false);
return;
}
showStatus("正在同步...", true);
binding.syncButton.setEnabled(false);
// 在后台线程执行同步
App.execute(() -> {
try {
// 先上传本地记录
syncManager.uploadHistory();
// 再下载远程记录并合并
boolean downloadSuccess = syncManager.downloadHistory();
App.post(() -> {
// 检查对话框是否还存在
if (binding == null || dialog == null || !dialog.isShowing()) {
return;
}
binding.syncButton.setEnabled(true);
if (downloadSuccess) {
showStatus("同步完成", true);
Notify.show("同步完成");
} else {
showStatus("同步完成(本地数据已上传)", true);
Notify.show("同步完成");
}
});
} catch (Exception e) {
App.post(() -> {
// 检查对话框是否还存在
if (binding == null || dialog == null || !dialog.isShowing()) {
return;
}
binding.syncButton.setEnabled(true);
showStatus("同步失败:" + e.getMessage(), false);
Notify.show("同步失败");
Logger.e("WebDAV: 同步失败: " + e.getMessage());
});
}
});
}
private void onSelectInterval() {
String[] intervals = {"15", "30", "60", "120", "240"};
int currentInterval = Setting.getWebDAVSyncInterval();
int selectedIndex = 0;
for (int i = 0; i < intervals.length; i++) {
if (Integer.parseInt(intervals[i]) == currentInterval) {
selectedIndex = i;
break;
}
}
AlertDialog intervalDialog = new MaterialAlertDialogBuilder(activity)
.setTitle("选择同步间隔")
.setSingleChoiceItems(intervals, selectedIndex, (dialog, which) -> {
int interval = Integer.parseInt(intervals[which]);
binding.syncIntervalText.setText(String.valueOf(interval));
// 立即保存同步间隔
Setting.putWebDAVSyncInterval(interval);
dialog.dismiss();
})
.setNegativeButton("取消", null)
.create();
// 设置对话框深色背景
intervalDialog.getWindow().setBackgroundDrawableResource(R.color.black_90);
intervalDialog.getWindow().setDimAmount(0);
intervalDialog.show();
// 设置标题和按钮文字颜色为白色
setDialogTextColor(intervalDialog, R.color.white);
// 设置列表项文字颜色为白色使用 post 确保在列表渲染后设置
android.widget.ListView listView = intervalDialog.getListView();
if (listView != null) {
listView.post(() -> {
for (int i = 0; i < listView.getChildCount(); i++) {
View itemView = listView.getChildAt(i);
setTextViewColorRecursive(itemView, R.color.white);
}
});
// 监听列表滚动确保新显示的项目也是白色
listView.setOnScrollListener(new android.widget.AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(android.widget.AbsListView view, int scrollState) {
if (scrollState == android.widget.AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
for (int i = 0; i < view.getChildCount(); i++) {
setTextViewColorRecursive(view.getChildAt(i), R.color.white);
}
}
}
@Override
public void onScroll(android.widget.AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
});
}
}
private void showStatus(String message, boolean isSuccess) {
// 检查对话框是否还存在
if (binding == null || dialog == null || !dialog.isShowing()) {
return;
}
// 取消之前的隐藏任务
if (hideStatusRunnable != null) {
statusHandler.removeCallbacks(hideStatusRunnable);
hideStatusRunnable = null;
}
binding.statusText.setText(message);
binding.statusText.setVisibility(TextUtils.isEmpty(message) ? View.GONE : View.VISIBLE);
binding.statusText.setTextColor(isSuccess ?
activity.getResources().getColor(R.color.white) :
activity.getResources().getColor(android.R.color.holo_red_dark));
// 3秒后自动隐藏状态消息
if (!TextUtils.isEmpty(message)) {
hideStatusRunnable = () -> clearStatus();
statusHandler.postDelayed(hideStatusRunnable, 3000);
}
}
/**
* 清除状态提示
*/
private void clearStatus() {
// 检查对话框是否还存在
if (binding == null || dialog == null || !dialog.isShowing()) {
return;
}
if (hideStatusRunnable != null) {
statusHandler.removeCallbacks(hideStatusRunnable);
hideStatusRunnable = null;
}
binding.statusText.setText("");
binding.statusText.setVisibility(View.GONE);
}
/**
* 获取服务器URL根据选择的服务提供商
*/
private String getServerUrl() {
if (selectedProvider == PROVIDERS.length - 1) {
// 自定义从输入框获取
return binding.urlText.getText().toString().trim();
} else {
// 使用预设URL或从输入框获取Nextcloud/ownCloud
String providerUrl = getProviderUrl();
if (!TextUtils.isEmpty(providerUrl)) {
return providerUrl;
} else {
// Nextcloud或ownCloud需要用户输入
return binding.urlText.getText().toString().trim();
}
}
}
private void onPositive(DialogInterface dialog, int which) {
String url = getServerUrl();
String username = binding.usernameText.getText().toString().trim();
String password = binding.passwordText.getText().toString().trim();
boolean autoSync = binding.autoSyncSwitch.isChecked();
int interval = Integer.parseInt(binding.syncIntervalText.getText().toString());
// 验证输入
if (TextUtils.isEmpty(url)) {
Notify.show("请选择服务提供商或输入服务器地址");
return;
}
if (TextUtils.isEmpty(username)) {
Notify.show("请输入用户名");
return;
}
if (TextUtils.isEmpty(password)) {
Notify.show("请输入密码");
return;
}
// 保存配置
Setting.putWebDAVUrl(url);
Setting.putWebDAVUsername(username);
Setting.putWebDAVPassword(password);
Setting.putWebDAVAutoSync(autoSync);
Setting.putWebDAVSyncInterval(interval);
// 重新加载配置
syncManager.reloadConfig();
// 配置保存后立即执行一次同步下载远程数据
// 这样新设备配置后就能立即看到其他设备的历史记录
if (syncManager.isConfigured()) {
Notify.show("WebDAV配置已保存,正在同步数据...");
App.execute(() -> {
try {
// 先上传本地记录
syncManager.uploadHistory();
// 再下载远程记录并合并
boolean downloadSuccess = syncManager.downloadHistory();
App.post(() -> {
if (downloadSuccess) {
Notify.show("同步完成,已获取远程观看记录");
} else {
Notify.show("同步完成(本地数据已上传)");
}
});
} catch (Exception e) {
App.post(() -> {
Notify.show("同步失败,请检查网络连接");
});
}
});
} else {
Notify.show("WebDAV配置已保存");
}
clearStatus();
if (this.dialog != null) {
this.dialog.dismiss();
}
// 通知设置界面更新状态通过RefreshEvent
// 使用App.post确保对话框关闭后再发送事件让状态能及时更新
App.post(() -> RefreshEvent.config());
}
private void onNegative(DialogInterface dialog, int which) {
clearStatus();
if (this.dialog != null) {
this.dialog.dismiss();
}
}
}
@@ -39,6 +39,8 @@ public class WebDialog {
params.width = (int) (ResUtil.getScreenWidth() * 0.8f); params.width = (int) (ResUtil.getScreenWidth() * 0.8f);
dialog.getWindow().setAttributes(params); dialog.getWindow().setAttributes(params);
dialog.getWindow().setDimAmount(0); dialog.getWindow().setDimAmount(0);
// 设置对话框背景为透明让布局的深色背景显示
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
dialog.show(); dialog.show();
} }
} }
@@ -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"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <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/primary" 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_selected="true" />
<item android:color="@color/white" /> <item android:color="@color/white" />
</selector> </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"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <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/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" /> <item android:color="@color/white" />
</selector> </selector>
@@ -2,7 +2,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="@color/white_90" /> <solid android:color="@color/black_90" />
<corners <corners
android:topLeftRadius="12dp" android:topLeftRadius="12dp"
@@ -2,7 +2,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="@color/white_90" /> <solid android:color="@color/black_80" />
<corners <corners
android:topLeftRadius="8dp" android:topLeftRadius="8dp"
@@ -2,6 +2,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="@color/green_400" /> <solid android:color="@color/primary" />
</shape> </shape>
@@ -4,7 +4,7 @@
<solid android:color="@color/black_20" /> <solid android:color="@color/black_20" />
<corners android:radius="4dp" /> <corners android:radius="12dp" />
<padding <padding
android:bottom="8dp" android:bottom="8dp"
@@ -2,7 +2,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="@color/grey_600" /> <solid android:color="@color/primary" />
<corners android:radius="4dp" /> <corners android:radius="4dp" />
@@ -2,7 +2,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="@color/grey_800" /> <solid android:color="@color/primary" />
<corners android:radius="4dp" /> <corners android:radius="4dp" />
@@ -2,7 +2,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="@color/grey_600" /> <solid android:color="@color/black_60" />
<corners android:radius="4dp" /> <corners android:radius="4dp" />
@@ -2,7 +2,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="@color/black_40" /> <solid android:color="@color/black_90" />
<corners android:radius="8dp" /> <corners android:radius="8dp" />
@@ -235,6 +235,36 @@
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/webdav"
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:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="WebDAV"
android:textColor="@color/white"
android:textSize="18sp" />
<TextView
android:id="@+id/webdavStatusText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:text="未配置"
android:textColor="@color/white"
android:textSize="18sp"
android:alpha="0.7" />
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/incognito" android:id="@+id/incognito"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -265,6 +295,36 @@
</LinearLayout> </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 <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -16,7 +16,7 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:gravity="center" android:gravity="center"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="18sp" android:textSize="18sp"
tools:text="https://fongmi.github.io/cat.json" /> tools:text="https://fongmi.github.io/cat.json" />
@@ -9,6 +9,6 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/text" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
tools:text="愛奇異彈幕" /> tools:text="愛奇異彈幕" />
+1 -1
View File
@@ -9,6 +9,6 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:gravity="center" android:gravity="center"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/text" android:textColor="@color/button_text"
android:textSize="18sp" android:textSize="18sp"
tools:text="Google" /> tools:text="Google" />
@@ -12,6 +12,6 @@
android:nextFocusUp="@id/flag" android:nextFocusUp="@id/flag"
android:nextFocusDown="@id/array" android:nextFocusDown="@id/array"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/text" android:textColor="@color/episode_text"
android:textSize="16sp" android:textSize="16sp"
tools:text="20" /> tools:text="20" />
+1 -1
View File
@@ -16,7 +16,7 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:gravity="center" android:gravity="center"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/text" android:textColor="@color/button_text"
android:textSize="18sp" android:textSize="18sp"
tools:text="https://fongmi.github.io/live.json" /> tools:text="https://fongmi.github.io/live.json" />
@@ -8,6 +8,6 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:gravity="center" android:gravity="center"
android:textColor="@color/text" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
tools:text="解析" /> tools:text="解析" />
@@ -24,7 +24,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/green_a_400" android:textColor="@color/primary"
android:textSize="14sp" android:textSize="14sp"
tools:text="泥巴" tools:text="泥巴"
tools:visibility="visible" /> tools:visibility="visible" />
@@ -16,7 +16,7 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:gravity="center" android:gravity="center"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="18sp" android:textSize="18sp"
tools:text="tv-2024-12-26.bk.gz" /> tools:text="tv-2024-12-26.bk.gz" />
+2 -1
View File
@@ -17,8 +17,9 @@
android:ellipsize="marquee" android:ellipsize="marquee"
android:gravity="center" android:gravity="center"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/text" android:textColor="@color/button_text"
android:textSize="18sp" android:textSize="18sp"
android:duplicateParentState="true"
tools:text="泥巴" /> tools:text="泥巴" />
<com.google.android.material.checkbox.MaterialCheckBox <com.google.android.material.checkbox.MaterialCheckBox
@@ -9,6 +9,6 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/text" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
tools:text="中文、哥斯拉.srt" /> tools:text="中文、哥斯拉.srt" />
@@ -3,6 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/black_90"
android:padding="48dp"> android:padding="48dp">
<com.google.android.material.slider.Slider <com.google.android.material.slider.Slider
@@ -12,9 +13,10 @@
android:stepSize="1" android:stepSize="1"
android:valueFrom="1" android:valueFrom="1"
android:valueTo="10" android:valueTo="10"
app:thumbColor="@color/yellow_500" app:thumbColor="@color/primary"
app:thumbRadius="9dp"
app:tickVisible="false" app:tickVisible="false"
app:trackColorActive="@color/yellow_500" app:trackColorActive="@color/primary"
app:trackColorInactive="@color/yellow_50" /> app:trackColorInactive="@color/white_30" />
</FrameLayout> </FrameLayout>
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/black_90"
android:padding="16dp"> android:padding="16dp">
<ImageView <ImageView
@@ -23,7 +24,7 @@
android:maxLines="3" android:maxLines="3"
android:paddingStart="4dp" android:paddingStart="4dp"
android:paddingEnd="4dp" android:paddingEnd="4dp"
android:textColor="@color/grey_700" android:textColor="@color/white"
android:textSize="18sp" android:textSize="18sp"
tools:text="@string/push_info" /> tools:text="@string/push_info" />
@@ -41,11 +42,13 @@
android:layout_alignStart="@+id/info" android:layout_alignStart="@+id/info"
android:layout_marginBottom="10dp" android:layout_marginBottom="10dp"
android:hint="@string/dialog_config_hint" android:hint="@string/dialog_config_hint"
android:textColorHint="@color/white_50"
android:imeOptions="actionDone" android:imeOptions="actionDone"
android:importantForAutofill="no" android:importantForAutofill="no"
android:inputType="text" android:inputType="text"
android:nextFocusDown="@id/positive" android:nextFocusDown="@id/positive"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/white"
android:textSize="18sp" /> android:textSize="18sp" />
<LinearLayout <LinearLayout
@@ -68,7 +71,7 @@
android:gravity="center" android:gravity="center"
android:singleLine="true" android:singleLine="true"
android:text="@string/setting_choose" android:text="@string/setting_choose"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" /> android:textSize="14sp" />
<TextView <TextView
@@ -83,7 +86,7 @@
android:gravity="center" android:gravity="center"
android:singleLine="true" android:singleLine="true"
android:text="@string/dialog_positive" android:text="@string/dialog_positive"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" /> android:textSize="14sp" />
<TextView <TextView
@@ -97,7 +100,7 @@
android:gravity="center" android:gravity="center"
android:singleLine="true" android:singleLine="true"
android:text="@string/dialog_negative" android:text="@string/dialog_negative"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" /> android:textSize="14sp" />
</LinearLayout> </LinearLayout>
@@ -4,6 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/black_90"
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout <LinearLayout
@@ -19,7 +20,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:text="@string/danmaku_select" android:text="@string/danmaku_select"
android:textColor="@color/grey_900" android:textColor="@color/white"
android:textSize="16sp" /> android:textSize="16sp" />
<ImageView <ImageView
+2 -1
View File
@@ -2,6 +2,7 @@
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" <androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/black_90"
android:fillViewport="true"> android:fillViewport="true">
<TextView <TextView
@@ -11,7 +12,7 @@
android:letterSpacing="0.05" android:letterSpacing="0.05"
android:lineSpacingExtra="8dp" android:lineSpacingExtra="8dp"
android:padding="16dp" android:padding="16dp"
android:textColor="@color/grey_800" android:textColor="@color/white"
android:textSize="16sp" /> android:textSize="16sp" />
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
@@ -5,6 +5,7 @@
android:id="@+id/recycler" android:id="@+id/recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/black_90"
android:padding="16dp" android:padding="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:maxHeight="296dp" app:maxHeight="296dp"
@@ -4,6 +4,7 @@
android:id="@+id/recycler" android:id="@+id/recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/black_90"
android:padding="16dp" android:padding="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:maxHeight="352dp" /> app:maxHeight="352dp" />
@@ -5,6 +5,7 @@
android:id="@+id/recycler" android:id="@+id/recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/black_90"
android:padding="16dp" android:padding="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:maxHeight="296dp" app:maxHeight="296dp"
@@ -2,6 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/black_90"
android:gravity="center" android:gravity="center"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="16dp"> android:padding="16dp">
+5 -2
View File
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/black_90"
android:padding="16dp"> android:padding="16dp">
<ImageView <ImageView
@@ -35,11 +36,13 @@
android:layout_alignStart="@+id/info" android:layout_alignStart="@+id/info"
android:layout_marginBottom="10dp" android:layout_marginBottom="10dp"
android:hint="socks5://127.0.0.1:9978" android:hint="socks5://127.0.0.1:9978"
android:textColorHint="@color/white_50"
android:imeOptions="actionDone" android:imeOptions="actionDone"
android:importantForAutofill="no" android:importantForAutofill="no"
android:inputType="text" android:inputType="text"
android:nextFocusDown="@id/positive" android:nextFocusDown="@id/positive"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/white"
android:textSize="18sp" /> android:textSize="18sp" />
<LinearLayout <LinearLayout
@@ -62,7 +65,7 @@
android:gravity="center" android:gravity="center"
android:singleLine="true" android:singleLine="true"
android:text="@string/dialog_positive" android:text="@string/dialog_positive"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" /> android:textSize="14sp" />
<TextView <TextView
@@ -76,7 +79,7 @@
android:gravity="center" android:gravity="center"
android:singleLine="true" android:singleLine="true"
android:text="@string/dialog_negative" android:text="@string/dialog_negative"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" /> android:textSize="14sp" />
</LinearLayout> </LinearLayout>
@@ -4,6 +4,7 @@
android:id="@+id/recycler" android:id="@+id/recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/black_90"
android:padding="16dp" android:padding="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:maxHeight="352dp" /> app:maxHeight="352dp" />
@@ -4,6 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/black_90"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="16dp"> android:padding="16dp">
+8 -4
View File
@@ -3,6 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/black_90"
android:padding="48dp"> android:padding="48dp">
<com.google.android.material.slider.Slider <com.google.android.material.slider.Slider
@@ -12,9 +13,12 @@
android:stepSize="0.5" android:stepSize="0.5"
android:valueFrom="2" android:valueFrom="2"
android:valueTo="5" android:valueTo="5"
app:thumbColor="@color/yellow_500" app:thumbColor="@color/primary"
app:tickVisible="false" app:thumbRadius="10dp"
app:trackColorActive="@color/yellow_500" app:tickVisible="true"
app:trackColorInactive="@color/yellow_50" /> app:tickColor="@color/black_50"
app:trackColorActive="@color/primary"
app:trackColorInactive="@color/white_20"
app:trackHeight="4dp" />
</FrameLayout> </FrameLayout>
@@ -2,6 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/black_90"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="16dp"> android:padding="16dp">
+2 -1
View File
@@ -4,6 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/black_90"
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout <LinearLayout
@@ -18,7 +19,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:textColor="@color/grey_900" android:textColor="@color/white"
android:textSize="16sp" android:textSize="16sp"
tools:text="選擇字幕" /> tools:text="選擇字幕" />
+5 -2
View File
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/black_90"
android:padding="16dp"> android:padding="16dp">
<ImageView <ImageView
@@ -35,11 +36,13 @@
android:layout_alignStart="@+id/info" android:layout_alignStart="@+id/info"
android:layout_marginBottom="10dp" android:layout_marginBottom="10dp"
android:hint="@string/player_ua" android:hint="@string/player_ua"
android:textColorHint="@color/white_50"
android:imeOptions="actionDone" android:imeOptions="actionDone"
android:importantForAutofill="no" android:importantForAutofill="no"
android:inputType="text" android:inputType="text"
android:nextFocusDown="@id/positive" android:nextFocusDown="@id/positive"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/white"
android:textSize="18sp" /> android:textSize="18sp" />
<LinearLayout <LinearLayout
@@ -62,7 +65,7 @@
android:gravity="center" android:gravity="center"
android:singleLine="true" android:singleLine="true"
android:text="@string/dialog_positive" android:text="@string/dialog_positive"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" /> android:textSize="14sp" />
<TextView <TextView
@@ -76,7 +79,7 @@
android:gravity="center" android:gravity="center"
android:singleLine="true" android:singleLine="true"
android:text="@string/dialog_negative" android:text="@string/dialog_negative"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" /> android:textSize="14sp" />
</LinearLayout> </LinearLayout>
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/black_90"
android:orientation="vertical" android:orientation="vertical"
android:padding="24dp"> android:padding="24dp">
@@ -11,7 +12,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:letterSpacing="0.02" android:letterSpacing="0.02"
android:textColor="@color/grey_900" android:textColor="@color/white"
android:textSize="18sp" android:textSize="18sp"
tools:text="@string/update_version" /> tools:text="@string/update_version" />
@@ -34,7 +35,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:letterSpacing="0.02" android:letterSpacing="0.02"
android:lineSpacingExtra="8dp" android:lineSpacingExtra="8dp"
android:textColor="@color/grey_900" android:textColor="@color/button_text"
android:textSize="16sp" android:textSize="16sp"
tools:text="1. 新增 ffmpeg 音頻軟解\n2. 詳情頁新增分詞快搜\n3. 修復搜尋閃退問題\n4. 設定支援渲染切換" /> tools:text="1. 新增 ffmpeg 音頻軟解\n2. 詳情頁新增分詞快搜\n3. 修復搜尋閃退問題\n4. 設定支援渲染切換" />
@@ -56,7 +57,7 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:gravity="center" android:gravity="center"
android:text="@string/update_confirm" android:text="@string/update_confirm"
android:textColor="@color/white" /> android:textColor="@color/button_text" />
<TextView <TextView
android:id="@+id/cancel" android:id="@+id/cancel"
@@ -68,7 +69,7 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:gravity="center" android:gravity="center"
android:text="@string/dialog_negative" android:text="@string/dialog_negative"
android:textColor="@color/white" /> android:textColor="@color/button_text" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
@@ -0,0 +1,296 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView 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="wrap_content"
android:maxHeight="600dp"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/black_90"
android:paddingStart="24dp"
android:paddingTop="16dp"
android:paddingEnd="24dp"
android:paddingBottom="16dp">
<!-- 标题 -->
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="WebDAV 配置"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold" />
<!-- 说明文字 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="请输入您在WebDAV服务提供商(如坚果云)注册的账号和密码(坚果云的密码为应用密码而非登录密码)"
android:textColor="@color/white"
android:textSize="14sp"
android:alpha="0.7"
android:lineSpacingMultiplier="1.2" />
<!-- 服务提供商选择 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="服务提供商"
android:textColor="@color/white"
android:textSize="18sp" />
<TextView
android:id="@+id/providerText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:text="坚果云"
android:textColor="@color/white"
android:textSize="18sp"
android:background="?attr/selectableItemBackground"
android:padding="12dp"
android:clickable="true"
android:focusable="true" />
</LinearLayout>
<!-- 服务器地址(自定义时显示) -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/urlInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:visibility="gone"
app:hintEnabled="false"
app:boxBackgroundColor="@color/grey_900"
app:boxStrokeColor="@color/white_50"
app:hintTextColor="@color/white_50"
app:boxBackgroundMode="filled">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/urlText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="WebDAV服务器地址(如:https://example.com/webdav"
android:textColor="@color/white"
android:textColorHint="@color/white_50"
android:background="@color/grey_900"
android:imeOptions="actionNext"
android:importantForAutofill="no"
android:inputType="textUri"
android:singleLine="true"
android:textSize="18sp" />
</com.google.android.material.textfield.TextInputLayout>
<!-- 用户名 -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/usernameInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:hintEnabled="false"
app:boxBackgroundColor="@color/grey_900"
app:boxStrokeColor="@color/white_50"
app:hintTextColor="@color/white_50"
app:boxBackgroundMode="filled">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/usernameText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="用户名"
android:textColor="@color/white"
android:textColorHint="@color/white_50"
android:background="@color/grey_900"
android:imeOptions="actionNext"
android:importantForAutofill="no"
android:inputType="text"
android:singleLine="true"
android:textSize="18sp" />
</com.google.android.material.textfield.TextInputLayout>
<!-- 密码 -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/passwordInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:hintEnabled="false"
app:passwordToggleEnabled="true"
app:boxBackgroundColor="@color/grey_900"
app:boxStrokeColor="@color/white_50"
app:hintTextColor="@color/white_50"
app:passwordToggleTint="@color/white_50"
app:boxBackgroundMode="filled">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/passwordText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="密码"
android:textColor="@color/white"
android:textColorHint="@color/white_50"
android:background="@color/grey_900"
android:imeOptions="actionDone"
android:importantForAutofill="no"
android:inputType="textPassword"
android:singleLine="true"
android:textSize="18sp" />
</com.google.android.material.textfield.TextInputLayout>
<!-- 自动同步开关 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="自动同步"
android:textColor="@color/white"
android:textSize="18sp" />
<com.fongmi.android.tv.ui.custom.CustomSwitch
android:id="@+id/autoSyncSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<!-- 同步间隔 -->
<LinearLayout
android:id="@+id/syncIntervalContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="同步间隔(分钟)"
android:textColor="@color/white"
android:textSize="18sp" />
<TextView
android:id="@+id/syncIntervalText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="30"
android:textColor="@color/white"
android:textSize="18sp" />
</LinearLayout>
<!-- 操作按钮区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<!-- 测试连接按钮 -->
<com.google.android.material.button.MaterialButton
android:id="@+id/testButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="测试连接"
android:textColor="@color/white"
android:textSize="18sp"
app:strokeColor="@color/white_50"
style="@style/Widget.Material3.Button.OutlinedButton" />
<!-- 立即同步按钮 -->
<com.google.android.material.button.MaterialButton
android:id="@+id/syncButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="立即同步"
android:textColor="@color/white"
android:textSize="18sp"
app:strokeColor="@color/white_50"
style="@style/Widget.Material3.Button.OutlinedButton" />
</LinearLayout>
<!-- 状态提示 -->
<TextView
android:id="@+id/statusText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:text=""
android:textColor="@color/white"
android:textSize="16sp"
android:visibility="gone" />
<!-- 保存和取消按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal"
android:gravity="end">
<TextView
android:id="@+id/positive"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:background="@drawable/selector_text"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"
android:singleLine="true"
android:text="@string/dialog_positive"
android:textColor="@color/button_text"
android:textSize="18sp" />
<TextView
android:id="@+id/negative"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/selector_text"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"
android:singleLine="true"
android:text="@string/dialog_negative"
android:textColor="@color/button_text"
android:textSize="18sp" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
@@ -34,7 +34,7 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:nextFocusLeft="@id/video" android:nextFocusLeft="@id/video"
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
tools:text="刷新" /> tools:text="刷新" />
@@ -48,7 +48,7 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
android:text="@string/play_exo" android:text="@string/play_exo"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" /> android:textSize="14sp" />
<TextView <TextView
@@ -60,7 +60,7 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
tools:text="硬解" /> tools:text="硬解" />
@@ -73,7 +73,7 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
tools:text="1.00" /> tools:text="1.00" />
@@ -86,7 +86,7 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
tools:text="預設" /> tools:text="預設" />
@@ -101,7 +101,7 @@
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
android:tag="3" android:tag="3"
android:text="@string/play_track_text" android:text="@string/play_track_text"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
@@ -117,7 +117,7 @@
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
android:tag="1" android:tag="1"
android:text="@string/play_track_audio" android:text="@string/play_track_audio"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
@@ -134,7 +134,7 @@
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
android:tag="2" android:tag="2"
android:text="@string/play_track_video" android:text="@string/play_track_video"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
@@ -26,7 +26,7 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:nextFocusLeft="@id/change" android:nextFocusLeft="@id/change"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
tools:text="首頁" /> tools:text="首頁" />
@@ -39,7 +39,7 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:text="@string/play" android:text="@string/play"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" /> android:textSize="14sp" />
<TextView <TextView
@@ -51,7 +51,7 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:text="@string/play_exo" android:text="@string/play_exo"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" /> android:textSize="14sp" />
<TextView <TextView
@@ -62,7 +62,7 @@
android:background="@drawable/selector_text" android:background="@drawable/selector_text"
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
tools:text="硬解" /> tools:text="硬解" />
@@ -74,7 +74,7 @@
android:background="@drawable/selector_text" android:background="@drawable/selector_text"
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
android:visibility="gone" android:visibility="gone"
tools:text="1.00" tools:text="1.00"
@@ -88,7 +88,7 @@
android:background="@drawable/selector_text" android:background="@drawable/selector_text"
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
tools:text="預設" /> tools:text="預設" />
@@ -100,7 +100,7 @@
android:background="@drawable/selector_text" android:background="@drawable/selector_text"
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
android:visibility="gone" android:visibility="gone"
tools:text="來源 1" tools:text="來源 1"
@@ -116,7 +116,7 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:tag="3" android:tag="3"
android:text="@string/play_track_text" android:text="@string/play_track_text"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
@@ -131,7 +131,7 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:tag="1" android:tag="1"
android:text="@string/play_track_audio" android:text="@string/play_track_audio"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
@@ -146,7 +146,7 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:tag="2" android:tag="2"
android:text="@string/play_track_video" android:text="@string/play_track_video"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
@@ -29,6 +29,8 @@
android:nextFocusUp="@id/next" android:nextFocusUp="@id/next"
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
app:bar_height="2dp" app:bar_height="2dp"
app:scrubber_enabled_size="14dp"
app:scrubber_disabled_size="14dp"
app:played_color="#FFEB3B" app:played_color="#FFEB3B"
app:scrubber_color="#FFEB3B" app:scrubber_color="#FFEB3B"
app:buffered_color="#80FFEB3B" app:buffered_color="#80FFEB3B"
@@ -45,7 +45,7 @@
android:nextFocusLeft="@id/loop" android:nextFocusLeft="@id/loop"
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
android:text="@string/play_next" android:text="@string/play_next"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" /> android:textSize="14sp" />
<TextView <TextView
@@ -58,7 +58,7 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
android:text="@string/play_prev" android:text="@string/play_prev"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" /> android:textSize="14sp" />
<TextView <TextView
@@ -70,7 +70,7 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
tools:text="刷新" /> tools:text="刷新" />
@@ -84,7 +84,7 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
android:text="@string/play_change" android:text="@string/play_change"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
tools:text="換源" /> tools:text="換源" />
@@ -98,7 +98,7 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
android:text="@string/play_exo" android:text="@string/play_exo"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" /> android:textSize="14sp" />
<TextView <TextView
@@ -110,7 +110,7 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
tools:text="硬解" /> tools:text="硬解" />
@@ -123,7 +123,7 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
tools:text="1.00" /> tools:text="1.00" />
@@ -136,7 +136,7 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
tools:text="預設" /> tools:text="預設" />
@@ -151,7 +151,7 @@
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
android:tag="3" android:tag="3"
android:text="@string/play_track_text" android:text="@string/play_track_text"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
@@ -167,7 +167,7 @@
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
android:tag="1" android:tag="1"
android:text="@string/play_track_audio" android:text="@string/play_track_audio"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
@@ -183,7 +183,7 @@
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
android:tag="2" android:tag="2"
android:text="@string/play_track_video" android:text="@string/play_track_video"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
@@ -197,7 +197,7 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
tools:text="00:00" /> tools:text="00:00" />
@@ -210,7 +210,7 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
android:textColor="@color/white" android:textColor="@color/button_text"
android:textSize="14sp" android:textSize="14sp"
tools:text="00:00" /> tools:text="00:00" />
+40 -3
View File
@@ -1,7 +1,44 @@
<resources> <resources>
<color name="primary">@color/black</color> <color name="primary">#FFEB3B</color>
<color name="primaryDark">@color/black</color> <color name="primaryDark">#FDD835</color>
<color name="accent">@color/blue_500</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> </resources>
+1 -1
View File
@@ -25,7 +25,7 @@
<item name="android:paddingBottom">24dp</item> <item name="android:paddingBottom">24dp</item>
</style> </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="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primaryDark</item> <item name="colorPrimaryDark">@color/primaryDark</item>
<item name="colorAccent">@color/accent</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 { .weui-primary-loading_brand {
color: #07c160; color: #FFEB3B;
color: var(--weui-BRAND); 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;
}
@@ -12,8 +12,12 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.os.HandlerCompat; import androidx.core.os.HandlerCompat;
import com.fongmi.android.tv.event.EventIndex; import com.fongmi.android.tv.Setting;
// import com.fongmi.android.tv.event.EventIndex; // 暂时注释如果不存在则删除
import com.fongmi.android.tv.ui.activity.CrashActivity; import com.fongmi.android.tv.ui.activity.CrashActivity;
import com.fongmi.android.tv.utils.CacheCleaner;
import com.fongmi.android.tv.utils.UpdateInstaller;
import com.fongmi.android.tv.utils.WebDAVSyncManager;
import com.fongmi.android.tv.utils.Notify; import com.fongmi.android.tv.utils.Notify;
import com.fongmi.hook.Hook; import com.fongmi.hook.Hook;
import com.github.catvod.Init; import com.github.catvod.Init;
@@ -41,13 +45,21 @@ public class App extends Application {
private final Gson gson; private final Gson gson;
private final long time; private final long time;
private Hook hook; private Hook hook;
private final Runnable cleanTask;
private final Runnable syncTask;
private boolean appJustLaunched;
public App() { public App() {
instance = this; instance = this;
executor = Executors.newFixedThreadPool(Constant.THREAD_POOL); executor = Executors.newFixedThreadPool(Constant.THREAD_POOL);
handler = HandlerCompat.createAsync(Looper.getMainLooper()); handler = HandlerCompat.createAsync(Looper.getMainLooper());
time = System.currentTimeMillis(); time = System.currentTimeMillis();
gson = new Gson(); gson = new com.google.gson.GsonBuilder()
.disableHtmlEscaping()
.create();
cleanTask = this::checkCacheClean;
syncTask = this::checkWebDAVSync;
appJustLaunched = true;
} }
public static App get() { public static App get() {
@@ -66,6 +78,14 @@ public class App extends Application {
return get().activity; return get().activity;
} }
public static boolean isAppJustLaunched() {
return get().appJustLaunched;
}
public static void setAppLaunched() {
get().appJustLaunched = false;
}
public static void execute(Runnable runnable) { public static void execute(Runnable runnable) {
get().executor.execute(runnable); get().executor.execute(runnable);
} }
@@ -113,12 +133,18 @@ public class App extends Application {
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
Notify.createChannel();
Logger.addLogAdapter(getLogAdapter()); Logger.addLogAdapter(getLogAdapter());
OkHttp.get().setProxy(Setting.getProxy()); OkHttp.get().setProxy(Setting.getProxy());
OkHttp.get().setDoh(Doh.objectFrom(Setting.getDoh())); OkHttp.get().setDoh(Doh.objectFrom(Setting.getDoh()));
EventBus.builder().addIndex(new EventIndex()).installDefaultEventBus(); // EventBus.builder().addIndex(new EventIndex()).installDefaultEventBus(); // 暂时注释如果EventIndex不存在则删除
EventBus.getDefault(); // 使用默认EventBus
CaocConfig.Builder.create().backgroundMode(CaocConfig.BACKGROUND_MODE_SILENT).errorActivity(CrashActivity.class).apply(); CaocConfig.Builder.create().backgroundMode(CaocConfig.BACKGROUND_MODE_SILENT).errorActivity(CrashActivity.class).apply();
// Ensure default notification channel exists for foreground playback service (TV flavor too)
Notify.createChannel();
// 初始化自动缓存清理
initCacheCleaner();
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override @Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
@@ -133,6 +159,14 @@ public class App extends Application {
@Override @Override
public void onActivityResumed(@NonNull Activity activity) { public void onActivityResumed(@NonNull Activity activity) {
if (activity != activity()) setActivity(activity); if (activity != activity()) setActivity(activity);
// 应用回到前台时检查缓存
checkCacheClean();
// 检查是否有待安装的更新文件用户从设置页面返回后
checkPendingInstall();
// 检查WebDAV自动同步
checkWebDAVSync();
// 自动检查更新如果启用
checkAutoUpdate(activity);
} }
@Override @Override
@@ -156,6 +190,99 @@ 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);
}
/**
* 检查是否有待安装的更新文件
* 当用户从设置页面授予安装权限后返回时自动安装
*/
private void checkPendingInstall() {
UpdateInstaller installer = UpdateInstaller.get();
if (installer.hasPendingInstall()) {
Logger.d("App: 检测到待安装文件且权限已授予,自动安装");
boolean success = installer.autoRetryInstall();
if (success) {
Notify.show("正在安装更新...");
} else {
Logger.e("App: 自动安装失败");
}
}
}
/**
* 检查并执行WebDAV自动同步
*/
private void checkWebDAVSync() {
WebDAVSyncManager manager = WebDAVSyncManager.get();
if (manager.isConfigured()) {
// 应用启动时如果已配置WebDAV立即执行一次同步下载远程数据
// 这样新设备配置后下次启动应用时就能看到其他设备的历史记录
Logger.d("App: WebDAV已配置,准备执行同步");
manager.syncHistory(true); // 使用统一的同步方法包含防重复逻辑
// 如果启用了自动同步设置定期同步
if (Setting.isWebDAVAutoSync()) {
int interval = Setting.getWebDAVSyncInterval();
// 延迟执行下次同步避免影响启动速度
post(syncTask, interval * 60 * 1000L);
}
} else {
Logger.d("App: WebDAV未配置,跳过同步");
}
}
/**
* 执行WebDAV同步
*/
private void doWebDAVSync() {
App.execute(() -> {
WebDAVSyncManager manager = WebDAVSyncManager.get();
if (manager.isConfigured()) {
Logger.d("App: 开始WebDAV自动同步");
manager.syncAll();
// 设置下次同步
int interval = Setting.getWebDAVSyncInterval();
post(syncTask, interval * 60 * 1000L);
}
});
}
/**
* 自动检查更新如果启用
*/
private void checkAutoUpdate(Activity activity) {
// 检查是否启用自动更新检查
if (!Setting.getAutoUpdateCheck()) {
return;
}
// 检查是否启用更新功能
if (!Setting.getUpdate()) {
return;
}
// 延迟一小段时间避免影响应用启动速度
post(() -> {
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
Logger.d("App: 开始自动检查更新");
Updater.create().auto().release().start(activity);
}
}, 2000); // 延迟2秒
}
@Override @Override
public PackageManager getPackageManager() { public PackageManager getPackageManager() {
return hook != null ? hook : getBaseContext().getPackageManager(); return hook != null ? hook : getBaseContext().getPackageManager();
@@ -201,6 +201,22 @@ public class Setting {
Prefers.put("update", update); 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() { public static boolean isCaption() {
return Prefers.getBoolean("caption"); return Prefers.getBoolean("caption");
} }
@@ -300,4 +316,113 @@ public class Setting {
public static boolean hasCaption() { public static boolean hasCaption() {
return new Intent(Settings.ACTION_CAPTIONING_SETTINGS).resolveActivity(App.get().getPackageManager()) != null; 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);
}
// WebDAV 同步配置
public static String getWebDAVUrl() {
return Prefers.getString("webdav_url");
}
public static void putWebDAVUrl(String url) {
Prefers.put("webdav_url", url);
}
public static String getWebDAVUsername() {
return Prefers.getString("webdav_username");
}
public static void putWebDAVUsername(String username) {
Prefers.put("webdav_username", username);
}
public static String getWebDAVPassword() {
return Prefers.getString("webdav_password");
}
public static void putWebDAVPassword(String password) {
Prefers.put("webdav_password", password);
}
public static boolean isWebDAVAutoSync() {
return Prefers.getBoolean("webdav_auto_sync", false);
}
public static void putWebDAVAutoSync(boolean autoSync) {
Prefers.put("webdav_auto_sync", autoSync);
}
public static int getWebDAVSyncInterval() {
return Prefers.getInt("webdav_sync_interval", 30); // 默认30分钟
}
public static void putWebDAVSyncInterval(int minutes) {
Prefers.put("webdav_sync_interval", minutes);
}
// WebDAV 同步模式ACCOUNT账号模式 CODE同步码模式
public static String getWebDAVSyncMode() {
return Prefers.getString("webdav_sync_mode", "ACCOUNT");
}
public static void putWebDAVSyncMode(String mode) {
Prefers.put("webdav_sync_mode", mode);
}
// 同步码用于同步码模式
public static String getWebDAVSyncCode() {
return Prefers.getString("webdav_sync_code");
}
public static void putWebDAVSyncCode(String code) {
Prefers.put("webdav_sync_code", code);
}
// 公开存储URL用于同步码模式如GitHub Gist URL
public static String getWebDAVPublicUrl() {
return Prefers.getString("webdav_public_url");
}
public static void putWebDAVPublicUrl(String url) {
Prefers.put("webdav_public_url", url);
}
// GitHub Gist相关用于同步码模式
public static String getWebDAVGistId() {
return Prefers.getString("webdav_gist_id");
}
public static void putWebDAVGistId(String gistId) {
Prefers.put("webdav_gist_id", gistId);
}
public static String getWebDAVGistRawUrl() {
return Prefers.getString("webdav_gist_raw_url");
}
public static void putWebDAVGistRawUrl(String url) {
Prefers.put("webdav_gist_raw_url", url);
}
public static String getWebDAVGistToken() {
return Prefers.getString("webdav_gist_token");
}
public static void putWebDAVGistToken(String token) {
Prefers.put("webdav_gist_token", token);
}
} }
@@ -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 boolean sync;
private Live home; private Live home;
private LiveConfig() {
// 在构造函数中初始化列表防止空指针异常
this.ads = new ArrayList<>();
this.rules = new ArrayList<>();
this.lives = new ArrayList<>();
}
private static class Loader { private static class Loader {
static volatile LiveConfig INSTANCE = new LiveConfig(); static volatile LiveConfig INSTANCE = new LiveConfig();
} }
@@ -97,9 +104,9 @@ public class LiveConfig {
public LiveConfig clear() { public LiveConfig clear() {
this.home = null; this.home = null;
this.ads.clear(); if (this.ads != null) this.ads.clear();
this.rules.clear(); if (this.rules != null) this.rules.clear();
this.lives.clear(); if (this.lives != null) this.lives.clear();
return this; return this;
} }
@@ -37,6 +37,17 @@ public class VodConfig {
private Parse parse; private Parse parse;
private String wall; private String wall;
private Site home; 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 { private static class Loader {
static volatile VodConfig INSTANCE = new VodConfig(); static volatile VodConfig INSTANCE = new VodConfig();
@@ -67,7 +78,44 @@ public class VodConfig {
} }
public static void load(Config config, Callback callback) { 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() { public VodConfig init() {
@@ -94,12 +142,12 @@ public class VodConfig {
this.wall = null; this.wall = null;
this.home = null; this.home = null;
this.parse = null; this.parse = null;
this.ads.clear(); if (this.ads != null) this.ads.clear();
this.doh.clear(); if (this.doh != null) this.doh.clear();
this.rules.clear(); if (this.rules != null) this.rules.clear();
this.sites.clear(); if (this.sites != null) this.sites.clear();
this.flags.clear(); if (this.flags != null) this.flags.clear();
this.parses.clear(); if (this.parses != null) this.parses.clear();
this.loadLive = true; this.loadLive = true;
BaseLoader.get().clear(); BaseLoader.get().clear();
return this; return this;
@@ -114,15 +162,23 @@ public class VodConfig {
OkHttp.cancel("vod"); OkHttp.cancel("vod");
checkJson(Json.parse(Decoder.getJson(UrlUtil.convert(config.getUrl()), "vod")).getAsJsonObject(), callback); checkJson(Json.parse(Decoder.getJson(UrlUtil.convert(config.getUrl()), "vod")).getAsJsonObject(), callback);
} catch (Throwable e) { } catch (Throwable e) {
if (TextUtils.isEmpty(config.getUrl())) App.post(() -> callback.error("")); if (TextUtils.isEmpty(config.getUrl())) {
else loadCache(callback, e); isLoading = false;
App.post(() -> callback.error(""));
} else {
loadCache(callback, e);
}
e.printStackTrace(); e.printStackTrace();
} }
} }
private void loadCache(Callback callback, Throwable e) { private void loadCache(Callback callback, Throwable e) {
if (!TextUtils.isEmpty(config.getJson())) checkJson(Json.parse(config.getJson()).getAsJsonObject(), callback); if (!TextUtils.isEmpty(config.getJson())) {
else App.post(() -> callback.error(Notify.getError(R.string.error_config_get, e))); 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) { private void checkJson(JsonObject object, Callback callback) {
@@ -152,11 +208,21 @@ public class VodConfig {
if (loadLive && object.has("lives")) initLive(object); if (loadLive && object.has("lives")) initLive(object);
String notice = Json.safeString(object, "notice"); String notice = Json.safeString(object, "notice");
config.logo(Json.safeString(object, "logo")); config.logo(Json.safeString(object, "logo"));
App.post(() -> callback.success(notice));
config.json(object.toString()).update(); config.json(object.toString()).update();
// 重置加载状态
isLoading = false;
// 只调用一次success回调优先显示通知消息
if (!TextUtils.isEmpty(notice)) {
App.post(() -> callback.success(notice));
} else {
App.post(callback::success); App.post(callback::success);
}
} catch (Throwable e) { } catch (Throwable e) {
e.printStackTrace(); e.printStackTrace();
// 重置加载状态
isLoading = false;
App.post(() -> callback.error(Notify.getError(R.string.error_config_parse, e))); App.post(() -> callback.error(Notify.getError(R.string.error_config_parse, e)));
} }
} }
@@ -167,14 +233,26 @@ public class VodConfig {
return; return;
} }
String spider = Json.safeString(object, "spider"); String spider = Json.safeString(object, "spider");
try {
BaseLoader.get().parseJar(spider, true); 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")) { for (JsonElement element : Json.safeListElement(object, "sites")) {
try {
Site site = Site.objectFrom(element); Site site = Site.objectFrom(element);
if (sites.contains(site)) continue; if (sites.contains(site)) continue;
site.setApi(UrlUtil.convert(site.getApi())); site.setApi(UrlUtil.convert(site.getApi()));
site.setExt(UrlUtil.convert(site.getExt())); site.setExt(UrlUtil.convert(site.getExt()));
site.setJar(parseJar(site, spider)); site.setJar(parseJar(site, spider));
sites.add(site.trans().sync()); 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) { for (Site site : sites) {
if (site.getKey().equals(config.getHome())) { if (site.getKey().equals(config.getHome())) {
@@ -318,10 +396,32 @@ public class VodConfig {
} }
public void setHome(Site home) { public void setHome(Site home) {
if (home == null) {
// 如果传入null使用默认站点或创建空站点
home = sites.isEmpty() ? new Site() : sites.get(0);
}
this.home = home; this.home = home;
this.home.setActivated(true); this.home.setActivated(true);
// 安全地保存配置防止空指针异常
try {
if (home.getKey() != null && config != null) {
config.home(home.getKey()).save(); config.home(home.getKey()).save();
for (Site item : getSites()) item.setActivated(home); }
} 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) { private void setWall(String wall) {
@@ -46,10 +46,15 @@ public class JarLoader {
} }
private void load(String key, File file) { private void load(String key, File file) {
try {
if (!file.setReadOnly()) return; if (!file.setReadOnly()) return;
loaders.put(key, dex(file)); loaders.put(key, dex(file));
invokeInit(key); invokeInit(key);
putProxy(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) { private DexClassLoader dex(File file) {
@@ -85,6 +90,7 @@ public class JarLoader {
} }
public synchronized void parseJar(String key, String jar) { public synchronized void parseJar(String key, String jar) {
try {
if (loaders.containsKey(key)) return; if (loaders.containsKey(key)) return;
String[] texts = jar.split(";md5;"); String[] texts = jar.split(";md5;");
String md5 = texts.length > 1 ? texts[1].trim() : ""; String md5 = texts.length > 1 ? texts[1].trim() : "";
@@ -99,6 +105,10 @@ public class JarLoader {
} else if (jar.startsWith("assets")) { } else if (jar.startsWith("assets")) {
parseJar(key, UrlUtil.convert(jar)); 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();
}
} }
public DexClassLoader dex(String jar) { public DexClassLoader dex(String jar) {
@@ -267,6 +267,19 @@ public class Config {
Keep.delete(getId()); 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 @NonNull
@Override @Override
public String toString() { public String toString() {
@@ -248,6 +248,10 @@ public class History {
return AppDatabase.get().getHistoryDao().find(cid, System.currentTimeMillis() - Constant.HISTORY_TIME); return AppDatabase.get().getHistoryDao().find(cid, System.currentTimeMillis() - Constant.HISTORY_TIME);
} }
public static List<History> getAll() {
return AppDatabase.get().getHistoryDao().findAllRecent(System.currentTimeMillis() - Constant.HISTORY_TIME);
}
public static History find(String key) { public static History find(String key) {
return AppDatabase.get().getHistoryDao().find(VodConfig.getCid(), key); return AppDatabase.get().getHistoryDao().find(VodConfig.getCid(), key);
} }
@@ -272,8 +276,15 @@ public class History {
} }
public void update() { public void update() {
try {
com.github.catvod.utils.Logger.d("History.update: 开始更新观看记录 key=" + getKey());
merge(find(), false); merge(find(), false);
save(); save();
com.github.catvod.utils.Logger.d("History.update: 更新成功");
} catch (Exception e) {
com.github.catvod.utils.Logger.e("History.update: 更新失败 - " + e.getMessage());
e.printStackTrace();
}
} }
public History update(int cid) { public History update(int cid) {
@@ -287,6 +298,7 @@ public class History {
} }
public History save() { public History save() {
com.github.catvod.utils.Logger.d("History.save: key=" + getKey() + ", vodName=" + getVodName());
AppDatabase.get().getHistoryDao().insertOrUpdate(this); AppDatabase.get().getHistoryDao().insertOrUpdate(this);
return this; return this;
} }
@@ -105,12 +105,37 @@ public class Site implements Parcelable {
public static Site objectFrom(JsonElement element) { public static Site objectFrom(JsonElement element) {
try { try {
return App.gson().fromJson(element, Site.class); Site site = App.gson().fromJson(element, Site.class);
// 尝试修复可能的编码问题
if (site != null && site.getKey() != null) {
site.setKey(fixEncoding(site.getKey()));
if (site.getName() != null) {
site.setName(fixEncoding(site.getName()));
}
}
return site;
} catch (Exception e) { } catch (Exception e) {
return new Site(); return new Site();
} }
} }
private static String fixEncoding(String str) {
if (str == null || str.isEmpty()) return str;
try {
// 检查是否包含乱码字符替换字符 U+FFFD
if (str.indexOf('\uFFFD') >= 0) {
// 尝试用ISO-8859-1重新解码为UTF-8
byte[] bytes = str.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
String fixed = new String(bytes, java.nio.charset.StandardCharsets.UTF_8);
com.github.catvod.utils.Logger.d("Site.fixEncoding: 修复编码 '" + str + "' -> '" + fixed + "'");
return fixed;
}
} catch (Exception e) {
com.github.catvod.utils.Logger.e("Site.fixEncoding: 修复失败 - " + e.getMessage());
}
return str;
}
public static Site get(String key) { public static Site get(String key) {
Site site = new Site(); Site site = new Site();
site.setKey(key); site.setKey(key);
@@ -133,6 +158,13 @@ public class Site implements Parcelable {
public void setKey(@NonNull String key) { public void setKey(@NonNull String key) {
this.key = key; this.key = key;
// 检查key中是否有异常字符
for (int i = 0; i < key.length(); i++) {
char c = key.charAt(i);
if (c == 0xFFFD || c < 0x20 || (c >= 0x7F && c < 0xA0)) {
com.github.catvod.utils.Logger.w("Site.setKey: 检测到异常字符 at position " + i + ": U+" + String.format("%04X", (int)c));
}
}
} }
public String getName() { public String getName() {
@@ -141,6 +173,15 @@ public class Site implements Parcelable {
public void setName(String name) { public void setName(String name) {
this.name = name; this.name = name;
// 检查name中是否有异常字符
if (name != null) {
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (c == 0xFFFD || c < 0x20 || (c >= 0x7F && c < 0xA0)) {
com.github.catvod.utils.Logger.w("Site.setName: 检测到异常字符 at position " + i + ": U+" + String.format("%04X", (int)c) + " in name: " + name);
}
}
}
} }
public String getApi() { public String getApi() {
@@ -17,7 +17,7 @@ public abstract class ConfigDao extends BaseDao<Config> {
@Query("SELECT * FROM Config WHERE type = :type ORDER BY time DESC") @Query("SELECT * FROM Config WHERE type = :type ORDER BY time DESC")
public abstract List<Config> findByType(int type); 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") @Query("SELECT id, name, url, type, time FROM Config WHERE type = :type ORDER BY time DESC")
public abstract List<Config> findUrlByType(int type); public abstract List<Config> findUrlByType(int type);
@@ -16,6 +16,9 @@ public abstract class HistoryDao extends BaseDao<History> {
@Query("SELECT * FROM History WHERE cid = :cid AND createTime >= :createTime ORDER BY createTime DESC") @Query("SELECT * FROM History WHERE cid = :cid AND createTime >= :createTime ORDER BY createTime DESC")
public abstract List<History> find(int cid, long createTime); public abstract List<History> find(int cid, long createTime);
@Query("SELECT * FROM History WHERE createTime >= :createTime ORDER BY createTime DESC")
public abstract List<History> findAllRecent(long createTime);
@Query("SELECT * FROM History WHERE cid = :cid AND `key` = :key") @Query("SELECT * FROM History WHERE cid = :cid AND `key` = :key")
public abstract History find(int cid, String key); public abstract History find(int cid, String key);
@@ -2,7 +2,9 @@ package com.fongmi.android.tv.ui.custom;
import android.content.Context; import android.content.Context;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.TextView; import android.widget.TextView;
@@ -33,6 +35,7 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
private long currentPosition; private long currentPosition;
private long currentBuffered; private long currentBuffered;
private boolean scrubbing; private boolean scrubbing;
private boolean isPressed;
public CustomSeekView(Context context) { public CustomSeekView(Context context) {
this(context, null); this(context, null);
@@ -55,12 +58,72 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
timeBar = findViewById(R.id.timeBar); timeBar = findViewById(R.id.timeBar);
timeBar.addListener(this); timeBar.addListener(this);
refresh = this::refresh; 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) { public void setListener(Players player) {
this.player = 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() { private void start() {
removeCallbacks(refresh); removeCallbacks(refresh);
post(refresh); post(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)) { if (duration > TimeUnit.HOURS.toMillis(3)) {
timeBar.setKeyTimeIncrement(TimeUnit.MINUTES.toMillis(5)); timeBar.setKeyTimeIncrement(TimeUnit.MINUTES.toMillis(5));
} else if (duration > TimeUnit.MINUTES.toMillis(30)) { } else if (duration > TimeUnit.MINUTES.toMillis(30)) {
@@ -124,8 +187,22 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
} }
private void seekToTimeBarPosition(long positionMs) { private void seekToTimeBarPosition(long positionMs) {
// 先设置播放位置
player.seekTo(positionMs); player.seekTo(positionMs);
// 延迟刷新进度条确保播放器已经处理了跳转操作
removeCallbacks(refresh);
postDelayed(() -> {
// 只有在非拖动状态下才刷新进度条位置
if (!scrubbing) {
refresh(); refresh();
// 确保进度条位置与实际播放位置一致
long actualPosition = player.getPosition();
if (Math.abs(actualPosition - positionMs) > 100) { // 如果差异超过100ms再次调整
timeBar.setPosition(actualPosition);
positionView.setText(player.stringToTime(actualPosition));
}
}
}, 100); // 增加延迟时间确保拖拽状态完全结束
} }
@Override @Override
@@ -148,6 +225,20 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
@Override @Override
public void onScrubStop(@NonNull TimeBar timeBar, long position, boolean canceled) { public void onScrubStop(@NonNull TimeBar timeBar, long position, boolean canceled) {
scrubbing = false; 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.App;
import com.fongmi.android.tv.Constant; import com.fongmi.android.tv.Constant;
import com.fongmi.android.tv.Setting; 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.LiveConfig;
import com.fongmi.android.tv.api.config.VodConfig; import com.fongmi.android.tv.api.config.VodConfig;
import com.fongmi.android.tv.impl.ParseCallback; import com.fongmi.android.tv.impl.ParseCallback;
@@ -177,8 +178,13 @@ public class CustomWebView extends WebView implements DialogInterface.OnDismissL
} }
private boolean isAd(String host) { 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 : VodConfig.get().getAds()) if (Util.containOrMatch(host, ad)) return true;
for (String ad : LiveConfig.get().getAds()) if (Util.containOrMatch(host, ad)) return true; for (String ad : LiveConfig.get().getAds()) if (Util.containOrMatch(host, ad)) return true;
return false; return false;
} }
@@ -9,6 +9,7 @@ import android.widget.RelativeLayout;
import com.fongmi.android.tv.databinding.ViewEmptyBinding; import com.fongmi.android.tv.databinding.ViewEmptyBinding;
import com.fongmi.android.tv.databinding.ViewProgressBinding; import com.fongmi.android.tv.databinding.ViewProgressBinding;
import com.airbnb.lottie.LottieAnimationView;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -47,7 +48,8 @@ public class ProgressLayout extends RelativeLayout {
} }
private void initView() { 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.setTag(TAG_PROGRESS);
mEmptyView.setVisibility(GONE); mEmptyView.setVisibility(GONE);
mProgressView = ViewProgressBinding.inflate(LayoutInflater.from(getContext())).getRoot(); mProgressView = ViewProgressBinding.inflate(LayoutInflater.from(getContext())).getRoot();
@@ -103,21 +105,46 @@ public class ProgressLayout extends RelativeLayout {
case CONTENT: case CONTENT:
mEmptyView.setVisibility(GONE); mEmptyView.setVisibility(GONE);
mProgressView.setVisibility(GONE); mProgressView.setVisibility(GONE);
pauseLottieAnimation();
setContentVisibility(true); setContentVisibility(true);
break; break;
case PROGRESS: case PROGRESS:
mEmptyView.setVisibility(GONE); mEmptyView.setVisibility(GONE);
mProgressView.setVisibility(VISIBLE); mProgressView.setVisibility(VISIBLE);
pauseLottieAnimation();
setContentVisibility(false); setContentVisibility(false);
break; break;
case EMPTY: case EMPTY:
mEmptyView.setVisibility(VISIBLE); mEmptyView.setVisibility(VISIBLE);
mProgressView.setVisibility(GONE); mProgressView.setVisibility(GONE);
playLottieAnimation();
setContentVisibility(false); setContentVisibility(false);
break; 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) { private void setContentVisibility(boolean visible) {
for (View view : mContentViews) { for (View view : mContentViews) {
if (visible) showView(view); 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.fongmi.android.tv.App;
import com.github.catvod.net.OkHttp; import com.github.catvod.net.OkHttp;
import com.github.catvod.utils.Logger;
import com.github.catvod.utils.Path; import com.github.catvod.utils.Path;
import com.google.common.net.HttpHeaders; import com.google.common.net.HttpHeaders;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.InputStream; import java.io.InputStream;
@@ -16,45 +18,219 @@ public class Download {
private final File file; private final File file;
private final String url; private final String url;
private final String fallbackUrl;
private Callback callback; private Callback callback;
private static final int MAX_RETRY_COUNT = 3; // 最大重试次数
public static Download create(String url, File file) { public static Download create(String url, File file) {
return create(url, file, null); return create(url, file, null);
} }
public static Download create(String url, File file, Callback callback) { public static Download create(String url, File file, Callback callback) {
return new Download(url, file, callback); return create(url, file, null, callback);
}
public static Download create(String url, File file, String fallbackUrl, Callback callback) {
return new Download(url, file, fallbackUrl, callback);
} }
public Download(String url, File file, Callback callback) { public Download(String url, File file, Callback callback) {
this(url, file, null, callback);
}
public Download(String url, File file, String fallbackUrl, Callback callback) {
this.url = url; this.url = url;
this.file = file; this.file = file;
this.fallbackUrl = fallbackUrl;
this.callback = callback; this.callback = callback;
} }
public void start() { public void start() {
if (url == null || url.isEmpty()) {
if (callback != null) {
App.post(() -> callback.error("下载URL为空"));
}
return;
}
if (url.startsWith("file")) return; if (url.startsWith("file")) return;
if (callback == null) doInBackground(); if (file == null) {
else App.execute(this::doInBackground); if (callback != null) {
App.post(() -> callback.error("保存文件路径为空"));
}
return;
}
if (callback == null) {
// 无回调时直接执行同步
doInBackgroundWithFallback();
} else {
// 有回调时异步执行
App.execute(this::doInBackgroundWithFallback);
}
}
/**
* 带智能回退的下载方法
* 先尝试主URL通常是jsDelivr CDN失败后回退到备用URL
*/
private void doInBackgroundWithFallback() {
// 先尝试主URL
boolean mainSuccess = doInBackground(url, "主URL");
if (mainSuccess) {
return;
}
// 主URL失败如果有回退URL尝试回退URL
if (fallbackUrl != null && !fallbackUrl.equals(url)) {
Logger.d("Download: 主URL下载失败,回退到备用URL: " + fallbackUrl);
doInBackground(fallbackUrl, "备用URL");
}
}
/**
* 使用指定URL下载文件带重试机制
*/
private boolean doInBackground(String downloadUrl, String source) {
Exception lastException = null;
for (int attempt = 1; attempt <= MAX_RETRY_COUNT; attempt++) {
try {
if (callback != null) {
App.post(() -> callback.progress(0));
}
boolean success = downloadWithUrl(downloadUrl, source, attempt);
if (success) {
return true;
}
} catch (Exception e) {
lastException = e;
Logger.w("Download: 下载失败 (来源: " + source + ", 尝试 " + attempt + "/" + MAX_RETRY_COUNT + "): " + e.getMessage());
// 如果不是最后一次尝试等待后重试
if (attempt < MAX_RETRY_COUNT) {
try {
long retryDelay = 500L * attempt; // 递增延迟
Thread.sleep(retryDelay);
Logger.d("Download: 等待 " + retryDelay + "ms 后重试...");
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
}
// 所有尝试都失败
if (callback != null && lastException != null) {
String errorMsg = lastException.getMessage();
App.post(() -> callback.error(errorMsg != null ? errorMsg : "下载失败"));
}
return false;
}
/**
* 使用指定URL下载文件
*/
private boolean downloadWithUrl(String downloadUrl, String source, int attempt) throws Exception {
if (downloadUrl == null || downloadUrl.isEmpty()) {
throw new Exception("下载URL为空");
}
if (file == null) {
throw new Exception("保存文件路径为空");
}
Response res = null;
InputStream inputStream = null;
try {
res = OkHttp.newCall(downloadUrl, downloadUrl).execute();
// 检查HTTP响应状态码
if (!res.isSuccessful()) {
throw new Exception("下载失败: HTTP " + res.code() + " " + (res.message() != null ? res.message() : "未知错误"));
}
// 检查响应体是否存在
if (res.body() == null) {
throw new Exception("下载失败: 响应体为空");
}
// 获取输入流
inputStream = res.body().byteStream();
if (inputStream == null) {
throw new Exception("下载失败: 无法获取输入流");
}
Path.create(file);
// 获取文件大小如果无法获取则使用-1表示未知大小
String contentLengthStr = res.header(HttpHeaders.CONTENT_LENGTH);
long expectedLength = -1;
if (contentLengthStr != null && !contentLengthStr.isEmpty()) {
try {
expectedLength = Long.parseLong(contentLengthStr);
if (expectedLength < 0) {
expectedLength = -1;
}
} catch (NumberFormatException e) {
Logger.w("Download: 无法解析Content-Length: " + contentLengthStr);
expectedLength = -1;
}
}
// 下载文件
download(inputStream, expectedLength);
// 验证下载的文件如果知道预期大小
if (expectedLength > 0 && !verifyDownloadedFile(file, expectedLength)) {
throw new Exception("下载的文件可能已损坏,请重试");
}
Logger.d("Download: 下载成功 (来源: " + source + ", 尝试 " + attempt + "/" + MAX_RETRY_COUNT + ")");
if (callback != null) {
App.post(() -> callback.success(file));
}
return true;
} catch (Exception e) {
// 如果下载失败删除可能不完整的文件
if (file != null && file.exists()) {
try {
file.delete();
} catch (Exception ignored) {
}
}
throw e;
} finally {
// 关闭输入流
if (inputStream != null) {
try {
inputStream.close();
} catch (Exception ignored) {
}
}
// 关闭响应
if (res != null) {
try {
res.close();
} catch (Exception ignored) {
}
}
}
} }
public void cancel() { public void cancel() {
OkHttp.cancel(url); OkHttp.cancel(url);
if (fallbackUrl != null) {
OkHttp.cancel(fallbackUrl);
}
Path.clear(file); Path.clear(file);
callback = null; callback = null;
} }
private void doInBackground() { private void download(InputStream is, long length) throws Exception {
try (Response res = OkHttp.newCall(url, url).execute()) { if (is == null) {
Path.create(file); throw new Exception("输入流为空,无法下载");
download(res.body().byteStream(), Double.parseDouble(res.header(HttpHeaders.CONTENT_LENGTH, "1")));
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 {
try (BufferedInputStream input = new BufferedInputStream(is); FileOutputStream os = new FileOutputStream(file)) { try (BufferedInputStream input = new BufferedInputStream(is); FileOutputStream os = new FileOutputStream(file)) {
byte[] buffer = new byte[4096]; byte[] buffer = new byte[4096];
int readBytes; int readBytes;
@@ -62,10 +238,67 @@ public class Download {
while ((readBytes = input.read(buffer)) != -1) { while ((readBytes = input.read(buffer)) != -1) {
totalBytes += readBytes; totalBytes += readBytes;
os.write(buffer, 0, readBytes); os.write(buffer, 0, readBytes);
int progress = (int) (totalBytes / length * 100.0);
App.post(() -> {if (callback != null) callback.progress(progress);}); // 只有当知道文件大小时才计算进度
if (length > 0 && callback != null) {
int progress = (int) (totalBytes * 100.0 / length);
final int finalProgress = Math.min(progress, 100); // 确保不超过100%并设为final
App.post(() -> callback.progress(finalProgress));
} else if (callback != null) {
// 不知道文件大小时显示不确定进度
App.post(() -> callback.progress(-1));
} }
} }
// 下载完成后如果不知道文件大小显示100%
if (length <= 0 && callback != null) {
App.post(() -> callback.progress(100));
}
}
}
private boolean verifyDownloadedFile(File file, long expectedLength) {
try {
// 如果文件不存在或为空验证失败
if (file == null || !file.exists() || file.length() == 0) {
Logger.e("File verification failed: file does not exist or is empty");
return false;
}
// 如果知道预期大小检查文件大小是否匹配
if (expectedLength > 0 && file.length() != expectedLength) {
Logger.e("File size mismatch: expected " + expectedLength + ", actual " + file.length());
return false;
}
// 检查APK文件头 (ZIP文件头)
if (file.length() < 4) {
Logger.e("File too small: " + file.length() + " bytes");
return false;
}
try (FileInputStream fis = new FileInputStream(file)) {
byte[] header = new byte[4];
int bytesRead = fis.read(header);
if (bytesRead < 4) {
Logger.e("Cannot read file header");
return false;
}
// ZIP文件头应该是 0x504B0304 (PK..)
if (header[0] != 0x50 || header[1] != 0x4B || header[2] != 0x03 || header[3] != 0x04) {
Logger.e("Invalid APK file header: " + String.format("%02X %02X %02X %02X", header[0], header[1], header[2], header[3]));
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());
e.printStackTrace();
return false;
}
} }
public interface Callback { public interface Callback {
@@ -11,6 +11,7 @@ import androidx.core.content.FileProvider;
import com.fongmi.android.tv.App; import com.fongmi.android.tv.App;
import com.fongmi.android.tv.R; import com.fongmi.android.tv.R;
import com.fongmi.android.tv.impl.Callback; import com.fongmi.android.tv.impl.Callback;
import com.github.catvod.utils.Logger;
import com.github.catvod.utils.Path; import com.github.catvod.utils.Path;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
@@ -34,11 +35,35 @@ public class FileUtil {
} }
public static void openFile(File file) { public static void openFile(File file) {
try {
Intent intent = new Intent(Intent.ACTION_VIEW); Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(getShareUri(file), FileUtil.getMimeType(file.getName()));
// 对于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); 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) { public static void gzipCompress(File target) {
@@ -4,6 +4,7 @@ import android.Manifest;
import android.app.Notification; import android.app.Notification;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.text.TextUtils; import android.text.TextUtils;
@@ -40,7 +41,7 @@ public class Notify {
public static void createChannel() { public static void createChannel() {
NotificationManagerCompat notifyMgr = NotificationManagerCompat.from(App.get()); NotificationManagerCompat notifyMgr = NotificationManagerCompat.from(App.get());
notifyMgr.createNotificationChannel(new NotificationChannelCompat.Builder(DEFAULT, NotificationManagerCompat.IMPORTANCE_LOW).setName("TV").build()); notifyMgr.createNotificationChannel(new NotificationChannelCompat.Builder(DEFAULT, NotificationManagerCompat.IMPORTANCE_LOW).setName("XMBOX").build());
} }
public static String getError(int resId, Throwable e) { public static String getError(int resId, Throwable e) {
@@ -49,7 +50,7 @@ public class Notify {
} }
public static void show(Notification notification) { public static void show(Notification notification) {
if (ActivityCompat.checkSelfPermission(App.get(), Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) return; if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(App.get(), Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) return;
NotificationManagerCompat.from(App.get()).notify(ID, notification); NotificationManagerCompat.from(App.get()).notify(ID, notification);
} }
@@ -90,13 +91,20 @@ public class Notify {
private void makeText(String message) { private void makeText(String message) {
if (mToast != null) mToast.cancel(); if (mToast != null) mToast.cancel();
if (mHandler == null) mHandler = new Handler(Looper.getMainLooper());
if (TextUtils.isEmpty(message)) return; if (TextUtils.isEmpty(message)) return;
mToast = new Toast(App.get()); mToast = new Toast(App.get());
TextView view = (TextView) LayoutInflater.from(App.get()).inflate(R.layout.view_toast, null); TextView view = (TextView) LayoutInflater.from(App.get()).inflate(R.layout.view_toast, null);
view.setText(message); view.setText(message);
mToast.setView(view); mToast.setView(view);
mToast.setDuration(Toast.LENGTH_LONG); mToast.setDuration(Toast.LENGTH_SHORT);
mToast.show(); mToast.show();
// 1秒后取消Toast
mHandler.removeCallbacksAndMessages(null);
mHandler.postDelayed(() -> {
if (mToast != null) mToast.cancel();
}, 1000); // 1000毫秒 = 1秒
} }
private void makeTextCenter(String message) { private void makeTextCenter(String message) {
@@ -0,0 +1,312 @@
package com.fongmi.android.tv.utils;
import android.text.TextUtils;
import com.fongmi.android.tv.Setting;
import com.fongmi.android.tv.App;
import com.fongmi.android.tv.bean.Backup;
import com.fongmi.android.tv.bean.History;
import com.fongmi.android.tv.db.AppDatabase;
import com.github.catvod.net.OkHttp;
import com.github.catvod.utils.Logger;
import com.github.catvod.utils.Prefers;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
/**
* 同步码管理器无需WebDAV账号
* 使用公开的HTTP存储服务通过同步码区分用户
*
* 方案使用GitHub Gist作为存储
* - 用户创建一个公开的GitHub Gist
* - 通过同步码作为文件名的一部分来区分不同用户
* - 所有知道同步码的设备可以共享数据
*/
public class SyncCodeManager {
private static final String HISTORY_FILE_PREFIX = "xmbox_history_";
private static final String SETTINGS_FILE_PREFIX = "xmbox_settings_";
private static final String BACKUP_FILE_PREFIX = "xmbox_backup_";
private static final String FILE_SUFFIX = ".json";
private static SyncCodeManager instance;
private String syncCode;
private String gistId; // GitHub Gist ID
private String gistToken; // GitHub Personal Access Token用于上传可选
public static SyncCodeManager get() {
if (instance == null) {
instance = new SyncCodeManager();
}
return instance;
}
private SyncCodeManager() {
loadConfig();
}
/**
* 加载配置
*/
private void loadConfig() {
syncCode = Setting.getWebDAVSyncCode();
gistId = Setting.getWebDAVGistId();
gistToken = Setting.getWebDAVGistToken();
}
/**
* 检查是否已配置
*/
public boolean isConfigured() {
return !TextUtils.isEmpty(syncCode) && !TextUtils.isEmpty(gistId);
}
/**
* 生成同步码
* @return 8位随机同步码字母+数字
*/
public static String generateSyncCode() {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random = new Random();
StringBuilder code = new StringBuilder();
for (int i = 0; i < 8; i++) {
code.append(chars.charAt(random.nextInt(chars.length())));
}
return code.toString();
}
/**
* 获取文件URLGitHub Gist raw URL
*/
private String getFileUrl(String prefix) {
// GitHub Gist raw URL格式
// https://gist.githubusercontent.com/{username}/{gist_id}/raw/{filename}
// 文件名格式{prefix}{syncCode}.json
// 例如xmbox_history_ABC123XYZ.json
// 如果用户提供了完整的Gist raw URL
String gistRawUrl = Setting.getWebDAVGistRawUrl();
if (!TextUtils.isEmpty(gistRawUrl)) {
String filename = prefix + syncCode + FILE_SUFFIX;
return gistRawUrl + "/" + filename;
}
// 否则需要从Gist ID构建需要知道username
// 这里简化处理要求用户提供完整的raw URL
return null;
}
/**
* 上传观看记录
*/
public boolean uploadHistory() {
if (!isConfigured()) {
Logger.e("SyncCode: 未配置,无法上传观看记录");
return false;
}
try {
// 获取所有观看记录
List<History> historyList = AppDatabase.get().getHistoryDao().findAll();
String json = App.gson().toJson(historyList);
// 上传到GitHub Gist
String fileUrl = getFileUrl(HISTORY_FILE_PREFIX);
if (fileUrl == null) {
Logger.e("SyncCode: 无法构建文件URL,请配置Gist Raw URL");
return false;
}
// 使用GitHub Gist API更新文件
boolean success = updateGistFile(fileUrl, json);
if (success) {
Logger.d("SyncCode: 观看记录上传成功,共 " + historyList.size() + "");
}
return success;
} catch (Exception e) {
Logger.e("SyncCode: 观看记录上传失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* 下载观看记录
*/
public boolean downloadHistory() {
if (!isConfigured()) {
Logger.e("SyncCode: 未配置,无法下载观看记录");
return false;
}
try {
String fileUrl = getFileUrl(HISTORY_FILE_PREFIX);
if (fileUrl == null) {
return false;
}
// 从GitHub Gist下载文件
String json = downloadGistFile(fileUrl);
if (TextUtils.isEmpty(json)) {
Logger.d("SyncCode: 观看记录文件不存在,跳过下载");
return false;
}
Type listType = new TypeToken<List<History>>(){}.getType();
List<History> remoteHistoryList = App.gson().fromJson(json, listType);
// 智能合并与WebDAV相同的逻辑
if (!remoteHistoryList.isEmpty()) {
List<History> localHistoryList = AppDatabase.get().getHistoryDao().findAll();
Map<String, History> localMap = new HashMap<>();
for (History local : localHistoryList) {
localMap.put(local.getKey(), local);
}
List<History> toInsert = new java.util.ArrayList<>();
List<History> toUpdate = new java.util.ArrayList<>();
for (History remote : remoteHistoryList) {
History local = localMap.get(remote.getKey());
if (local == null) {
toInsert.add(remote);
} else {
if (remote.getCreateTime() > local.getCreateTime()) {
toUpdate.add(remote);
} else if (remote.getCreateTime() == local.getCreateTime() && remote.getPosition() > local.getPosition()) {
toUpdate.add(remote);
}
}
}
if (!toInsert.isEmpty()) {
AppDatabase.get().getHistoryDao().insert(toInsert);
Logger.d("SyncCode: 新增 " + toInsert.size() + " 条观看记录");
}
if (!toUpdate.isEmpty()) {
AppDatabase.get().getHistoryDao().update(toUpdate);
Logger.d("SyncCode: 更新 " + toUpdate.size() + " 条观看记录");
}
Logger.d("SyncCode: 观看记录合并完成");
return true;
}
return false;
} catch (Exception e) {
Logger.e("SyncCode: 观看记录下载失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* 从GitHub Gist下载文件
*/
private String downloadGistFile(String fileUrl) {
try {
Response response = OkHttp.newCall(fileUrl).execute();
if (response.isSuccessful()) {
return response.body().string();
}
return "";
} catch (Exception e) {
Logger.e("SyncCode: 下载文件失败: " + e.getMessage());
return "";
}
}
/**
* 更新GitHub Gist文件
* 注意GitHub Gist需要通过API更新不能直接PUT文件
*/
private boolean updateGistFile(String fileUrl, String content) {
// GitHub Gist需要通过REST API更新
// 这里简化处理实际需要调用GitHub Gist API
// POST https://api.github.com/gists/{gist_id}
if (TextUtils.isEmpty(gistToken)) {
Logger.w("SyncCode: 未提供GitHub Token,无法上传(Gist需要Token才能更新)");
// 可以提示用户同步码模式需要GitHub Token才能上传
return false;
}
try {
// 构建GitHub Gist API请求
String apiUrl = "https://api.github.com/gists/" + gistId;
String filename = HISTORY_FILE_PREFIX + syncCode + FILE_SUFFIX;
// 构建请求体
Map<String, Object> requestBody = new HashMap<>();
Map<String, Object> files = new HashMap<>();
Map<String, String> fileContent = new HashMap<>();
fileContent.put("content", content);
files.put(filename, fileContent);
requestBody.put("files", files);
String jsonBody = App.gson().toJson(requestBody);
RequestBody body = RequestBody.create(
MediaType.parse("application/json; charset=utf-8"),
jsonBody
);
Request request = new Request.Builder()
.url(apiUrl)
.method("PATCH", body)
.header("Authorization", "Bearer " + gistToken)
.header("Accept", "application/vnd.github.v3+json")
.build();
Response response = OkHttp.client().newCall(request).execute();
boolean success = response.isSuccessful();
response.close();
return success;
} catch (Exception e) {
Logger.e("SyncCode: 更新Gist失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* 同步观看记录
*/
public boolean syncHistory() {
if (!isConfigured()) {
return false;
}
App.execute(() -> {
uploadHistory();
downloadHistory();
});
return true;
}
/**
* 重新加载配置
*/
public void reloadConfig() {
loadConfig();
}
}
@@ -0,0 +1,150 @@
package com.fongmi.android.tv.utils;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import androidx.core.content.FileProvider;
import com.fongmi.android.tv.App;
import com.github.catvod.utils.Logger;
import java.io.File;
/**
* Android 更新安装器
* 处理安装权限检查和请求以及APK安装
*/
public class UpdateInstaller {
private static UpdateInstaller instance;
private File pendingInstallFile; // 待安装的文件
public static UpdateInstaller get() {
if (instance == null) {
instance = new UpdateInstaller();
}
return instance;
}
/**
* 检查是否有安装权限
* @return 是否有安装权限
*/
public boolean hasInstallPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return App.get().getPackageManager().canRequestPackageInstalls();
}
return true; // Android 8.0以下不需要此权限
}
/**
* 请求安装权限打开设置页面
*/
public void requestInstallPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
intent.setData(Uri.parse("package:" + App.get().getPackageName()));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
App.get().startActivity(intent);
Logger.d("UpdateInstaller: 已打开安装权限设置页面");
} catch (Exception e) {
Logger.e("UpdateInstaller: 无法打开安装权限设置页面: " + e.getMessage());
e.printStackTrace();
}
}
}
/**
* 安装 APK 文件
* @param apkFile APK 文件
* @return 是否成功启动安装流程
*/
public boolean install(File apkFile) {
return install(apkFile, false);
}
/**
* 安装 APK 文件
* @param apkFile APK 文件
* @param checkPermission 是否检查权限如果为false即使没有权限也会尝试安装
* @return 是否成功启动安装流程
*/
public boolean install(File apkFile, boolean checkPermission) {
try {
// Android 8.0+ 需要请求安装权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (checkPermission && !hasInstallPermission()) {
// 没有权限保存待安装的文件返回 false由调用方处理
this.pendingInstallFile = apkFile;
Logger.d("UpdateInstaller: 没有安装权限,已保存待安装文件: " + apkFile.getAbsolutePath());
return false; // 返回false表示需要权限但不表示失败
}
}
// 检查文件是否存在
if (!apkFile.exists() || !apkFile.isFile()) {
Logger.e("UpdateInstaller: APK文件不存在或不是文件: " + apkFile.getAbsolutePath());
return false;
}
// 使用 FileProvider 获取 URI
String authority = App.get().getPackageName() + ".provider";
Uri apkUri = FileProvider.getUriForFile(App.get(), authority, apkFile);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
App.get().startActivity(intent);
Logger.d("UpdateInstaller: 已启动安装程序");
this.pendingInstallFile = null; // 清除待安装文件
return true;
} catch (Exception e) {
Logger.e("UpdateInstaller: 安装失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* 获取待安装的文件
* @return 待安装的文件如果没有则返回null
*/
public File getPendingInstallFile() {
return pendingInstallFile;
}
/**
* 检查是否有待安装的文件且权限已授予
* 用于应用恢复时自动检测
*/
public boolean hasPendingInstall() {
return pendingInstallFile != null && pendingInstallFile.exists() && hasInstallPermission();
}
/**
* 自动重试安装用于应用恢复时
*/
public boolean autoRetryInstall() {
if (hasPendingInstall()) {
File file = pendingInstallFile;
pendingInstallFile = null; // 清除待安装文件
return install(file, false); // 不检查权限因为已经检查过了
}
return false;
}
/**
* 清除待安装的文件
*/
public void clearPendingInstall() {
this.pendingInstallFile = null;
}
}
@@ -0,0 +1,1010 @@
package com.fongmi.android.tv.utils;
import android.text.TextUtils;
import com.fongmi.android.tv.App;
import com.fongmi.android.tv.bean.Backup;
import com.fongmi.android.tv.bean.History;
import com.fongmi.android.tv.db.AppDatabase;
import com.fongmi.android.tv.event.RefreshEvent;
import com.github.catvod.utils.Logger;
import com.github.catvod.utils.Prefers;
import com.google.gson.Gson;
import com.thegrizzlylabs.sardineandroid.Sardine;
import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import com.google.gson.reflect.TypeToken;
import com.fongmi.android.tv.Setting;
/**
* WebDAV同步管理器
* 用于同步观看记录和设置到WebDAV服务器
*/
public class WebDAVSyncManager {
private static final String HISTORY_FILE = "xmbox_history.json";
private static final String SETTINGS_FILE = "xmbox_settings.json";
private static final String BACKUP_FILE = "xmbox_backup.json";
// 同步模式ACCOUNT账号模式 CODE同步码模式
public enum SyncMode {
ACCOUNT, // 使用WebDAV账号
CODE // 使用同步码无需账号
}
private static WebDAVSyncManager instance;
private Sardine sardine;
private String baseUrl;
private String username;
private String password;
private String syncCode; // 同步码
private SyncMode syncMode = SyncMode.ACCOUNT; // 默认使用账号模式
private volatile boolean isSyncing = false; // 同步锁防止重复同步
public static WebDAVSyncManager get() {
if (instance == null) {
instance = new WebDAVSyncManager();
}
return instance;
}
private WebDAVSyncManager() {
loadConfig();
}
/**
* 加载WebDAV配置
*/
private void loadConfig() {
// 检查同步模式
String modeStr = Setting.getWebDAVSyncMode();
if ("CODE".equals(modeStr)) {
syncMode = SyncMode.CODE;
syncCode = Setting.getWebDAVSyncCode();
// 同步码模式使用公开的WebDAV服务器如jsDelivr CDN的GitHub仓库
// 或者使用其他公开存储服务
baseUrl = getPublicStorageUrl();
username = null;
password = null;
} else {
syncMode = SyncMode.ACCOUNT;
baseUrl = Setting.getWebDAVUrl();
username = Setting.getWebDAVUsername();
password = Setting.getWebDAVPassword();
}
if (syncMode == SyncMode.ACCOUNT) {
// 账号模式需要账号密码
if (!TextUtils.isEmpty(baseUrl) && !TextUtils.isEmpty(username) && !TextUtils.isEmpty(password)) {
try {
sardine = new OkHttpSardine();
sardine.setCredentials(username, password);
Logger.d("WebDAV: 账号模式配置已加载");
} catch (Exception e) {
Logger.e("WebDAV: 初始化失败: " + e.getMessage());
sardine = null;
}
} else {
sardine = null;
}
} else {
// 同步码模式使用公开存储无需认证
if (!TextUtils.isEmpty(syncCode) && !TextUtils.isEmpty(baseUrl)) {
try {
sardine = new OkHttpSardine();
// 公开存储不需要认证
Logger.d("WebDAV: 同步码模式配置已加载,同步码: " + syncCode);
} catch (Exception e) {
Logger.e("WebDAV: 初始化失败: " + e.getMessage());
sardine = null;
}
} else {
sardine = null;
}
}
}
/**
* 获取公开存储URL同步码模式使用
* 方案使用GitHub Gist作为公开存储
* 用户需要
* 1. 创建一个GitHub Gist公开
* 2. 获取Gist的raw URL
* 3. 输入同步码
*
* 文件路径格式{gist_raw_url}/{syncCode}/xmbox_history.json
*/
private String getPublicStorageUrl() {
// 获取用户配置的GitHub Gist raw URL
// 例如https://gist.githubusercontent.com/username/gist_id/raw/
String gistBaseUrl = Setting.getWebDAVPublicUrl();
if (TextUtils.isEmpty(gistBaseUrl)) {
// 如果没有配置返回null需要用户配置
return null;
}
// 将同步码添加到路径中作为子目录
// 例如https://gist.githubusercontent.com/username/gist_id/raw/ABC123XYZ/
if (!TextUtils.isEmpty(syncCode)) {
String url = gistBaseUrl.endsWith("/") ? gistBaseUrl : gistBaseUrl + "/";
return url + syncCode + "/";
}
return gistBaseUrl;
}
/**
* 检查WebDAV是否已配置
*/
public boolean isConfigured() {
if (syncMode == SyncMode.CODE) {
// 同步码模式需要同步码和公开存储URL
return sardine != null && !TextUtils.isEmpty(baseUrl) && !TextUtils.isEmpty(syncCode);
} else {
// 账号模式需要账号密码和URL
return sardine != null && !TextUtils.isEmpty(baseUrl) && !TextUtils.isEmpty(username) && !TextUtils.isEmpty(password);
}
}
/**
* 生成同步码
* @return 8位随机同步码字母+数字
*/
public static String generateSyncCode() {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
java.util.Random random = new java.util.Random();
StringBuilder code = new StringBuilder();
for (int i = 0; i < 8; i++) {
code.append(chars.charAt(random.nextInt(chars.length())));
}
return code.toString();
}
/**
* 测试WebDAV连接
* @return 测试结果包含成功状态和错误信息
*/
public TestResult testConnectionWithMessage() {
if (!isConfigured()) {
return new TestResult(false, "WebDAV未配置,请检查URL、用户名和密码");
}
try {
// 确保baseUrl以/结尾
String testUrl = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
Logger.d("WebDAV: 测试连接URL: " + testUrl);
Logger.d("WebDAV: 用户名: " + (username != null ? username : "null"));
// 尝试列出目录
sardine.list(testUrl);
Logger.d("WebDAV: 连接测试成功,可以访问目录");
return new TestResult(true, "连接成功!");
} catch (java.io.IOException e) {
String errorMsg = e.getMessage();
Logger.e("WebDAV: 连接测试失败: " + errorMsg);
Logger.e("WebDAV: 异常类型: " + e.getClass().getName());
e.printStackTrace();
// 根据错误类型提供更详细的提示
if (errorMsg != null) {
if (errorMsg.contains("401") || errorMsg.contains("Unauthorized")) {
return new TestResult(false, "认证失败:用户名或密码错误,请检查账号密码。\n提示:坚果云需要使用应用密码,不是登录密码");
} else if (errorMsg.contains("403") || errorMsg.contains("Forbidden")) {
return new TestResult(false, "访问被拒绝:账号可能没有WebDAV权限");
} else if (errorMsg.contains("404") || errorMsg.contains("Not Found")) {
return new TestResult(false, "URL不存在:请检查WebDAV服务器地址是否正确");
} else if (errorMsg.contains("SSL") || errorMsg.contains("Certificate")) {
return new TestResult(false, "SSL证书错误:请检查服务器证书是否有效");
} else if (errorMsg.contains("timeout") || errorMsg.contains("Timeout")) {
return new TestResult(false, "连接超时:请检查网络连接或服务器地址");
} else if (errorMsg.contains("UnknownHost") || errorMsg.contains("unreachable")) {
return new TestResult(false, "无法连接到服务器:请检查网络连接和服务器地址");
}
}
return new TestResult(false, "连接失败:" + (errorMsg != null ? errorMsg : "未知错误"));
} catch (Exception e) {
String errorMsg = e.getMessage();
Logger.e("WebDAV: 连接测试失败: " + errorMsg);
Logger.e("WebDAV: 异常类型: " + e.getClass().getName());
e.printStackTrace();
return new TestResult(false, "连接失败:" + (errorMsg != null ? errorMsg : e.getClass().getSimpleName()));
}
}
/**
* 测试WebDAV连接兼容旧接口
*/
public boolean testConnection() {
return testConnectionWithMessage().success;
}
/**
* 测试结果类
*/
public static class TestResult {
public final boolean success;
public final String message;
public TestResult(boolean success, String message) {
this.success = success;
this.message = message;
}
}
/**
* 确保目录存在
*/
private void ensureDirectory(String path) throws Exception {
try {
if (!sardine.exists(path)) {
sardine.createDirectory(path);
Logger.d("WebDAV: 创建目录: " + path);
}
} catch (Exception e) {
Logger.e("WebDAV: 创建目录失败: " + e.getMessage());
throw e;
}
}
/**
* 获取文件完整URL
*/
private String getFileUrl(String filename) {
if (syncMode == SyncMode.CODE) {
// 同步码模式使用GitHub Gist raw URL
// 格式https://gist.githubusercontent.com/username/gist_id/raw/{syncCode}/{filename}
String url = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
return url + filename;
} else {
// 账号模式使用WebDAV URL
// 对于坚果云https://dav.jianguoyun.com/dav/xmbox_history.json
// 确保 baseUrl / 结尾
String url = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
return url + filename;
}
}
/**
* 上传观看记录
*/
public boolean uploadHistory() {
if (!isConfigured()) {
Logger.e("WebDAV: 未配置,无法上传观看记录");
return false;
}
try {
// 获取所有观看记录 - 使用findAllRecent(0)来获取所有记录包括旧记录
Logger.d("WebDAV: 开始查询数据库中的观看记录...");
List<History> historyList = AppDatabase.get().getHistoryDao().findAllRecent(0);
Logger.d("WebDAV: 数据库查询完成,结果: " + (historyList == null ? "null" : historyList.size() + ""));
if (historyList == null) {
Logger.w("WebDAV: 查询结果为null,创建空列表");
historyList = new java.util.ArrayList<>();
}
// 修复数据中可能的编码问题重点修复key中的站点名称部分
Logger.d("WebDAV: 开始修复上传数据的编码问题...");
for (History h : historyList) {
String originalKey = h.getKey();
// key格式: 站点key$视频ID$cid需要单独修复站点key部分
String fixedKey = fixHistoryKey(originalKey);
if (!originalKey.equals(fixedKey)) {
Logger.d("WebDAV: 修复key编码: '" + originalKey + "' -> '" + fixedKey + "'");
h.setKey(fixedKey);
}
String originalName = h.getVodName();
String fixedName = fixEncodingIfNeeded(originalName);
if (!originalName.equals(fixedName)) {
Logger.d("WebDAV: 修复vodName编码: '" + originalName + "' -> '" + fixedName + "'");
h.setVodName(fixedName);
}
}
Logger.d("WebDAV: 准备上传观看记录,共 " + historyList.size() + "");
// 记录前3条数据的详细信息
for (int i = 0; i < Math.min(3, historyList.size()); i++) {
History h = historyList.get(i);
Logger.d("WebDAV: 上传记录[" + i + "] key=" + h.getKey() + ", vodName=" + h.getVodName());
// 检查key中的每个字符
String key = h.getKey();
StringBuilder hexDump = new StringBuilder();
for (int j = 0; j < Math.min(20, key.length()); j++) {
hexDump.append(String.format("%04x ", (int)key.charAt(j)));
}
Logger.d("WebDAV: key前20字符的Unicode: " + hexDump.toString());
}
String json = App.gson().toJson(historyList);
if (TextUtils.isEmpty(json)) {
Logger.w("WebDAV: JSON数据为空");
json = "[]"; // 确保至少有一个有效的JSON数组
}
// 记录JSON的前500个字符
Logger.d("WebDAV: JSON前500字符: " + json.substring(0, Math.min(500, json.length())));
// 确保目录存在如果baseUrl包含子目录
if (syncMode == SyncMode.ACCOUNT && !TextUtils.isEmpty(baseUrl)) {
try {
String dirUrl = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
ensureDirectory(dirUrl);
} catch (Exception e) {
Logger.w("WebDAV: 创建目录失败,尝试继续上传: " + e.getMessage());
// 继续尝试上传某些WebDAV服务可能不需要预先创建目录
}
}
// 上传文件
String fileUrl = getFileUrl(HISTORY_FILE);
Logger.d("WebDAV: 上传文件URL: " + fileUrl);
Logger.d("WebDAV: 上传数据大小: " + json.length() + " 字节");
byte[] data = json.getBytes("UTF-8");
// 对于坚果云等WebDAV服务直接上传文件即可会自动创建文件
// 如果文件已存在会被覆盖
sardine.put(fileUrl, data);
// 验证上传是否成功检查文件是否存在
if (sardine.exists(fileUrl)) {
Logger.d("WebDAV: 观看记录上传成功,共 " + historyList.size() + " 条,文件已确认存在");
return true;
} else {
Logger.e("WebDAV: 上传后文件不存在,可能上传失败");
return false;
}
} catch (Exception e) {
Logger.e("WebDAV: 观看记录上传失败: " + e.getMessage());
Logger.e("WebDAV: 异常类型: " + e.getClass().getName());
e.printStackTrace();
return false;
}
}
/**
* 下载观看记录
*/
public boolean downloadHistory() {
if (!isConfigured()) {
Logger.e("WebDAV: 未配置,无法下载观看记录");
Logger.e("WebDAV: baseUrl=" + baseUrl + ", username=" + username);
return false;
}
try {
String fileUrl = getFileUrl(HISTORY_FILE);
Logger.d("WebDAV: 检查文件是否存在: " + fileUrl);
// 检查文件是否存在
if (!sardine.exists(fileUrl)) {
Logger.w("WebDAV: 观看记录文件不存在,跳过下载");
return false;
}
Logger.d("WebDAV: 文件存在,开始下载");
// 下载文件使用循环读取避免available()不准确的问题
InputStream is = sardine.get(fileUrl);
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
is.close();
byte[] data = baos.toByteArray();
baos.close();
String json = new String(data, "UTF-8");
if (TextUtils.isEmpty(json)) {
Logger.d("WebDAV: 观看记录文件为空");
return true; // 文件存在但为空也算同步成功
}
Type listType = new TypeToken<List<History>>(){}.getType();
List<History> remoteHistoryList = App.gson().fromJson(json, listType);
// 验证数据
if (remoteHistoryList == null) {
Logger.e("WebDAV: JSON解析失败,返回null");
return false;
}
// 智能合并比较本地和远程记录保留较新的
List<History> localHistoryList = AppDatabase.get().getHistoryDao().findAllRecent(0);
Logger.d("WebDAV: 本地记录数: " + localHistoryList.size());
Logger.d("WebDAV: 远程记录数: " + remoteHistoryList.size());
// 修复远程记录的编码问题和时间戳
Logger.d("WebDAV: 开始修复远程记录编码和时间戳...");
long currentTime = System.currentTimeMillis();
long historyTimeLimit = currentTime - com.fongmi.android.tv.Constant.HISTORY_TIME; // 60天前
for (History remote : remoteHistoryList) {
if (remote != null) {
String originalKey = remote.getKey();
// 修复key中的站点名称部分
String fixedKey = fixHistoryKey(originalKey);
if (!originalKey.equals(fixedKey)) {
Logger.d("WebDAV: 修复远程key: '" + originalKey + "' -> '" + fixedKey + "'");
remote.setKey(fixedKey);
}
String originalName = remote.getVodName();
String fixedName = fixEncodingIfNeeded(originalName);
if (!originalName.equals(fixedName)) {
Logger.d("WebDAV: 修复远程vodName: '" + originalName + "' -> '" + fixedName + "'");
remote.setVodName(fixedName);
}
// 关键修复确保createTime在60天内否则会被过滤掉
long remoteCreateTime = remote.getCreateTime();
if (remoteCreateTime < historyTimeLimit) {
Logger.d("WebDAV: 修复过期时间戳: " + remote.getVodName() +
" createTime=" + remoteCreateTime + " -> " + currentTime +
" (已过期 " + ((currentTime - remoteCreateTime) / (24*60*60*1000)) + " 天)");
remote.setCreateTime(currentTime);
}
// 记录前3条远程数据的详细信息
if (remoteHistoryList.indexOf(remote) < 3) {
Logger.d("WebDAV: 远程记录[" + remoteHistoryList.indexOf(remote) + "]: " +
remote.getVodName() + " (key=" + remote.getKey() +
", cid=" + remote.getCid() +
", createTime=" + remote.getCreateTime() + ")");
}
}
}
// 修复本地记录的编码问题重要
Logger.d("WebDAV: 开始修复本地记录编码...");
for (History local : localHistoryList) {
if (local != null) {
String originalKey = local.getKey();
// 修复key中的站点名称部分
String fixedKey = fixHistoryKey(originalKey);
if (!originalKey.equals(fixedKey)) {
Logger.d("WebDAV: 修复本地key: '" + originalKey + "' -> '" + fixedKey + "'");
local.setKey(fixedKey);
}
// 记录前3条本地数据的详细信息
if (localHistoryList.indexOf(local) < 3) {
Logger.d("WebDAV: 本地记录[" + localHistoryList.indexOf(local) + "]: " +
local.getVodName() + " (key=" + local.getKey() +
", cid=" + local.getCid() +
", createTime=" + local.getCreateTime() + ")");
}
}
}
// 创建本地记录的映射key -> History
java.util.Map<String, History> localMap = new java.util.HashMap<>();
for (History local : localHistoryList) {
if (local != null && local.getKey() != null) {
localMap.put(local.getKey(), local);
}
}
Logger.d("WebDAV: 本地记录映射大小: " + localMap.size());
// 合并远程记录
List<History> toInsert = new java.util.ArrayList<>();
List<History> toUpdate = new java.util.ArrayList<>();
Logger.d("WebDAV: 开始合并 " + remoteHistoryList.size() + " 条远程记录...");
for (History remote : remoteHistoryList) {
// 验证远程记录
if (remote == null || TextUtils.isEmpty(remote.getKey())) {
Logger.w("WebDAV: 跳过无效的远程记录(key为空)");
continue;
}
History local = localMap.get(remote.getKey());
if (local == null) {
// 本地没有直接添加
Logger.d("WebDAV: 发现新记录: " + remote.getVodName() + " (key=" + remote.getKey() + ")");
toInsert.add(remote);
} else {
Logger.d("WebDAV: 本地已有记录: " + remote.getVodName() + ", 比较时间 remote=" + remote.getCreateTime() + " local=" + local.getCreateTime());
// 改进的合并策略优先保留较新的记录但也要比较播放进度
long remotePos = remote.getPosition();
long localPos = local.getPosition();
long remoteTime = remote.getCreateTime();
long localTime = local.getCreateTime();
boolean shouldUpdate = false;
String reason = "";
// 策略1如果远程时间更新直接更新
if (remoteTime > localTime) {
shouldUpdate = true;
reason = "远程时间更新 (" + remoteTime + " > " + localTime + ")";
}
// 策略2如果时间相同或相近误差1秒内比较播放进度
else if (Math.abs(remoteTime - localTime) <= 1000) {
if (remotePos >= 0 && localPos >= 0) {
if (remotePos > localPos) {
shouldUpdate = true;
reason = "播放进度更新 (" + remotePos + " > " + localPos + ")";
} else {
reason = "本地进度更新或相同";
}
} else if (remotePos >= 0 && localPos < 0) {
shouldUpdate = true;
reason = "远程有有效进度,本地无效";
} else {
reason = "保留本地";
}
}
// 策略3即使本地时间更新如果远程有更大的播放进度也更新
else if (remoteTime < localTime) {
if (remotePos >= 0 && localPos >= 0 && remotePos > localPos + 60000) {
// 远程进度领先本地超过1分钟可能是用户在另一台设备继续观看
shouldUpdate = true;
reason = "虽然本地时间更新,但远程进度显著领先 (" + remotePos + " > " + localPos + ")";
} else {
reason = "本地时间更新 (" + localTime + " > " + remoteTime + "),保留本地";
}
}
if (shouldUpdate) {
Logger.d("WebDAV: → 将更新本地 - " + reason);
toUpdate.add(remote);
} else {
Logger.d("WebDAV: → 保留本地 - " + reason);
}
}
}
Logger.d("WebDAV: 合并完成,待插入 " + toInsert.size() + " 条,待更新 " + toUpdate.size() + "");
// 执行插入和更新
if (!toInsert.isEmpty()) {
Logger.d("WebDAV: 开始插入 " + toInsert.size() + " 条新记录...");
AppDatabase.get().getHistoryDao().insert(toInsert);
Logger.d("WebDAV: 新增 " + toInsert.size() + " 条观看记录");
for (History h : toInsert) {
Logger.d("WebDAV: ✓ 新增 - " + h.getVodName() + " (cid=" + h.getCid() + ", key=" + h.getKey() + ")");
}
} else {
Logger.d("WebDAV: 没有需要插入的新记录");
}
if (!toUpdate.isEmpty()) {
Logger.d("WebDAV: 开始更新 " + toUpdate.size() + " 条记录...");
AppDatabase.get().getHistoryDao().update(toUpdate);
Logger.d("WebDAV: 更新 " + toUpdate.size() + " 条观看记录");
for (History h : toUpdate) {
Logger.d("WebDAV: ✓ 更新 - " + h.getVodName() + " (cid=" + h.getCid() + ")");
}
} else {
Logger.d("WebDAV: 没有需要更新的记录");
}
Logger.d("WebDAV: 观看记录合并完成,远程 " + remoteHistoryList.size() + " 条,本地 " + localHistoryList.size() + "");
// 验证数据库中的记录总数
List<History> allInDb = AppDatabase.get().getHistoryDao().findAllRecent(0);
Logger.d("WebDAV: 数据库中总共有 " + allInDb.size() + " 条观看记录");
// 输出数据库中前5条记录的详细信息
Logger.d("WebDAV: === 数据库中的记录(前5条)===");
for (int i = 0; i < Math.min(5, allInDb.size()); i++) {
History h = allInDb.get(i);
Logger.d("WebDAV: [" + i + "] " + h.getVodName() +
" (key=" + h.getKey() +
", cid=" + h.getCid() +
", createTime=" + h.getCreateTime() + ")");
}
Logger.d("WebDAV: =========================");
// 强制触发UI刷新即使没有新增或更新也刷新一次以确保显示
Logger.d("WebDAV: 触发UI刷新事件");
App.post(() -> {
RefreshEvent.history();
Logger.d("WebDAV: UI刷新事件已发送到主线程");
});
return true; // 即使远程为空也算同步成功
} catch (Exception e) {
Logger.e("WebDAV: 观看记录下载失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* 上传设置
*/
public boolean uploadSettings() {
if (!isConfigured()) {
Logger.e("WebDAV: 未配置,无法上传设置");
return false;
}
try {
// 获取所有设置
Map<String, ?> allPrefs = Prefers.getPrefers().getAll();
String json = App.gson().toJson(allPrefs);
// 确保目录存在如果baseUrl包含子目录
if (syncMode == SyncMode.ACCOUNT && !TextUtils.isEmpty(baseUrl)) {
try {
String dirUrl = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
ensureDirectory(dirUrl);
} catch (Exception e) {
Logger.w("WebDAV: 创建目录失败,尝试继续上传: " + e.getMessage());
}
}
// 上传文件
String fileUrl = getFileUrl(SETTINGS_FILE);
byte[] data = json.getBytes("UTF-8");
sardine.put(fileUrl, data);
Logger.d("WebDAV: 设置上传成功");
return true;
} catch (Exception e) {
Logger.e("WebDAV: 设置上传失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* 下载设置
*/
public boolean downloadSettings() {
if (!isConfigured()) {
Logger.e("WebDAV: 未配置,无法下载设置");
return false;
}
try {
String fileUrl = getFileUrl(SETTINGS_FILE);
// 检查文件是否存在
if (!sardine.exists(fileUrl)) {
Logger.d("WebDAV: 设置文件不存在,跳过下载");
return false;
}
// 下载文件
InputStream is = sardine.get(fileUrl);
byte[] buffer = new byte[is.available()];
is.read(buffer);
is.close();
String json = new String(buffer, "UTF-8");
Gson gson = App.gson();
Map<String, Object> settings = gson.fromJson(json, Map.class);
// 应用设置合并不覆盖已存在的
if (settings != null && !settings.isEmpty()) {
for (Map.Entry<String, Object> entry : settings.entrySet()) {
// 只同步非敏感设置跳过某些本地设置
String key = entry.getKey();
if (!shouldSkipSetting(key)) {
Prefers.put(key, entry.getValue());
}
}
Logger.d("WebDAV: 设置下载成功");
return true;
}
return false;
} catch (Exception e) {
Logger.e("WebDAV: 设置下载失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* 判断是否应该跳过某个设置项
*/
private boolean shouldSkipSetting(String key) {
// 跳过WebDAV相关设置避免循环同步
if (key.startsWith("webdav_")) {
return true;
}
// 跳过设备特定设置
if (key.equals("device_uuid") || key.equals("device_name")) {
return true;
}
return false;
}
/**
* 上传完整备份包含所有数据
*/
public boolean uploadBackup() {
if (!isConfigured()) {
Logger.e("WebDAV: 未配置,无法上传备份");
return false;
}
try {
Backup backup = Backup.create();
String json = backup.toString();
// 确保目录存在如果baseUrl包含子目录
if (syncMode == SyncMode.ACCOUNT && !TextUtils.isEmpty(baseUrl)) {
try {
String dirUrl = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
ensureDirectory(dirUrl);
} catch (Exception e) {
Logger.w("WebDAV: 创建目录失败,尝试继续上传: " + e.getMessage());
}
}
// 上传文件
String fileUrl = getFileUrl(BACKUP_FILE);
byte[] data = json.getBytes("UTF-8");
sardine.put(fileUrl, data);
Logger.d("WebDAV: 完整备份上传成功");
return true;
} catch (Exception e) {
Logger.e("WebDAV: 完整备份上传失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* 下载完整备份
*/
public boolean downloadBackup() {
if (!isConfigured()) {
Logger.e("WebDAV: 未配置,无法下载备份");
return false;
}
try {
String fileUrl = getFileUrl(BACKUP_FILE);
// 检查文件是否存在
if (!sardine.exists(fileUrl)) {
Logger.d("WebDAV: 备份文件不存在,跳过下载");
return false;
}
// 下载文件
InputStream is = sardine.get(fileUrl);
byte[] buffer = new byte[is.available()];
is.read(buffer);
is.close();
String json = new String(buffer, "UTF-8");
Backup backup = Backup.objectFrom(json);
// 恢复备份
if (!backup.getConfig().isEmpty()) {
backup.restore();
Logger.d("WebDAV: 完整备份下载并恢复成功");
return true;
}
return false;
} catch (Exception e) {
Logger.e("WebDAV: 完整备份下载失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* 同步观看记录上传+下载合并
* @param async 是否异步执行true=异步false=同步阻塞
*/
public boolean syncHistory(boolean async) {
if (!isConfigured()) {
return false;
}
// 防止重复同步
if (isSyncing) {
Logger.w("WebDAV: 同步正在进行中,跳过本次请求");
return false;
}
Runnable syncTask = () -> {
try {
isSyncing = true;
// 先上传本地记录
uploadHistory();
// 再下载远程记录并合并
downloadHistory();
} finally {
isSyncing = false;
}
};
if (async) {
App.execute(syncTask);
} else {
syncTask.run();
}
return true;
}
/**
* 同步观看记录异步执行默认
*/
public boolean syncHistory() {
return syncHistory(true);
}
/**
* 同步设置上传+下载合并
* @param async 是否异步执行
*/
public boolean syncSettings(boolean async) {
if (!isConfigured()) {
return false;
}
Runnable syncTask = () -> {
// 先上传本地设置
uploadSettings();
// 再下载远程设置并合并
downloadSettings();
};
if (async) {
App.execute(syncTask);
} else {
syncTask.run();
}
return true;
}
/**
* 同步设置异步执行默认
*/
public boolean syncSettings() {
return syncSettings(true);
}
/**
* 完整同步观看记录+设置
* @param async 是否异步执行
*/
public boolean syncAll(boolean async) {
if (!isConfigured()) {
return false;
}
// 防止重复同步
if (isSyncing) {
Logger.w("WebDAV: 同步正在进行中,跳过本次请求");
return false;
}
Runnable syncTask = () -> {
try {
isSyncing = true;
// 先上传本地记录
uploadHistory();
// 再下载远程记录并合并
downloadHistory();
// 同步设置
syncSettings(false); // 设置同步使用同步方式避免嵌套异步
} finally {
isSyncing = false;
}
};
if (async) {
App.execute(syncTask);
} else {
syncTask.run();
}
return true;
}
/**
* 完整同步异步执行默认
*/
public boolean syncAll() {
return syncAll(true);
}
/**
* 重新加载配置配置更改后调用
*/
public void reloadConfig() {
loadConfig();
}
/**
* 修复History的key中的站点名称编码
* key格式: 站点key$视频ID$cid
*/
private String fixHistoryKey(String key) {
if (key == null || key.isEmpty()) {
return key;
}
try {
// 使用AppDatabase.SYMBOL分隔
String symbol = com.fongmi.android.tv.db.AppDatabase.SYMBOL;
String[] parts = key.split(java.util.regex.Pattern.quote(symbol));
if (parts.length >= 3) {
// parts[0] = 站点key, parts[1] = 视频ID, parts[2] = cid
String siteKey = parts[0];
String fixedSiteKey = fixEncodingIfNeeded(siteKey);
if (!siteKey.equals(fixedSiteKey)) {
// 重新组装key
StringBuilder newKey = new StringBuilder(fixedSiteKey);
for (int i = 1; i < parts.length; i++) {
newKey.append(symbol).append(parts[i]);
}
return newKey.toString();
}
}
} catch (Exception e) {
Logger.e("WebDAV: 修复History key失败: " + e.getMessage());
}
return key;
}
/**
* 修复字符串编码问题
* 尝试将错误编码的UTF-8字符串修复为正确的UTF-8
*/
private String fixEncodingIfNeeded(String str) {
if (str == null || str.isEmpty()) {
return str;
}
try {
// 检查字符串中是否包含明显的乱码特征
// 1. 包含替换字符 U+FFFD
// 2. 包含异常的低位控制字符
boolean needsFix = false;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (c == '\uFFFD' || (c >= 0x80 && c < 0xA0)) {
needsFix = true;
break;
}
}
if (needsFix) {
// 尝试修复假设原始数据是UTF-8但被错误地当作ISO-8859-1解码
byte[] bytes = str.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
String fixed = new String(bytes, java.nio.charset.StandardCharsets.UTF_8);
Logger.d("WebDAV: 编码修复 '" + str + "' -> '" + fixed + "'");
return fixed;
}
} catch (Exception e) {
Logger.e("WebDAV: 编码修复失败: " + e.getMessage());
}
return str;
}
}
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:topLeftRadius="8dp"
android:topRightRadius="8dp" /> android:topRightRadius="8dp" />
<solid android:color="#B32196F3" /> <solid android:color="#B3000000" />
<padding <padding
android:bottom="4dp" 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:layout_marginTop="16dp"
android:text="@string/error_empty" android:text="@string/error_empty"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="16sp" /> android:textSize="14sp" />
</LinearLayout> </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"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/white" /> <!-- 使用纯色背景,自动适配深浅色模式 -->
<background android:drawable="@color/launcher_background" />
<!-- 前景图标:使用 inset 缩小显示(因为图标铺满了画布)-->
<foreground> <foreground>
<inset <inset
android:drawable="@mipmap/ic_launcher_foreground" android:drawable="@mipmap/ic_launcher_foreground"
android:inset="20%" /> android:inset="20%" />
</foreground> </foreground>
<!-- 主题图标:也需要使用 inset 保持大小一致 -->
<monochrome>
<inset
android:drawable="@mipmap/ic_launcher_foreground"
android:inset="20%" />
</monochrome>
</adaptive-icon> </adaptive-icon>
@@ -1,9 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/white" /> <!-- 使用纯色背景,自动适配深浅色模式 -->
<background android:drawable="@color/launcher_background" />
<!-- 前景图标:使用 inset 缩小显示(因为图标铺满了画布)-->
<foreground> <foreground>
<inset <inset
android:drawable="@mipmap/ic_launcher_foreground" android:drawable="@mipmap/ic_launcher_foreground"
android:inset="20%" /> android:inset="20%" />
</foreground> </foreground>
<!-- 主题图标:也需要使用 inset 保持大小一致 -->
<monochrome>
<inset
android:drawable="@mipmap/ic_launcher_foreground"
android:inset="20%" />
</monochrome>
</adaptive-icon> </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_subscription">订阅管理</string>
<string name="setting_player">播放设置</string> <string name="setting_player">播放设置</string>
<string name="setting_incognito">无痕模式</string> <string name="setting_incognito">无痕模式</string>
<string name="setting_live_tab_visible">隐藏直播</string>
<string name="setting_quality">图片品质</string> <string name="setting_quality">图片品质</string>
<string name="setting_size">图片尺寸</string> <string name="setting_size">图片尺寸</string>
<string name="setting_doh">DoH</string> <string name="setting_doh">DoH</string>
<string name="setting_proxy">Proxy</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_backup">备份</string>
<string name="setting_restore">恢复</string> <string name="setting_restore">恢复</string>
<string name="setting_version">版本</string> <string name="setting_version">版本</string>
@@ -88,6 +91,8 @@
<string name="setting_app">应用设置</string> <string name="setting_app">应用设置</string>
<string name="setting_network">网络设置</string> <string name="setting_network">网络设置</string>
<string name="setting_data">数据管理</string> <string name="setting_data">数据管理</string>
<string name="app_version">v3.1.1</string>
<string name="about_github">在GitHub上查看</string>
<!-- Backup & Restore --> <!-- Backup & Restore -->
<string name="restore_select">选择备份</string> <string name="restore_select">选择备份</string>
@@ -121,7 +126,7 @@
<string name="dialog_edit">修改</string> <string name="dialog_edit">修改</string>
<string name="dialog_positive">确定</string> <string name="dialog_positive">确定</string>
<string name="dialog_negative">取消</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_hint">请输入接口…</string>
<string name="dialog_config_name">请输入名称…</string> <string name="dialog_config_name">请输入名称…</string>
<string name="dialog_config_url">请输入地址…</string> <string name="dialog_config_url">请输入地址…</string>
@@ -143,6 +148,7 @@
<string name="error_cast_file">不支持的文件格式</string> <string name="error_cast_file">不支持的文件格式</string>
<string name="error_device_limit">设备授权数已达上限</string> <string name="error_device_limit">设备授权数已达上限</string>
<string name="error_live_empty">该订阅无直播内容</string> <string name="error_live_empty">该订阅无直播内容</string>
<string name="error_no_live">当前源没有直播内容</string>
<!-- Update --> <!-- Update -->
<string name="update_version">发现新版本 <xliff:g name="name">%s</xliff:g></string> <string name="update_version">发现新版本 <xliff:g name="name">%s</xliff:g></string>
@@ -166,6 +172,7 @@
<string name="none"></string> <string name="none"></string>
<string name="times"></string> <string name="times"></string>
<string name="lines"></string> <string name="lines"></string>
<string name="last_watch">上次播放</string>
<string-array name="select_decode"> <string-array name="select_decode">
<item>软解</item> <item>软解</item>
@@ -209,4 +216,10 @@
<item>选择字幕</item> <item>选择字幕</item>
</string-array> </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> </resources>
+14 -2
View File
@@ -76,7 +76,9 @@
<string name="setting_size">圖片尺寸</string> <string name="setting_size">圖片尺寸</string>
<string name="setting_doh">DoH</string> <string name="setting_doh">DoH</string>
<string name="setting_proxy">Proxy</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_backup">備份</string>
<string name="setting_restore">還原</string> <string name="setting_restore">還原</string>
<string name="setting_version">版本</string> <string name="setting_version">版本</string>
@@ -87,6 +89,8 @@
<string name="setting_app">應用設置</string> <string name="setting_app">應用設置</string>
<string name="setting_network">網絡設置</string> <string name="setting_network">網絡設置</string>
<string name="setting_data">數據管理</string> <string name="setting_data">數據管理</string>
<string name="app_version">v3.1.1</string>
<string name="about_github">在GitHub上查看</string>
<!-- Backup & Restore --> <!-- Backup & Restore -->
<string name="restore_select">選擇備份</string> <string name="restore_select">選擇備份</string>
@@ -137,7 +141,11 @@
<string name="error_play_flag">暫無線路資料</string> <string name="error_play_flag">暫無線路資料</string>
<string name="error_play_timeout">連線逾時</string> <string name="error_play_timeout">連線逾時</string>
<string name="error_detail">暫無播放資料</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 --> <!-- Update -->
<string name="update_version">發現新版本 <xliff:g name="name">%s</xliff:g></string> <string name="update_version">發現新版本 <xliff:g name="name">%s</xliff:g></string>
@@ -204,4 +212,8 @@
<item>選擇字幕</item> <item>選擇字幕</item>
</string-array> </string-array>
<string name="config_set_current">已設為當前點播源</string>
<string name="source_hint">點我添加源</string>
</resources> </resources>
+3
View File
@@ -28,4 +28,7 @@
<color name="text_toast">#FFEB3B</color> <color name="text_toast">#FFEB3B</color>
<color name="yellow_500">#FFEB3B</color> <color name="yellow_500">#FFEB3B</color>
<!-- Launcher icon background (Light mode) -->
<color name="launcher_background">#FFFFFF</color>
</resources> </resources>
+34 -7
View File
@@ -65,7 +65,7 @@
<!-- Push --> <!-- Push -->
<string name="push">Push</string> <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 --> <!-- Setting -->
<string name="setting_vod">Vod</string> <string name="setting_vod">Vod</string>
@@ -78,17 +78,22 @@
<string name="setting_data">Data Management</string> <string name="setting_data">Data Management</string>
<string name="setting_player">Player setting</string> <string name="setting_player">Player setting</string>
<string name="setting_incognito">Incognito mode</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_quality">Image quality</string>
<string name="setting_size">Image size</string> <string name="setting_size">Image size</string>
<string name="setting_doh">DoH</string> <string name="setting_doh">DoH</string>
<string name="setting_proxy">Proxy</string> <string name="setting_proxy">Proxy</string>
<string name="setting_cache">Cache</string> <string name="setting_cache">缓存</string>
<string name="setting_backup">Backup</string> <string name="setting_auto_clean">Auto Clean Cache</string>
<string name="setting_restore">Restore</string> <string name="setting_cache_threshold">Cache Threshold</string>
<string name="setting_version">Version</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_choose">Choose</string>
<string name="setting_off">Off</string> <string name="setting_off">Off</string>
<string name="setting_on">On</string> <string name="setting_on">On</string>
<string name="app_version">v3.1.1</string>
<string name="about_github">View on GitHub</string>
<!-- Backup & Restore --> <!-- Backup & Restore -->
<string name="restore_select">Select backup</string> <string name="restore_select">Select backup</string>
@@ -122,7 +127,7 @@
<string name="dialog_edit">Edit</string> <string name="dialog_edit">Edit</string>
<string name="dialog_positive">OK</string> <string name="dialog_positive">OK</string>
<string name="dialog_negative">Cancel</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_hint">Please enter the config…</string>
<string name="dialog_config_name">Please enter the name…</string> <string name="dialog_config_name">Please enter the name…</string>
<string name="dialog_config_url">Please enter the url…</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_cast_file">Unsupported file format</string>
<string name="error_device_limit">Device authorization limit reached</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_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_detail">No play data</string>
<string name="error_play_flag">No flag 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> <string name="error_play_code">Error code: <xliff:g name="name">%s</xliff:g></string>
@@ -168,12 +176,14 @@
<!-- Hint --> <!-- Hint -->
<string name="copied">Copied</string> <string name="copied">Copied</string>
<string name="copied_to_clipboard">Error log copied to clipboard</string>
<!-- UNIT --> <!-- UNIT -->
<string name="all">All</string> <string name="all">All</string>
<string name="none">None</string> <string name="none">None</string>
<string name="times">times</string> <string name="times">times</string>
<string name="lines">lines</string> <string name="lines">lines</string>
<string name="last_watch">上次播放</string>
<string-array name="select_decode"> <string-array name="select_decode">
<item>Soft</item> <item>Soft</item>
@@ -224,4 +234,21 @@
<string name="config_set_current">已设为当前点播源</string> <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> </resources>
+12
View File
@@ -10,6 +10,13 @@
<application> <application>
<activity
android:name=".ui.activity.PrivacyAgreementActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode|orientation"
android:exported="false"
android:screenOrientation="fullUser"
android:windowSoftInputMode="adjustPan" />
<activity <activity
android:name=".ui.activity.HomeActivity" android:name=".ui.activity.HomeActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode|orientation" android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode|orientation"
@@ -106,6 +113,11 @@
android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode" android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode"
android:screenOrientation="fullUser" /> android:screenOrientation="fullUser" />
<activity
android:name=".ui.activity.SettingPlayerActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode"
android:screenOrientation="fullUser" />
<activity <activity
android:name=".ui.activity.VideoActivity" android:name=".ui.activity.VideoActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode|orientation" android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode|orientation"
@@ -2,6 +2,8 @@ package com.fongmi.android.tv;
import android.app.Activity; import android.app.Activity;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -12,11 +14,14 @@ import com.fongmi.android.tv.utils.Download;
import com.fongmi.android.tv.utils.FileUtil; import com.fongmi.android.tv.utils.FileUtil;
import com.fongmi.android.tv.utils.Notify; import com.fongmi.android.tv.utils.Notify;
import com.fongmi.android.tv.utils.ResUtil; import com.fongmi.android.tv.utils.ResUtil;
import com.fongmi.android.tv.utils.UpdateInstaller;
import com.github.catvod.net.OkHttp; import com.github.catvod.net.OkHttp;
import com.github.catvod.utils.Github; import com.github.catvod.utils.Github;
import com.github.catvod.utils.Logger;
import com.github.catvod.utils.Path; import com.github.catvod.utils.Path;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import java.io.File; import java.io.File;
@@ -25,20 +30,34 @@ import java.util.Locale;
public class Updater implements Download.Callback { public class Updater implements Download.Callback {
private DialogUpdateBinding binding; private DialogUpdateBinding binding;
private final Download download; private Download download;
private AlertDialog dialog; private AlertDialog dialog;
private boolean dev; private boolean dev;
private boolean forceCheck; // 是否为手动检查
private boolean autoShow; // 是否自动显示更新对话框用于自动检查
private String latestVersion; // 存储检测到的最新版本
private String releaseApkUrl; // GitHub Release 获取的 APK 下载链接jsDelivr CDN
private String fallbackApkUrl; // 备用下载链接GitHub原始URL
// 静态变量记录上次检查时间用于时间间隔限制
private static long lastCheckTime = 0;
private static final long CHECK_INTERVAL = 60 * 60 * 1000; // 1小时毫秒
private File getFile() { private File getFile() {
return Path.cache("update.apk"); // Android 10+ 无法直接访问外部存储的Download目录
} // 使用应用的cache目录FileProvider可以正常访问
return Path.cache("XMBOX-update.apk");
private String getJson() {
return Github.getJson(dev, BuildConfig.FLAVOR_mode);
} }
private String getApk() { private String getApk() {
return Github.getApk(dev, BuildConfig.FLAVOR_mode + "-" + BuildConfig.FLAVOR_abi); // 使用从 GitHub Release 获取的 APK URLjsDelivr CDN
if (releaseApkUrl != null && !releaseApkUrl.isEmpty()) {
Logger.d("APK download URL from Release (jsDelivr): " + releaseApkUrl);
return releaseApkUrl;
}
// 如果没有获取到URL返回空不应该发生
Logger.e("Updater: 未找到APK下载链接");
return "";
} }
public static Updater create() { public static Updater create() {
@@ -46,12 +65,24 @@ public class Updater implements Download.Callback {
} }
public Updater() { public Updater() {
this.download = Download.create(getApk(), getFile(), this); this.forceCheck = false;
this.autoShow = false;
// download对象将在需要时创建
} }
public Updater force() { public Updater force() {
Notify.show(R.string.update_check); Notify.show(R.string.update_check);
Setting.putUpdate(true); Setting.putUpdate(true);
this.forceCheck = true; // 标记为手动检查
return this;
}
/**
* 设置自动检查模式应用启动时自动检查
*/
public Updater auto() {
this.forceCheck = false;
this.autoShow = true; // 自动显示更新对话框
return this; return this;
} }
@@ -71,6 +102,16 @@ public class Updater implements Download.Callback {
} }
public void start(Activity activity) { public void start(Activity activity) {
// 如果是自动检查检查时间间隔
if (autoShow && !forceCheck) {
long currentTime = System.currentTimeMillis();
long timeSinceLastCheck = currentTime - lastCheckTime;
// 1小时内只检查一次
if (lastCheckTime > 0 && timeSinceLastCheck < CHECK_INTERVAL) {
Logger.d("Updater: 距离上次检查仅 " + (timeSinceLastCheck / 1000 / 60) + " 分钟,跳过本次检查");
return;
}
}
App.execute(() -> doInBackground(activity)); App.execute(() -> doInBackground(activity));
} }
@@ -79,14 +120,205 @@ public class Updater implements Download.Callback {
} }
private void doInBackground(Activity activity) { private void doInBackground(Activity activity) {
Logger.d("Updater: Starting update check...");
lastCheckTime = System.currentTimeMillis(); // 更新检查时间
// 直接使用 GitHub Releases API 检查更新
checkViaGitHubAPI(activity);
}
private void checkViaGitHubAPI(Activity activity) {
try { try {
JSONObject object = new JSONObject(OkHttp.string(getJson())); String releasesUrl = "https://api.github.com/repos/Tosencen/XMBOX/releases/latest";
String name = object.optString("name"); Logger.d("Updater: Trying GitHub Releases API: " + releasesUrl);
String desc = object.optString("desc");
int code = object.optInt("code"); // 检查是否有GitHub Token
if (need(code, name)) App.post(() -> show(activity, name, desc)); String githubToken = BuildConfig.GITHUB_TOKEN;
String response;
if (githubToken != null && !githubToken.isEmpty()) {
// 使用token进行认证请求5000次/小时
java.util.Map<String, String> headers = new java.util.HashMap<>();
headers.put("Authorization", "Bearer " + githubToken);
headers.put("Accept", "application/vnd.github.v3+json");
Logger.d("Updater: Using GitHub Token for authenticated request");
response = OkHttp.string(releasesUrl, headers);
} else {
// 使用未认证请求60次/小时
Logger.d("Updater: Using unauthenticated request (60 requests/hour limit)");
response = OkHttp.string(releasesUrl);
}
// 检查响应是否为空可能是网络错误VPN问题等
if (response == null || response.isEmpty()) {
Logger.e("Updater: 网络请求失败,响应为空。可能是网络连接问题或VPN配置问题");
if (forceCheck) {
// 手动检查时显示错误提示
App.post(() -> {
Notify.show("检查更新失败:网络连接异常,请检查网络设置或VPN配置");
showVersionInfo(activity, BuildConfig.VERSION_NAME, "");
});
} else {
Logger.w("Updater: 自动检查失败,网络不可用");
}
return;
}
if (response.contains("rate limit exceeded") || response.contains("API rate limit exceeded")) {
Logger.e("Updater: Rate limit exceeded");
if (forceCheck) {
// 手动检查时显示版本信息弹窗不显示错误提示
App.post(() -> showVersionInfo(activity, BuildConfig.VERSION_NAME, ""));
}
return;
}
if (response.contains("Not Found") || response.contains("404")) {
Logger.e("Updater: Release not found");
if (forceCheck) {
// 手动检查时显示版本信息弹窗不显示错误提示
App.post(() -> showVersionInfo(activity, BuildConfig.VERSION_NAME, ""));
}
return;
}
JSONObject release = new JSONObject(response);
String tagName = release.optString("tag_name");
String body = release.optString("body");
String version = tagName.startsWith("v") ? tagName.substring(1) : tagName;
Logger.d("Updater: GitHub API Remote version: " + version);
// assets 中查找 APK
JSONArray assets = release.optJSONArray("assets");
if (assets != null) {
String mode = BuildConfig.FLAVOR_mode;
String abi = BuildConfig.FLAVOR_abi;
// 尝试多种文件名格式
String[] possibleNames = {
mode + "-" + abi + "-v" + version + ".apk", // mobile-arm64_v8a-v3.1.0.apk
mode + "-" + abi + "-release.apk", // mobile-arm64_v8a-release.apk
mode + "-" + abi + ".apk", // mobile-arm64_v8a.apk
mode + "-" + abi + "-" + version + ".apk" // mobile-arm64_v8a-3.1.0.apk
};
boolean found = false;
for (int i = 0; i < assets.length() && !found; i++) {
JSONObject asset = assets.getJSONObject(i);
String assetName = asset.optString("name");
// 检查是否匹配任何可能的文件名格式
for (String targetName : possibleNames) {
if (targetName.equals(assetName)) {
String githubUrl = asset.optString("browser_download_url");
// jsDelivr无法访问GitHub Release文件直接使用GitHub Release URL
// 如果GitHub访问慢可以配置代理或使用其他CDN
this.releaseApkUrl = githubUrl;
this.fallbackApkUrl = githubUrl;
Logger.d("Updater: 找到匹配的APK: " + assetName);
Logger.d("Updater: APK URL (GitHub Release): " + this.releaseApkUrl);
found = true;
break;
}
}
}
// 如果精确匹配失败尝试模糊匹配包含mode和abi的APK文件
if (!found) {
Logger.w("Updater: 未找到精确匹配的APK,尝试模糊匹配...");
for (int i = 0; i < assets.length(); i++) {
JSONObject asset = assets.getJSONObject(i);
String assetName = asset.optString("name");
// 检查文件名是否包含mode和abi且是APK文件
if (assetName.endsWith(".apk") &&
assetName.contains(mode) &&
assetName.contains(abi.replace("_", "-"))) {
String githubUrl = asset.optString("browser_download_url");
// jsDelivr无法访问GitHub Release文件直接使用GitHub Release URL
this.releaseApkUrl = githubUrl;
this.fallbackApkUrl = githubUrl;
Logger.d("Updater: 找到模糊匹配的APK: " + assetName);
Logger.d("Updater: APK URL (GitHub Release): " + this.releaseApkUrl);
found = true;
break;
}
}
}
if (!found) {
Logger.e("Updater: 在Release中未找到匹配的APK文件");
Logger.e("Updater: 期望的格式: " + mode + "-" + abi + "-v" + version + ".apk");
Logger.e("Updater: 可用的assets:");
for (int i = 0; i < assets.length(); i++) {
JSONObject asset = assets.getJSONObject(i);
String assetName = asset.optString("name");
if (assetName.endsWith(".apk")) {
Logger.e("Updater: - " + assetName);
}
}
}
} else {
Logger.e("Updater: Release中没有assets数组");
}
if (needUpdate(version)) {
this.latestVersion = version;
// 有新版本时自动显示或手动显示更新对话框
App.post(() -> show(activity, version, body));
} else {
// 没有新版本
if (forceCheck) {
// 手动检查时显示版本信息弹窗
App.post(() -> showVersionInfo(activity, version, body));
} else if (autoShow) {
// 自动检查时不显示任何内容静默检查
Logger.d("Updater: 自动检查完成,当前已是最新版本");
}
}
} catch (Exception e) { } catch (Exception e) {
Logger.e("Updater: GitHub API check failed: " + e.getMessage());
e.printStackTrace(); e.printStackTrace();
if (forceCheck) {
// 手动检查时显示错误提示
String errorMsg = e.getMessage();
if (errorMsg != null && (errorMsg.contains("network") || errorMsg.contains("timeout") || errorMsg.contains("connect"))) {
App.post(() -> {
Notify.show("检查更新失败:网络连接异常,请检查网络设置或VPN配置");
showVersionInfo(activity, BuildConfig.VERSION_NAME, "");
});
} else {
App.post(() -> showVersionInfo(activity, BuildConfig.VERSION_NAME, ""));
}
} else {
Logger.w("Updater: 自动检查失败: " + e.getMessage());
}
}
}
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;
} }
} }
@@ -98,19 +330,51 @@ public class Updater implements Download.Callback {
binding.desc.setText(desc); binding.desc.setText(desc);
} }
/**
* 显示版本信息弹窗无更新时
*/
private void showVersionInfo(Activity activity, String remoteVersion, String desc) {
binding = DialogUpdateBinding.inflate(LayoutInflater.from(activity));
// 先设置内容只显示当前版本号不使用远程信息
binding.desc.setText(BuildConfig.VERSION_NAME);
check().create(activity, "最新版本").show();
// 隐藏确认按钮只显示取消按钮改为"确定"
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setVisibility(View.GONE);
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setText("确定");
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(v -> {
if (dialog != null) dialog.dismiss();
});
}
private AlertDialog create(Activity activity, String title) { private AlertDialog create(Activity activity, String title) {
return dialog = new MaterialAlertDialogBuilder(activity).setTitle(title).setView(binding.getRoot()).setPositiveButton(R.string.update_confirm, null).setNegativeButton(R.string.dialog_negative, null).setCancelable(false).create(); return dialog = new MaterialAlertDialogBuilder(activity).setTitle(title).setView(binding.getRoot()).setPositiveButton(R.string.update_confirm, null).setNegativeButton(R.string.dialog_negative, null).setCancelable(false).create();
} }
private void cancel(View view) { private void cancel(View view) {
Setting.putUpdate(false); Setting.putUpdate(false);
if (download != null) {
download.cancel(); download.cancel();
}
dialog.dismiss(); dialog.dismiss();
} }
private void confirm(View view) { private void confirm(View view) {
view.setEnabled(false); // 开始下载更新使用jsDelivr CDN失败时回退到GitHub
download.start(); String downloadUrl = getApk();
String fallbackUrl = this.fallbackApkUrl;
// 检查URL是否为空
if (downloadUrl == null || downloadUrl.isEmpty()) {
Logger.e("Updater: 下载URL为空,无法下载");
Notify.show("无法获取下载链接,请稍后重试或手动下载");
return;
}
Logger.d("Updater: 开始下载,URL: " + downloadUrl);
// 创建带回退URL的下载对象
this.download = Download.create(downloadUrl, getFile(), fallbackUrl, this);
this.download.start();
} }
private void dismiss() { private void dismiss() {
@@ -133,7 +397,30 @@ public class Updater implements Download.Callback {
@Override @Override
public void success(File file) { public void success(File file) {
FileUtil.openFile(file); // 使用UpdateInstaller处理安装包括权限检查和请求
UpdateInstaller installer = UpdateInstaller.get();
// 检查安装权限
if (!installer.hasInstallPermission()) {
// 没有权限请求权限并保存待安装的文件
Logger.d("Updater: 没有安装权限,请求权限");
installer.requestInstallPermission();
// 保存待安装的文件将在权限授予后自动安装
installer.install(file, true); // checkPermission=true会保存文件
Notify.show("请授予安装权限以完成更新");
dismiss();
return;
}
// 有权限直接安装
boolean success = installer.install(file, false);
if (success) {
Logger.d("Updater: 已启动安装程序");
dismiss();
} else {
Logger.e("Updater: 启动安装程序失败");
Notify.show("无法启动安装程序,请检查文件是否完整");
dismiss(); dismiss();
} }
} }
}
@@ -44,6 +44,7 @@ import com.fongmi.android.tv.utils.Util;
import com.github.catvod.net.OkHttp; import com.github.catvod.net.OkHttp;
import com.google.android.flexbox.FlexDirection; import com.google.android.flexbox.FlexDirection;
import com.google.android.flexbox.FlexboxLayoutManager; import com.google.android.flexbox.FlexboxLayoutManager;
import com.airbnb.lottie.LottieAnimationView;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder; import java.net.URLEncoder;
@@ -151,12 +152,14 @@ public class CollectActivity extends BaseActivity implements CustomScroller.Call
if (mCollectAdapter.getPosition() == 0) mSearchAdapter.addAll(result.getList()); if (mCollectAdapter.getPosition() == 0) mSearchAdapter.addAll(result.getList());
mCollectAdapter.add(Collect.create(result.getList())); mCollectAdapter.add(Collect.create(result.getList()));
mCollectAdapter.add(result.getList()); mCollectAdapter.add(result.getList());
updateEmptyState();
}); });
mViewModel.result.observe(this, result -> { mViewModel.result.observe(this, result -> {
boolean same = !result.getList().isEmpty() && mCollectAdapter.getActivated().getSite().equals(result.getList().get(0).getSite()); boolean same = !result.getList().isEmpty() && mCollectAdapter.getActivated().getSite().equals(result.getList().get(0).getSite());
if (same) mCollectAdapter.getActivated().getList().addAll(result.getList()); if (same) mCollectAdapter.getActivated().getList().addAll(result.getList());
if (same) mSearchAdapter.addAll(result.getList()); if (same) mSearchAdapter.addAll(result.getList());
mScroller.endLoading(result); mScroller.endLoading(result);
updateEmptyState();
}); });
} }
@@ -187,6 +190,7 @@ public class CollectActivity extends BaseActivity implements CustomScroller.Call
mBinding.agent.setVisibility(View.GONE); mBinding.agent.setVisibility(View.GONE);
mBinding.view.setVisibility(View.VISIBLE); mBinding.view.setVisibility(View.VISIBLE);
mBinding.result.setVisibility(View.VISIBLE); mBinding.result.setVisibility(View.VISIBLE);
updateEmptyState(); // 搜索开始时显示空状态
if (mExecutor != null) mExecutor.shutdownNow(); if (mExecutor != null) mExecutor.shutdownNow();
mExecutor = new PauseExecutor(20); mExecutor = new PauseExecutor(20);
String keyword = mBinding.keyword.getText().toString().trim(); 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); 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) { private void search(Site site, String keyword) {
try { try {
mViewModel.searchContent(site, keyword, false); mViewModel.searchContent(site, keyword, false);
@@ -235,6 +260,7 @@ public class CollectActivity extends BaseActivity implements CustomScroller.Call
mBinding.result.setVisibility(View.GONE); mBinding.result.setVisibility(View.GONE);
mBinding.site.setVisibility(View.VISIBLE); mBinding.site.setVisibility(View.VISIBLE);
mBinding.agent.setVisibility(View.VISIBLE); mBinding.agent.setVisibility(View.VISIBLE);
mBinding.emptyLayout.getRoot().setVisibility(View.GONE); // 隐藏空状态动画
if (mExecutor != null) mExecutor.shutdownNow(); 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.base.BaseActivity;
import com.fongmi.android.tv.ui.dialog.SyncDialog; import com.fongmi.android.tv.ui.dialog.SyncDialog;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.airbnb.lottie.LottieAnimationView;
import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;
@@ -57,8 +58,27 @@ public class HistoryActivity extends BaseActivity implements HistoryAdapter.OnCl
} }
private void getHistory() { private void getHistory() {
mAdapter.addAll(History.get()); mAdapter.addAll(History.getAll()); // 显示所有视频源的观看记录
mBinding.delete.setVisibility(mAdapter.getItemCount() > 0 ? View.VISIBLE : View.GONE); 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) { private void onSync(View view) {
@@ -67,7 +87,10 @@ public class HistoryActivity extends BaseActivity implements HistoryAdapter.OnCl
private void onDelete(View view) { private void onDelete(View view) {
if (mAdapter.isDelete()) { 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) { } else if (mAdapter.getItemCount() > 0) {
mAdapter.setDelete(true); mAdapter.setDelete(true);
} else { } else {
@@ -91,6 +114,7 @@ public class HistoryActivity extends BaseActivity implements HistoryAdapter.OnCl
if (mAdapter.getItemCount() > 0) return; if (mAdapter.getItemCount() > 0) return;
mBinding.delete.setVisibility(View.GONE); mBinding.delete.setVisibility(View.GONE);
mAdapter.setDelete(false); mAdapter.setDelete(false);
updateEmptyState();
} }
@Override @Override
@@ -16,6 +16,7 @@ import androidx.viewbinding.ViewBinding;
import com.fongmi.android.tv.App; import com.fongmi.android.tv.App;
import com.fongmi.android.tv.R; import com.fongmi.android.tv.R;
import com.fongmi.android.tv.Setting;
import com.fongmi.android.tv.Updater; import com.fongmi.android.tv.Updater;
import com.fongmi.android.tv.api.config.LiveConfig; import com.fongmi.android.tv.api.config.LiveConfig;
import com.fongmi.android.tv.api.config.VodConfig; 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.base.BaseActivity;
import com.fongmi.android.tv.ui.custom.FragmentStateManager; import com.fongmi.android.tv.ui.custom.FragmentStateManager;
import com.fongmi.android.tv.ui.fragment.SettingFragment; 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.ui.fragment.VodFragment;
import com.fongmi.android.tv.utils.FileChooser; import com.fongmi.android.tv.utils.FileChooser;
import com.fongmi.android.tv.utils.Notify; import com.fongmi.android.tv.utils.Notify;
@@ -63,6 +63,18 @@ public class HomeActivity extends BaseActivity implements NavigationBarView.OnIt
@Override @Override
protected void initView(Bundle savedInstanceState) { 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; orientation = getResources().getConfiguration().orientation;
Updater.create().release().start(this); Updater.create().release().start(this);
initFragment(savedInstanceState); initFragment(savedInstanceState);
@@ -95,7 +107,6 @@ public class HomeActivity extends BaseActivity implements NavigationBarView.OnIt
public Fragment getItem(int position) { public Fragment getItem(int position) {
if (position == 0) return VodFragment.newInstance(); if (position == 0) return VodFragment.newInstance();
if (position == 1) return SettingFragment.newInstance(); if (position == 1) return SettingFragment.newInstance();
if (position == 2) return SettingPlayerFragment.newInstance();
return null; return null;
} }
}; };
@@ -143,7 +154,7 @@ public class HomeActivity extends BaseActivity implements NavigationBarView.OnIt
private void setNavigation() { private void setNavigation() {
mBinding.navigation.getMenu().findItem(R.id.vod).setVisible(true); mBinding.navigation.getMenu().findItem(R.id.vod).setVisible(true);
mBinding.navigation.getMenu().findItem(R.id.setting).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() { private boolean openLive() {
@@ -179,7 +190,13 @@ public class HomeActivity extends BaseActivity implements NavigationBarView.OnIt
if (mBinding.navigation.getSelectedItemId() == item.getItemId()) return false; if (mBinding.navigation.getSelectedItemId() == item.getItemId()) return false;
if (item.getItemId() == R.id.setting) return mManager.change(1); 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.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; return false;
} }
@@ -204,8 +221,6 @@ public class HomeActivity extends BaseActivity implements NavigationBarView.OnIt
protected void onBackPress() { protected void onBackPress() {
if (!mBinding.navigation.getMenu().findItem(R.id.vod).isVisible()) { if (!mBinding.navigation.getMenu().findItem(R.id.vod).isVisible()) {
setNavigation(); setNavigation();
} else if (mManager.isVisible(2)) {
change(1);
} else if (mManager.isVisible(1)) { } else if (mManager.isVisible(1)) {
mBinding.navigation.setSelectedItemId(R.id.vod); mBinding.navigation.setSelectedItemId(R.id.vod);
} else if (mManager.canBack(0)) { } 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.ui.dialog.SyncDialog;
import com.fongmi.android.tv.utils.Notify; import com.fongmi.android.tv.utils.Notify;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.airbnb.lottie.LottieAnimationView;
import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;
@@ -63,6 +64,25 @@ public class KeepActivity extends BaseActivity implements KeepAdapter.OnClickLis
private void getKeep() { private void getKeep() {
mAdapter.addAll(Keep.getVod()); mAdapter.addAll(Keep.getVod());
mBinding.delete.setVisibility(mAdapter.getItemCount() > 0 ? View.VISIBLE : View.GONE); 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) { private void onSync(View view) {
@@ -71,7 +91,10 @@ public class KeepActivity extends BaseActivity implements KeepAdapter.OnClickLis
private void onDelete(View view) { private void onDelete(View view) {
if (mAdapter.isDelete()) { 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) { } else if (mAdapter.getItemCount() > 0) {
mAdapter.setDelete(true); mAdapter.setDelete(true);
} else { } else {
@@ -114,6 +137,7 @@ public class KeepActivity extends BaseActivity implements KeepAdapter.OnClickLis
if (mAdapter.getItemCount() > 0) return; if (mAdapter.getItemCount() > 0) return;
mBinding.delete.setVisibility(View.GONE); mBinding.delete.setVisibility(View.GONE);
mAdapter.setDelete(false); mAdapter.setDelete(false);
updateEmptyState();
} }
@Override @Override
@@ -1,8 +1,10 @@
package com.fongmi.android.tv.ui.activity; package com.fongmi.android.tv.ui.activity;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
@@ -98,6 +100,8 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
private String tag; private String tag;
private int count; private int count;
private PiP mPiP; private PiP mPiP;
private BroadcastReceiver mScreenReceiver;
private boolean mPausedByScreen = false;
public static void start(Context context) { 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)); 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; mR2 = this::setTraffic;
mR3 = this::hideInfo; mR3 = this::hideInfo;
mPiP = new PiP(); mPiP = new PiP();
initScreenReceiver();
Server.get().start(); Server.get().start();
setRecyclerView(); setRecyclerView();
setVideoView(); setVideoView();
@@ -155,6 +160,33 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
checkLive(); 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 @Override
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
protected void initEvent() { protected void initEvent() {
@@ -1048,6 +1080,8 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
hideInfo(); hideInfo();
hideUI(); hideUI();
} else { } else {
// 退出画中画模式时重置屏幕暂停标志
mPausedByScreen = false;
hideInfo(); hideInfo();
if (isStop()) finish(); if (isStop()) finish();
} }
@@ -1075,6 +1109,13 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
@Override @Override
protected void onResume() { protected void onResume() {
super.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(); if (isRedirect()) onPlay();
setRedirect(false); setRedirect(false);
} }
@@ -1082,6 +1123,14 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
@Override @Override
protected void onPause() { protected void onPause() {
super.onPause(); super.onPause();
// 注销屏幕开关监听
try {
if (mScreenReceiver != null) {
unregisterReceiver(mScreenReceiver);
}
} catch (Exception e) {
// Ignore
}
if (isRedirect()) onPaused(); 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.content.Intent;
import android.os.Bundle;
import android.provider.Settings; import android.provider.Settings;
import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewbinding.ViewBinding; import androidx.viewbinding.ViewBinding;
import com.fongmi.android.tv.R; import com.fongmi.android.tv.R;
import com.fongmi.android.tv.Setting; 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.BufferCallback;
import com.fongmi.android.tv.impl.SpeedCallback; import com.fongmi.android.tv.impl.SpeedCallback;
import com.fongmi.android.tv.impl.UaCallback; 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.BufferDialog;
import com.fongmi.android.tv.ui.dialog.SpeedDialog; import com.fongmi.android.tv.ui.dialog.SpeedDialog;
import com.fongmi.android.tv.ui.dialog.UaDialog; import com.fongmi.android.tv.ui.dialog.UaDialog;
import com.fongmi.android.tv.utils.ResUtil; import com.fongmi.android.tv.utils.ResUtil;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.text.DecimalFormat; 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 DecimalFormat format;
private String[] background; private String[] background;
private String[] caption; private String[] caption;
private String[] render; private String[] render;
private String[] scale; private String[] scale;
public static SettingPlayerFragment newInstance() { public static void start(Activity activity) {
return new SettingPlayerFragment(); activity.startActivity(new Intent(activity, SettingPlayerActivity.class));
} }
private String getSwitch(boolean value) { private String getSwitch(boolean value) {
@@ -43,14 +40,13 @@ public class SettingPlayerFragment extends BaseFragment implements UaCallback, B
} }
@Override @Override
protected ViewBinding getBinding(@NonNull LayoutInflater inflater, @Nullable ViewGroup container) { protected ViewBinding getBinding() {
return mBinding = FragmentSettingPlayerBinding.inflate(inflater, container, false); return mBinding = ActivitySettingPlayerBinding.inflate(getLayoutInflater());
} }
@Override @Override
protected void initView() { protected void initView(Bundle savedInstanceState) {
format = new DecimalFormat("0.#"); format = new DecimalFormat("0.#");
mBinding.back.setOnClickListener(v -> requireActivity().onBackPressed());
mBinding.uaText.setText(Setting.getUa()); mBinding.uaText.setText(Setting.getUa());
mBinding.tunnelSwitch.setChecked(Setting.isTunnel()); mBinding.tunnelSwitch.setChecked(Setting.isTunnel());
mBinding.audioDecodeSwitch.setChecked(Setting.isAudioPrefer()); 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.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.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()]); 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 @Override
protected void initEvent() { protected void initEvent() {
mBinding.back.setOnClickListener(v -> finish());
mBinding.ua.setOnClickListener(this::onUa); mBinding.ua.setOnClickListener(this::onUa);
mBinding.aac.setOnClickListener(this::setAAC);
mBinding.scale.setOnClickListener(this::onScale); mBinding.scale.setOnClickListener(this::onScale);
mBinding.speed.setOnClickListener(this::onSpeed); mBinding.speed.setOnClickListener(this::onSpeed);
mBinding.buffer.setOnClickListener(this::onBuffer); mBinding.buffer.setOnClickListener(this::onBuffer);
mBinding.render.setOnClickListener(this::setRender); mBinding.render.setOnClickListener(this::setRender);
mBinding.tunnel.setOnClickListener(this::setTunnel);
mBinding.caption.setOnClickListener(this::setCaption); mBinding.caption.setOnClickListener(this::setCaption);
mBinding.caption.setOnLongClickListener(this::onCaption); mBinding.caption.setOnLongClickListener(this::onCaption);
mBinding.background.setOnClickListener(this::onBackground); 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) { private void onUa(View view) {
@@ -116,11 +93,11 @@ public class SettingPlayerFragment extends BaseFragment implements UaCallback, B
private void setAAC(View view) { private void setAAC(View view) {
boolean isChecked = !Setting.isPreferAAC(); boolean isChecked = !Setting.isPreferAAC();
Setting.putPreferAAC(isChecked); Setting.putPreferAAC(isChecked);
mBinding.aacSwitch.setChecked(isChecked); // 不需要再次调用 setChecked因为点击已经触发了状态变化
} }
private void onScale(View view) { 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]); mBinding.scaleText.setText(scale[which]);
Setting.putScale(which); Setting.putScale(which);
dialog.dismiss(); dialog.dismiss();
@@ -157,7 +134,7 @@ public class SettingPlayerFragment extends BaseFragment implements UaCallback, B
private void setTunnel(View view) { private void setTunnel(View view) {
boolean isChecked = !Setting.isTunnel(); boolean isChecked = !Setting.isTunnel();
Setting.putTunnel(isChecked); Setting.putTunnel(isChecked);
mBinding.tunnelSwitch.setChecked(isChecked); // 不需要再次调用 setChecked因为点击已经触发了状态变化
if (isChecked && Setting.getRender() == 1) setRender(view); if (isChecked && Setting.getRender() == 1) setRender(view);
} }
@@ -172,7 +149,7 @@ public class SettingPlayerFragment extends BaseFragment implements UaCallback, B
} }
private void onBackground(View view) { 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]); mBinding.backgroundText.setText(background[which]);
Setting.putBackground(which); Setting.putBackground(which);
dialog.dismiss(); dialog.dismiss();
@@ -182,17 +159,12 @@ public class SettingPlayerFragment extends BaseFragment implements UaCallback, B
private void setAudioDecode(View view) { private void setAudioDecode(View view) {
boolean isChecked = !Setting.isAudioPrefer(); boolean isChecked = !Setting.isAudioPrefer();
Setting.putAudioPrefer(isChecked); Setting.putAudioPrefer(isChecked);
mBinding.audioDecodeSwitch.setChecked(isChecked); // 不需要再次调用 setChecked因为点击已经触发了状态变化
} }
private void setDanmakuLoad(View view) { private void setDanmakuLoad(View view) {
boolean isChecked = !Setting.isDanmakuLoad(); boolean isChecked = !Setting.isDanmakuLoad();
Setting.putDanmakuLoad(isChecked); Setting.putDanmakuLoad(isChecked);
mBinding.danmakuLoadSwitch.setChecked(isChecked); // 不需要再次调用 setChecked因为点击已经触发了状态变化
}
@Override
public void onHiddenChanged(boolean hidden) {
if (!hidden) initView();
} }
} }
@@ -4,24 +4,35 @@ import android.Manifest;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.app.Dialog; import android.app.Dialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.BatteryManager;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings; import android.provider.Settings;
import android.text.Html; import android.text.Html;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.format.DateFormat;
import android.text.style.ClickableSpan; import android.text.style.ClickableSpan;
import android.view.KeyEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowManager; import android.view.WindowManager;
import android.media.AudioManager;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import java.util.concurrent.TimeUnit;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -63,6 +74,7 @@ import com.fongmi.android.tv.event.RefreshEvent;
import com.fongmi.android.tv.model.SiteViewModel; import com.fongmi.android.tv.model.SiteViewModel;
import com.fongmi.android.tv.player.Players; import com.fongmi.android.tv.player.Players;
import com.fongmi.android.tv.player.exo.ExoUtil; 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.service.PlaybackService;
import com.fongmi.android.tv.ui.adapter.EpisodeAdapter; import com.fongmi.android.tv.ui.adapter.EpisodeAdapter;
import com.fongmi.android.tv.ui.adapter.FlagAdapter; import com.fongmi.android.tv.ui.adapter.FlagAdapter;
@@ -98,6 +110,7 @@ import com.github.catvod.utils.Trans;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.permissionx.guolindev.PermissionX; import com.permissionx.guolindev.PermissionX;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;
@@ -148,6 +161,14 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
private Clock mClock; private Clock mClock;
private String tag; private String tag;
private PiP mPiP; 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;
private AudioManager mAudioManager;
public static void push(FragmentActivity activity, String text) { public static void push(FragmentActivity activity, String text) {
if (FileChooser.isValid(activity, Uri.parse(text))) file(activity, FileChooser.getPathFromUri(activity, Uri.parse(text))); if (FileChooser.isValid(activity, Uri.parse(text))) file(activity, FileChooser.getPathFromUri(activity, Uri.parse(text)));
@@ -290,6 +311,7 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
mDialogs = new ArrayList<>(); mDialogs = new ArrayList<>();
mBroken = new ArrayList<>(); mBroken = new ArrayList<>();
mClock = Clock.create(); mClock = Clock.create();
mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
mR1 = this::hideControl; mR1 = this::hideControl;
mR2 = this::setTraffic; mR2 = this::setTraffic;
mR3 = this::setOrient; mR3 = this::setOrient;
@@ -302,6 +324,131 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
showProgress(); showProgress();
showDanmaku(); showDanmaku();
checkId(); 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 @Override
@@ -441,8 +588,11 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
mBinding.swipeLayout.setRefreshing(false); mBinding.swipeLayout.setRefreshing(false);
if (result.getList().isEmpty()) setEmpty(result.hasMsg()); if (result.getList().isEmpty()) setEmpty(result.hasMsg());
else setDetail(result.getList().get(0)); else setDetail(result.getList().get(0));
// 只在有错误或重要消息时显示提示
if (result.hasMsg() && result.getList().isEmpty()) {
Notify.show(result.getMsg()); Notify.show(result.getMsg());
} }
}
private void setEmpty(boolean finish) { private void setEmpty(boolean finish) {
if (isFromCollect() || finish) { if (isFromCollect() || finish) {
@@ -960,6 +1110,7 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
mBinding.control.bottom.setVisibility(isLock() ? View.GONE : View.VISIBLE); mBinding.control.bottom.setVisibility(isLock() ? View.GONE : View.VISIBLE);
mBinding.control.top.setVisibility(isLock() ? View.GONE : View.VISIBLE); mBinding.control.top.setVisibility(isLock() ? View.GONE : View.VISIBLE);
mBinding.control.getRoot().setVisibility(View.VISIBLE); mBinding.control.getRoot().setVisibility(View.VISIBLE);
updateTimeBattery();
setR1Callback(); setR1Callback();
checkPlayImg(); checkPlayImg();
} }
@@ -1389,6 +1540,55 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
this.rotate = rotate; this.rotate = rotate;
if (fullscreen && rotate) noPadding(mBinding.control.getRoot()); if (fullscreen && rotate) noPadding(mBinding.control.getRoot());
if (fullscreen && !rotate) setPadding(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() { public boolean isStop() {
@@ -1490,10 +1690,40 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
@Override @Override
public void onSeekEnd(long time) { public void onSeekEnd(long time) {
handleLandscapeSeek(time);
}
// 添加新的方法处理横屏模式下的特殊逻辑
private void handleLandscapeSeek(long time) {
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
// 横屏模式下的特殊处理
mBinding.widget.seek.setVisibility(View.GONE); mBinding.widget.seek.setVisibility(View.GONE);
mPlayers.pause();
mPlayers.seek(time); mPlayers.seek(time);
showProgress(); showProgress();
App.post(() -> {
long actualPosition = mPlayers.getPosition();
if (Math.abs(actualPosition - time) > 500) {
mPlayers.seek(time);
}
onPlay(); 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 @Override
@@ -1544,6 +1774,8 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
hideDanmaku(); hideDanmaku();
hideSheet(); hideSheet();
} else { } else {
// 退出画中画模式时重置屏幕暂停标志
mPausedByScreen = false;
showDanmaku(); showDanmaku();
if (isStop()) finish(); if (isStop()) finish();
} }
@@ -1555,6 +1787,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_PORTRAIT && !isRotate()) exitFullscreen();
if (isAutoRotate() && isPort() && newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) enterFullscreen(); if (isAutoRotate() && isPort() && newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) enterFullscreen();
if (isFullscreen()) Util.hideSystemUI(this); if (isFullscreen()) Util.hideSystemUI(this);
updateTimeBattery();
} }
@Override @Override
@@ -1574,6 +1807,7 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
startTimeBatteryUpdates();
if (isRedirect()) onPlay(); if (isRedirect()) onPlay();
setRedirect(false); setRedirect(false);
} }
@@ -1581,6 +1815,7 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
@Override @Override
protected void onPause() { protected void onPause() {
super.onPause(); super.onPause();
stopTimeBatteryUpdates();
if (isRedirect()) onPaused(); if (isRedirect()) onPaused();
} }
@@ -1592,6 +1827,71 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
setStop(true); setStop(true);
} }
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// 只在视频播放时处理键盘事件
if (mPlayers != null && !mPlayers.isEmpty()) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_LEFT:
// 左方向键快退10秒
if (mPlayers.isPlaying() || mPlayers.getPosition() > 0) {
long currentPosition = mPlayers.getPosition();
long seekTime = -10000; // 快退10秒
long newPosition = Math.max(0, currentPosition + seekTime);
mPlayers.seekTo(newPosition);
// 显示快退提示
onSeek(seekTime);
App.post(() -> {
mBinding.widget.seek.setVisibility(View.GONE);
}, 1000);
return true;
}
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
// 右方向键快进10秒
if (mPlayers.isPlaying() || mPlayers.getPosition() > 0) {
long currentPosition = mPlayers.getPosition();
long duration = mPlayers.getDuration();
long seekTime = 10000; // 快进10秒
long newPosition = Math.min(duration > 0 ? duration : Long.MAX_VALUE, currentPosition + seekTime);
mPlayers.seekTo(newPosition);
// 显示快进提示
onSeek(seekTime);
App.post(() -> {
mBinding.widget.seek.setVisibility(View.GONE);
}, 1000);
return true;
}
break;
case KeyEvent.KEYCODE_DPAD_UP:
// 上方向键增加音量
if (mAudioManager != null) {
int currentVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
int newVolume = Math.min(maxVolume, currentVolume + 1);
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, newVolume, 0);
onVolume((int) (newVolume * 100.0f / maxVolume));
App.post(() -> onVolumeEnd(), 1000);
return true;
}
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
// 下方向键减少音量
if (mAudioManager != null) {
int currentVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
int newVolume = Math.max(0, currentVolume - 1);
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, newVolume, 0);
onVolume((int) (newVolume * 100.0f / maxVolume));
App.post(() -> onVolumeEnd(), 1000);
return true;
}
break;
}
}
return super.onKeyDown(keyCode, event);
}
@Override @Override
public void onBackPressed() { public void onBackPressed() {
if (isVisible(mBinding.control.getRoot())) { if (isVisible(mBinding.control.getRoot())) {
@@ -1608,14 +1908,17 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
stopSearch(); stopSearch();
mClock.release();
mPlayers.release(); mPlayers.release();
mClock.release();
Timer.get().reset(); Timer.get().reset();
RefreshEvent.history(); RefreshEvent.history();
PlaybackService.stop(); PlaybackService.stop();
mHandler.removeCallbacksAndMessages(null);
App.removeCallbacks(mR1, mR2, mR3, mR4); App.removeCallbacks(mR1, mR2, mR3, mR4);
EventBus.getDefault().unregister(this);
mViewModel.result.removeObserver(mObserveDetail); mViewModel.result.removeObserver(mObserveDetail);
mViewModel.player.removeObserver(mObservePlayer); mViewModel.player.removeObserver(mObservePlayer);
mViewModel.search.removeObserver(mObserveSearch); 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.bean.Config;
import com.fongmi.android.tv.databinding.AdapterConfigBinding; import com.fongmi.android.tv.databinding.AdapterConfigBinding;
import java.util.ArrayList;
import java.util.List; import java.util.List;
public class ConfigAdapter extends RecyclerView.Adapter<ConfigAdapter.ViewHolder> { 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 onTextClick(Config item);
void onCopyClick(Config item);
void onDeleteClick(Config item); void onDeleteClick(Config item);
} }
public ConfigAdapter addAll(int type) { public ConfigAdapter addAll(int type) {
mItems = Config.getAll(type); mItems = new ArrayList<>();
mItems.remove(type == 0 ? VodConfig.get().getConfig() : LiveConfig.get().getConfig()); 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; return this;
} }
public void addItem(Config item) {
if (item.isEmpty()) return;
mItems.add(0, item);
notifyItemInserted(0);
}
public int remove(Config item) { public int remove(Config item) {
int position = mItems.indexOf(item);
item.delete(); item.delete();
mItems.remove(item); mItems.remove(item);
notifyDataSetChanged(); notifyItemRemoved(position);
return getItemCount(); return getItemCount();
} }
@@ -58,6 +76,7 @@ public class ConfigAdapter extends RecyclerView.Adapter<ConfigAdapter.ViewHolder
Config item = mItems.get(position); Config item = mItems.get(position);
holder.binding.text.setText(item.getDesc()); holder.binding.text.setText(item.getDesc());
holder.binding.text.setOnClickListener(v -> mListener.onTextClick(item)); holder.binding.text.setOnClickListener(v -> mListener.onTextClick(item));
holder.binding.copy.setOnClickListener(v -> mListener.onCopyClick(item));
holder.binding.delete.setOnClickListener(v -> mListener.onDeleteClick(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) { public void addAll(Result result) {
mItems.addAll(result.getTypes()); mItems.addAll(result.getTypes());
if (!result.getList().isEmpty()) mItems.add(0, home());
if (!mItems.isEmpty()) mItems.get(0).setActivated(true); if (!mItems.isEmpty()) mItems.get(0).setActivated(true);
notifyDataSetChanged(); notifyDataSetChanged();
} }
@@ -8,6 +8,7 @@ import android.view.MotionEvent;
import android.view.ScaleGestureDetector; import android.view.ScaleGestureDetector;
import android.view.View; import android.view.View;
import android.view.WindowManager; import android.view.WindowManager;
import android.content.res.Configuration;
import androidx.annotation.NonNull; 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; if (isEdge(e1) || changeScale || lock || e1.getPointerCount() > 1) return true;
float deltaX = e2.getX() - e1.getX(); float deltaX = e2.getX() - e1.getX();
float deltaY = e1.getY() - e2.getY(); 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 (touch) checkFunc(distanceX, distanceY, e2);
if (changeTime) listener.onSeek(time = (long) (deltaX * 50)); if (changeTime) listener.onSeek(time = (long) (deltaX * 50));
if (changeBright) setBright(deltaY); if (changeBright) setBright(deltaY);
@@ -145,9 +157,32 @@ public class CustomKeyDownVod extends GestureDetector.SimpleOnGestureListener im
private void checkFunc(float distanceX, float distanceY, MotionEvent e2) { private void checkFunc(float distanceX, float distanceY, MotionEvent e2) {
int four = ResUtil.getScreenWidth(activity) / 4; 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; 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; package com.fongmi.android.tv.ui.dialog;
import android.app.Activity;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -22,11 +23,20 @@ public class BufferDialog {
return new BufferDialog(fragment); return new BufferDialog(fragment);
} }
public static BufferDialog create(Activity activity) {
return new BufferDialog(activity);
}
public BufferDialog(Fragment fragment) { public BufferDialog(Fragment fragment) {
this.callback = (BufferCallback) fragment; this.callback = (BufferCallback) fragment;
this.binding = DialogBufferBinding.inflate(LayoutInflater.from(fragment.getContext())); 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() { public void show() {
initDialog(); initDialog();
initView(); 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.bean.Config;
import com.fongmi.android.tv.databinding.DialogConfigBinding; import com.fongmi.android.tv.databinding.DialogConfigBinding;
import com.fongmi.android.tv.impl.ConfigCallback; 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.ui.custom.CustomTextListener;
import com.fongmi.android.tv.utils.FileChooser; import com.fongmi.android.tv.utils.FileChooser;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@@ -52,6 +53,10 @@ public class ConfigDialog {
public ConfigDialog(Fragment fragment) { public ConfigDialog(Fragment fragment) {
this.fragment = fragment; this.fragment = fragment;
// 确保fragment实现了ConfigCallback接口
if (!(fragment instanceof ConfigCallback)) {
throw new IllegalArgumentException("Fragment must implement ConfigCallback");
}
this.callback = (ConfigCallback) fragment; this.callback = (ConfigCallback) fragment;
this.binding = DialogConfigBinding.inflate(LayoutInflater.from(fragment.getContext())); this.binding = DialogConfigBinding.inflate(LayoutInflater.from(fragment.getContext()));
this.append = true; this.append = true;
@@ -144,9 +149,89 @@ public class ConfigDialog {
private void onPositive(DialogInterface dialog, int which) { private void onPositive(DialogInterface dialog, int which) {
String url = binding.url.getText().toString().trim(); String url = binding.url.getText().toString().trim();
String name = binding.name.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 (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(); 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.ui.custom.SpaceItemDecoration;
import com.fongmi.android.tv.utils.ResUtil; import com.fongmi.android.tv.utils.ResUtil;
import com.fongmi.android.tv.utils.Timer; 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.bottomsheet.BottomSheetDialogFragment;
import com.google.android.material.slider.Slider; import com.google.android.material.slider.Slider;
import java.util.Arrays; import java.util.Arrays;
import java.util.Formatter;
import java.util.List; 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 DialogControlBinding binding;
private ActivityVideoBinding parent; private ActivityVideoBinding parent;
@@ -40,6 +43,8 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
private History history; private History history;
private Players player; private Players player;
private boolean parse; private boolean parse;
private StringBuilder builder;
private Formatter formatter;
public static ControlDialog create() { public static ControlDialog create() {
return new ControlDialog(); return new ControlDialog();
@@ -47,6 +52,8 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
public ControlDialog() { public ControlDialog() {
this.scale = ResUtil.getStringArray(R.array.select_scale); this.scale = ResUtil.getStringArray(R.array.select_scale);
this.builder = new StringBuilder();
this.formatter = new Formatter(builder, Locale.getDefault());
} }
public ControlDialog parent(ActivityVideoBinding parent) { 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.opening.setText(parent.control.action.opening.getText());
binding.loop.setActivated(parent.control.action.loop.isActivated()); binding.loop.setActivated(parent.control.action.loop.isActivated());
binding.timer.setActivated(Timer.get().isRunning()); 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(); setTrackVisible();
setScaleText(); setScaleText();
setPlayer(); setPlayer();
@@ -126,6 +142,8 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
private void setSpeed(@NonNull Slider slider, float value, boolean fromUser) { private void setSpeed(@NonNull Slider slider, float value, boolean fromUser) {
parent.control.action.speed.setText(player.setSpeed(value)); parent.control.action.speed.setText(player.setSpeed(value));
if (history != null) history.setSpeed(player.getSpeed()); if (history != null) history.setSpeed(player.getSpeed());
// 实时更新倍速数值显示
binding.speedValue.setText(String.format("%.1fx", value));
} }
private void setScaleText() { private void setScaleText() {
@@ -179,6 +197,8 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
binding.player.setText(parent.control.action.player.getText()); binding.player.setText(parent.control.action.player.getText());
binding.decode.setVisibility(parent.control.action.decode.getVisibility()); binding.decode.setVisibility(parent.control.action.decode.getVisibility());
binding.danmaku.setVisibility(parent.control.action.danmaku.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) { 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()); 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 { public interface Listener {
void onScale(int tag); void onScale(int tag);
@@ -60,8 +60,35 @@ public class HistoryDialog implements ConfigAdapter.OnClickListener {
@Override @Override
public void onTextClick(Config item) { public void onTextClick(Config item) {
callback.setConfig(item); // 防止重复点击和空值
if (!dialog.isShowing() || item == null) return;
// 检查callback是否有效
if (callback == null) {
dialog.dismiss(); 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 @Override
@@ -73,6 +100,13 @@ public class HistoryDialog implements ConfigAdapter.OnClickListener {
@Override @Override
public void onDeleteClick(Config item) { 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; package com.fongmi.android.tv.ui.dialog;
import android.app.Activity;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -22,11 +23,20 @@ public class SpeedDialog {
return new SpeedDialog(fragment); return new SpeedDialog(fragment);
} }
public static SpeedDialog create(Activity activity) {
return new SpeedDialog(activity);
}
public SpeedDialog(Fragment fragment) { public SpeedDialog(Fragment fragment) {
this.callback = (SpeedCallback) fragment; this.callback = (SpeedCallback) fragment;
this.binding = DialogSpeedBinding.inflate(LayoutInflater.from(fragment.getContext())); 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() { public void show() {
initDialog(); initDialog();
initView(); initView();
@@ -60,6 +60,8 @@ public class TimerDialog extends BaseDialog implements Timer.Callback {
binding.time2.setOnClickListener(this::setTimer); binding.time2.setOnClickListener(this::setTimer);
binding.time3.setOnClickListener(this::setTimer); binding.time3.setOnClickListener(this::setTimer);
binding.time4.setOnClickListener(this::setTimer); binding.time4.setOnClickListener(this::setTimer);
binding.time5.setOnClickListener(this::setTimer);
binding.time6.setOnClickListener(this::setTimer);
} }
private void setTimer(View view) { private void setTimer(View view) {
@@ -1,5 +1,6 @@
package com.fongmi.android.tv.ui.dialog; package com.fongmi.android.tv.ui.dialog;
import android.app.Activity;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -27,12 +28,22 @@ public class UaDialog {
return new UaDialog(fragment); return new UaDialog(fragment);
} }
public static UaDialog create(Activity activity) {
return new UaDialog(activity);
}
public UaDialog(Fragment fragment) { public UaDialog(Fragment fragment) {
this.callback = (UaCallback) fragment; this.callback = (UaCallback) fragment;
this.binding = DialogUaBinding.inflate(LayoutInflater.from(fragment.getContext())); this.binding = DialogUaBinding.inflate(LayoutInflater.from(fragment.getContext()));
this.append = true; this.append = true;
} }
public UaDialog(Activity activity) {
this.callback = (UaCallback) activity;
this.binding = DialogUaBinding.inflate(LayoutInflater.from(activity));
this.append = true;
}
public void show() { public void show() {
initDialog(); initDialog();
initView(); initView();
@@ -0,0 +1,474 @@
package com.fongmi.android.tv.ui.dialog;
import android.content.DialogInterface;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import com.fongmi.android.tv.App;
import com.fongmi.android.tv.R;
import com.fongmi.android.tv.Setting;
import com.fongmi.android.tv.databinding.DialogWebdavBinding;
import com.fongmi.android.tv.event.RefreshEvent;
import com.fongmi.android.tv.utils.Notify;
import com.fongmi.android.tv.utils.WebDAVSyncManager;
import com.github.catvod.utils.Logger;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
public class WebDAVDialog {
// 预设的WebDAV服务提供商
private static final String[] PROVIDERS = {
"坚果云",
"Nextcloud",
"ownCloud",
"自定义"
};
private static final String[] PROVIDER_URLS = {
"https://dav.jianguoyun.com/dav/XMBOX/", // 坚果云添加XMBOX子目录方便在网页版查看
"", // Nextcloud需要用户输入
"", // ownCloud需要用户输入
"" // 自定义需要用户输入
};
private final DialogWebdavBinding binding;
private final Fragment fragment;
private AlertDialog dialog;
private WebDAVSyncManager syncManager;
private int selectedProvider = 0; // 默认选择坚果云
private boolean isInitializing = false; // 标记是否正在初始化防止初始化时触发监听器
public static WebDAVDialog create(Fragment fragment) {
return new WebDAVDialog(fragment);
}
public WebDAVDialog(Fragment fragment) {
this.fragment = fragment;
this.binding = DialogWebdavBinding.inflate(LayoutInflater.from(fragment.getContext()));
this.syncManager = WebDAVSyncManager.get();
}
public void show() {
initDialog();
initView();
initEvent();
}
private void initDialog() {
dialog = new MaterialAlertDialogBuilder(binding.getRoot().getContext())
.setTitle("WebDAV 配置")
.setView(binding.getRoot())
.setPositiveButton("保存", this::onPositive)
.setNegativeButton("取消", this::onNegative)
.create();
dialog.getWindow().setDimAmount(0);
dialog.show();
}
private void initView() {
isInitializing = true; // 标记开始初始化
// 加载已保存的配置
String url = Setting.getWebDAVUrl();
String username = Setting.getWebDAVUsername();
String password = Setting.getWebDAVPassword();
boolean autoSync = Setting.isWebDAVAutoSync();
int interval = Setting.getWebDAVSyncInterval();
// 根据保存的URL判断是哪个服务提供商
selectedProvider = getProviderIndexByUrl(url);
binding.providerText.setText(PROVIDERS[selectedProvider]);
// 根据选择的服务提供商决定是否显示URL输入框
if (selectedProvider == PROVIDERS.length - 1) {
// 自定义显示URL输入框
binding.urlInput.setVisibility(View.VISIBLE);
binding.urlText.setText(url);
if (!TextUtils.isEmpty(url)) {
binding.urlText.setSelection(url.length());
}
} else if (selectedProvider == 0) {
// 坚果云永远隐藏输入框有预设URL
binding.urlInput.setVisibility(View.GONE);
} else {
// Nextcloud或ownCloud需要用户输入URL
binding.urlInput.setVisibility(View.VISIBLE);
binding.urlText.setText(url);
if (!TextUtils.isEmpty(url)) {
binding.urlText.setSelection(url.length());
}
}
binding.usernameText.setText(username);
binding.passwordText.setText(password);
binding.autoSyncSwitch.setChecked(autoSync);
binding.syncIntervalText.setText(String.valueOf(interval));
// 根据自动同步开关显示/隐藏同步间隔
updateSyncIntervalVisibility(autoSync);
isInitializing = false; // 初始化完成
}
/**
* 根据URL判断是哪个服务提供商
*/
private int getProviderIndexByUrl(String url) {
if (TextUtils.isEmpty(url)) {
return 0; // 默认坚果云
}
if (url.contains("jianguoyun.com")) {
return 0; // 坚果云
}
if (url.contains("nextcloud")) {
return 1; // Nextcloud
}
if (url.contains("owncloud")) {
return 2; // ownCloud
}
return PROVIDERS.length - 1; // 自定义
}
/**
* 获取当前选择的服务提供商的URL
*/
private String getProviderUrl() {
if (selectedProvider < PROVIDER_URLS.length && !TextUtils.isEmpty(PROVIDER_URLS[selectedProvider])) {
return PROVIDER_URLS[selectedProvider];
}
return "";
}
private void initEvent() {
// 服务提供商选择
binding.providerText.setOnClickListener(v -> onSelectProvider());
// 自动同步开关监听立即保存状态
// 使用setOnClickListener而不是setOnCheckedChangeListener避免覆盖CustomSwitch内部的动画监听器
// AppCompatCheckBox会自动处理状态切换我们只需要在状态切换后获取新状态
binding.autoSyncSwitch.setOnClickListener(v -> {
// 防止初始化时触发监听器
if (isInitializing) {
return;
}
// 使用post()确保在状态切换后获取新状态
binding.autoSyncSwitch.post(() -> {
boolean newState = binding.autoSyncSwitch.isChecked();
// 立即保存自动同步状态
Setting.putWebDAVAutoSync(newState);
// 更新同步间隔的可见性
updateSyncIntervalVisibility(newState);
});
});
// 测试连接按钮
binding.testButton.setOnClickListener(v -> onTestConnection());
// 立即同步按钮
binding.syncButton.setOnClickListener(v -> onSyncNow());
// 同步间隔点击弹出选择对话框
binding.syncIntervalContainer.setOnClickListener(v -> onSelectInterval());
// 密码输入框回车键
binding.passwordText.setOnEditorActionListener((textView, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE) {
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();
return true;
}
return false;
});
}
private void onSelectProvider() {
new MaterialAlertDialogBuilder(binding.getRoot().getContext())
.setTitle("选择服务提供商")
.setSingleChoiceItems(PROVIDERS, selectedProvider, (dialog, which) -> {
selectedProvider = which;
binding.providerText.setText(PROVIDERS[which]);
// 如果是自定义显示URL输入框
if (which == PROVIDERS.length - 1) {
binding.urlInput.setVisibility(View.VISIBLE);
String currentUrl = binding.urlText.getText().toString().trim();
if (TextUtils.isEmpty(currentUrl)) {
binding.urlText.setText("");
}
} else {
// 使用预设的URL
binding.urlInput.setVisibility(View.GONE);
String providerUrl = getProviderUrl();
if (!TextUtils.isEmpty(providerUrl)) {
// URL会在保存时自动填充
} else {
// Nextcloud或ownCloud需要用户输入URL
binding.urlInput.setVisibility(View.VISIBLE);
binding.urlText.setHint("请输入" + PROVIDERS[which] + "服务器地址");
}
}
dialog.dismiss();
})
.setNegativeButton("取消", null)
.show();
}
private void updateSyncIntervalVisibility(boolean visible) {
binding.syncIntervalContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
}
private void onTestConnection() {
String url = getServerUrl();
String username = binding.usernameText.getText().toString().trim();
String password = binding.passwordText.getText().toString().trim();
if (TextUtils.isEmpty(url)) {
showStatus("请选择服务提供商或输入服务器地址", false);
return;
}
if (TextUtils.isEmpty(username)) {
showStatus("请输入用户名", false);
return;
}
if (TextUtils.isEmpty(password)) {
showStatus("请输入密码", false);
return;
}
// 临时保存配置用于测试
Setting.putWebDAVUrl(url);
Setting.putWebDAVUsername(username);
Setting.putWebDAVPassword(password);
// 重新加载配置
syncManager.reloadConfig();
showStatus("正在测试连接...", true);
binding.testButton.setEnabled(false);
// 在后台线程测试连接
App.execute(() -> {
boolean success = syncManager.testConnection();
App.post(() -> {
// 检查对话框是否还存在
if (binding == null || dialog == null || !dialog.isShowing()) {
return;
}
binding.testButton.setEnabled(true);
if (success) {
showStatus("连接成功!", true);
} else {
showStatus("连接失败,请检查配置", false);
}
});
});
}
private void onSyncNow() {
// 先临时保存当前配置用于测试同步
String url = getServerUrl();
String username = binding.usernameText.getText().toString().trim();
String password = binding.passwordText.getText().toString().trim();
// 验证输入
if (TextUtils.isEmpty(url)) {
showStatus("请选择服务提供商或输入服务器地址", false);
return;
}
if (TextUtils.isEmpty(username)) {
showStatus("请输入用户名", false);
return;
}
if (TextUtils.isEmpty(password)) {
showStatus("请输入密码", false);
return;
}
// 临时保存配置用于同步
Setting.putWebDAVUrl(url);
Setting.putWebDAVUsername(username);
Setting.putWebDAVPassword(password);
syncManager.reloadConfig();
if (!syncManager.isConfigured()) {
showStatus("配置无效,无法同步", false);
return;
}
showStatus("正在同步...", true);
binding.syncButton.setEnabled(false);
// 在后台线程执行同步
App.execute(() -> {
try {
// 先上传本地记录
syncManager.uploadHistory();
// 再下载远程记录并合并
boolean downloadSuccess = syncManager.downloadHistory();
App.post(() -> {
// 检查对话框是否还存在
if (binding == null || dialog == null || !dialog.isShowing()) {
return;
}
binding.syncButton.setEnabled(true);
if (downloadSuccess) {
showStatus("同步完成", true);
Notify.show("同步完成");
} else {
showStatus("同步完成(本地数据已上传)", true);
Notify.show("同步完成");
}
});
} catch (Exception e) {
App.post(() -> {
// 检查对话框是否还存在
if (binding == null || dialog == null || !dialog.isShowing()) {
return;
}
binding.syncButton.setEnabled(true);
showStatus("同步失败:" + e.getMessage(), false);
Notify.show("同步失败");
Logger.e("WebDAV: 同步失败: " + e.getMessage());
});
}
});
}
private void onSelectInterval() {
String[] intervals = {"15", "30", "60", "120", "240"};
int currentInterval = Setting.getWebDAVSyncInterval();
int selectedIndex = 0;
for (int i = 0; i < intervals.length; i++) {
if (Integer.parseInt(intervals[i]) == currentInterval) {
selectedIndex = i;
break;
}
}
new MaterialAlertDialogBuilder(binding.getRoot().getContext())
.setTitle("选择同步间隔")
.setSingleChoiceItems(intervals, selectedIndex, (dialog, which) -> {
int interval = Integer.parseInt(intervals[which]);
binding.syncIntervalText.setText(String.valueOf(interval));
// 立即保存同步间隔
Setting.putWebDAVSyncInterval(interval);
dialog.dismiss();
})
.setNegativeButton("取消", null)
.show();
}
private void showStatus(String message, boolean isSuccess) {
// 检查对话框是否还存在
if (binding == null || dialog == null || !dialog.isShowing()) {
return;
}
binding.statusText.setText(message);
binding.statusText.setVisibility(TextUtils.isEmpty(message) ? View.GONE : View.VISIBLE);
// 可以根据isSuccess设置不同的颜色
binding.statusText.setTextColor(isSuccess ?
fragment.getResources().getColor(R.color.white) :
fragment.getResources().getColor(android.R.color.holo_red_dark));
}
/**
* 获取服务器URL根据选择的服务提供商
*/
private String getServerUrl() {
if (selectedProvider == PROVIDERS.length - 1) {
// 自定义从输入框获取
return binding.urlText.getText().toString().trim();
} else {
// 使用预设URL或从输入框获取Nextcloud/ownCloud
String providerUrl = getProviderUrl();
if (!TextUtils.isEmpty(providerUrl)) {
return providerUrl;
} else {
// Nextcloud或ownCloud需要用户输入
return binding.urlText.getText().toString().trim();
}
}
}
private void onPositive(DialogInterface dialog, int which) {
String url = getServerUrl();
String username = binding.usernameText.getText().toString().trim();
String password = binding.passwordText.getText().toString().trim();
boolean autoSync = binding.autoSyncSwitch.isChecked();
int interval = Integer.parseInt(binding.syncIntervalText.getText().toString());
// 验证输入
if (TextUtils.isEmpty(url)) {
Notify.show("请选择服务提供商或输入服务器地址");
return;
}
if (TextUtils.isEmpty(username)) {
Notify.show("请输入用户名");
return;
}
if (TextUtils.isEmpty(password)) {
Notify.show("请输入密码");
return;
}
// 保存配置
Setting.putWebDAVUrl(url);
Setting.putWebDAVUsername(username);
Setting.putWebDAVPassword(password);
Setting.putWebDAVAutoSync(autoSync);
Setting.putWebDAVSyncInterval(interval);
// 重新加载配置
syncManager.reloadConfig();
// 配置保存后立即执行一次同步下载远程数据
// 这样新设备配置后就能立即看到其他设备的历史记录
if (syncManager.isConfigured()) {
Notify.show("WebDAV配置已保存,正在同步数据...");
App.execute(() -> {
try {
// 先上传本地记录
syncManager.uploadHistory();
// 再下载远程记录并合并
boolean downloadSuccess = syncManager.downloadHistory();
App.post(() -> {
if (downloadSuccess) {
Notify.show("同步完成,已获取远程观看记录");
} else {
Notify.show("同步完成(本地数据已上传)");
}
});
} catch (Exception e) {
App.post(() -> {
Notify.show("同步失败,请检查网络连接");
});
}
});
} else {
Notify.show("WebDAV配置已保存");
}
dialog.dismiss();
// 通知设置界面更新状态通过RefreshEvent
// 使用App.post确保对话框关闭后再发送事件让状态能及时更新
App.post(() -> RefreshEvent.config());
}
private void onNegative(DialogInterface dialog, int which) {
dialog.dismiss();
}
/**
* 重新加载配置用于外部调用
*/
public void reloadConfig() {
syncManager.reloadConfig();
}
}
@@ -3,9 +3,17 @@ package com.fongmi.android.tv.ui.fragment;
import android.Manifest; import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; 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.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -31,16 +39,20 @@ import com.fongmi.android.tv.impl.ProxyCallback;
import com.fongmi.android.tv.impl.SiteCallback; import com.fongmi.android.tv.impl.SiteCallback;
import com.fongmi.android.tv.player.Source; import com.fongmi.android.tv.player.Source;
import com.fongmi.android.tv.ui.activity.HomeActivity; 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.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.ConfigDialog;
import com.fongmi.android.tv.ui.dialog.HistoryDialog; import com.fongmi.android.tv.ui.dialog.HistoryDialog;
import com.fongmi.android.tv.ui.dialog.LiveDialog; import com.fongmi.android.tv.ui.dialog.LiveDialog;
import com.fongmi.android.tv.ui.dialog.ProxyDialog; import com.fongmi.android.tv.ui.dialog.ProxyDialog;
import com.fongmi.android.tv.ui.dialog.RestoreDialog; import com.fongmi.android.tv.ui.dialog.RestoreDialog;
import com.fongmi.android.tv.ui.dialog.SiteDialog; import com.fongmi.android.tv.ui.dialog.SiteDialog;
import com.fongmi.android.tv.ui.dialog.WebDAVDialog;
import com.fongmi.android.tv.utils.FileChooser; import com.fongmi.android.tv.utils.FileChooser;
import com.fongmi.android.tv.utils.FileUtil; import com.fongmi.android.tv.utils.FileUtil;
import com.fongmi.android.tv.utils.Notify; import com.fongmi.android.tv.utils.Notify;
import com.fongmi.android.tv.utils.WebDAVSyncManager;
import com.fongmi.android.tv.utils.ResUtil; import com.fongmi.android.tv.utils.ResUtil;
import com.fongmi.android.tv.utils.UrlUtil; import com.fongmi.android.tv.utils.UrlUtil;
import com.github.catvod.bean.Doh; import com.github.catvod.bean.Doh;
@@ -49,6 +61,10 @@ import com.github.catvod.utils.Path;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.permissionx.guolindev.PermissionX; import com.permissionx.guolindev.PermissionX;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -91,25 +107,10 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
@Override @Override
protected void initView() { protected void initView() {
mBinding.vodUrl.setText(VodConfig.getDesc()); setSourceHintText(mBinding.vodUrl, VodConfig.getDesc(), R.string.source_hint_setting);
mBinding.liveUrl.setText(LiveConfig.getDesc()); setSourceHintText(mBinding.liveUrl, LiveConfig.getDesc(), R.string.source_hint_live);
mBinding.wallUrl.setText(WallConfig.getDesc()); // setSourceHintText(mBinding.wallUrl, WallConfig.getDesc(), R.string.source_hint_wall); // 壁纸功能已移除
mBinding.versionText.setText(BuildConfig.VERSION_NAME); mBinding.versionText.setText(getString(R.string.setting_version) + " " + 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);
setOtherText(); setOtherText();
setCacheText(); setCacheText();
@@ -122,7 +123,53 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
mBinding.dohText.setText(getDohList()[getDohIndex()]); mBinding.dohText.setText(getDohList()[getDohIndex()]);
mBinding.proxyText.setText(getProxy(Setting.getProxy())); mBinding.proxyText.setText(getProxy(Setting.getProxy()));
mBinding.incognitoSwitch.setChecked(Setting.isIncognito()); mBinding.incognitoSwitch.setChecked(Setting.isIncognito());
mBinding.liveTabVisibleSwitch.setChecked(Setting.isLiveTabVisible());
mBinding.sizeText.setText((size = ResUtil.getStringArray(R.array.select_size))[Setting.getSize()]); mBinding.sizeText.setText((size = ResUtil.getStringArray(R.array.select_size))[Setting.getSize()]);
setWebDAVStatus();
setLiveSettingsVisibility();
}
private void setWebDAVStatus() {
WebDAVSyncManager manager = WebDAVSyncManager.get();
if (manager.isConfigured()) {
// 显示账号昵称用户名
String username = Setting.getWebDAVUsername();
if (!TextUtils.isEmpty(username)) {
// 如果用户名是邮箱只显示@前面的部分
String displayName = username;
if (username.contains("@")) {
displayName = username.substring(0, username.indexOf("@"));
}
String status = Setting.isWebDAVAutoSync() ? displayName + "(自动同步)" : displayName;
mBinding.webdavStatusText.setText(status);
} else {
String status = Setting.isWebDAVAutoSync() ? "已配置(自动同步)" : "已配置";
mBinding.webdavStatusText.setText(status);
}
} else {
mBinding.webdavStatusText.setText("未配置");
}
}
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() { private void setCacheText() {
@@ -138,73 +185,121 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
protected void initEvent() { protected void initEvent() {
mBinding.vod.setOnClickListener(this::onVod); mBinding.vod.setOnClickListener(this::onVod);
mBinding.live.setOnClickListener(this::onLive); mBinding.live.setOnClickListener(this::onLive);
mBinding.wall.setOnClickListener(this::onWall); // mBinding.wall.setOnClickListener(this::onWall); // 壁纸功能已移除
mBinding.proxy.setOnClickListener(this::onProxy); mBinding.proxy.setOnClickListener(this::onProxy);
mBinding.cache.setOnClickListener(this::onCache); mBinding.cache.setOnClickListener(this::onCache);
mBinding.backup.setOnClickListener(this::onBackup); mBinding.backup.setOnClickListener(this::onBackup);
mBinding.player.setOnClickListener(this::onPlayer); mBinding.player.setOnClickListener(this::onPlayer);
mBinding.restore.setOnClickListener(this::onRestore); mBinding.restore.setOnClickListener(this::onRestore);
mBinding.version.setOnClickListener(this::onVersion); mBinding.version.setOnClickListener(this::onVersion);
mBinding.about.setOnClickListener(this::onAbout);
mBinding.vod.setOnLongClickListener(this::onVodEdit); mBinding.vod.setOnLongClickListener(this::onVodEdit);
mBinding.vodHome.setOnClickListener(this::onVodHome); mBinding.vodHome.setOnClickListener(this::onVodHome);
mBinding.live.setOnLongClickListener(this::onLiveEdit); mBinding.live.setOnLongClickListener(this::onLiveEdit);
mBinding.liveHome.setOnClickListener(this::onLiveHome); mBinding.liveHome.setOnClickListener(this::onLiveHome);
mBinding.wall.setOnLongClickListener(this::onWallEdit); // mBinding.wall.setOnLongClickListener(this::onWallEdit); // 壁纸功能已移除
mBinding.vodHistory.setOnClickListener(this::onVodHistory); mBinding.vodHistory.setOnClickListener(this::onVodHistory);
mBinding.version.setOnLongClickListener(this::onVersionDev); mBinding.version.setOnLongClickListener(this::onVersionDev);
mBinding.liveHistory.setOnClickListener(this::onLiveHistory); mBinding.liveHistory.setOnClickListener(this::onLiveHistory);
mBinding.wallDefault.setOnClickListener(this::setWallDefault); // mBinding.wallDefault.setOnClickListener(this::setWallDefault); // 壁纸功能已移除
mBinding.wallRefresh.setOnClickListener(this::setWallRefresh); // mBinding.wallRefresh.setOnClickListener(this::setWallRefresh); // 壁纸功能已移除
mBinding.incognito.setOnClickListener(this::setIncognito); mBinding.incognitoSwitch.setOnClickListener(this::setIncognito);
mBinding.liveTabVisibleSwitch.setOnClickListener(this::setLiveTabVisible);
mBinding.size.setOnClickListener(this::setSize); mBinding.size.setOnClickListener(this::setSize);
mBinding.doh.setOnClickListener(this::setDoh); mBinding.doh.setOnClickListener(this::setDoh);
mBinding.webdav.setOnClickListener(this::onWebDAV);
} }
@Override @Override
public void setConfig(Config config) { public void setConfig(Config 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)) { 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)); PermissionX.init(this).permissions(Manifest.permission.WRITE_EXTERNAL_STORAGE).request((allGranted, grantedList, deniedList) -> {
if (getActivity() != null && isAdded()) {
load(config);
}
});
} else { } else {
load(config); load(config);
} }
} catch (Exception e) {
e.printStackTrace();
}
} }
private void load(Config config) { private void load(Config config) {
// 再次检查Fragment状态防止在异步回调中执行
if (getActivity() == null || !isAdded() || isDetached()) return;
try {
switch (config.getType()) { switch (config.getType()) {
case 0: case 0:
Notify.progress(getActivity()); Notify.progress(getActivity());
VodConfig.load(config, getCallback(0)); VodConfig.load(config, getCallback(0));
if (mBinding != null && mBinding.vodUrl != null) {
mBinding.vodUrl.setText(config.getDesc()); mBinding.vodUrl.setText(config.getDesc());
}
break; break;
case 1: case 1:
Notify.progress(getActivity()); Notify.progress(getActivity());
LiveConfig.load(config, getCallback(1)); LiveConfig.load(config, getCallback(1));
if (mBinding != null && mBinding.liveUrl != null) {
mBinding.liveUrl.setText(config.getDesc()); mBinding.liveUrl.setText(config.getDesc());
}
break; break;
case 2: case 2:
Notify.progress(getActivity()); Notify.progress(getActivity());
WallConfig.load(config, getCallback(2)); WallConfig.load(config, getCallback(2));
mBinding.wallUrl.setText(config.getDesc()); // if (mBinding != null && mBinding.wallUrl != null) { // 壁纸功能已移除
// mBinding.wallUrl.setText(config.getDesc());
// }
break; break;
} }
} catch (Exception e) {
e.printStackTrace();
Notify.dismiss();
}
} }
private Callback getCallback(int type) { private Callback getCallback(int type) {
return new Callback() { return new Callback() {
@Override @Override
public void success(String result) { public void success(String result) {
// 检查Fragment是否还在活动状态
if (getActivity() == null || !isAdded()) return;
Notify.show(result); Notify.show(result);
} }
@Override @Override
public void success() { public void success() {
// 检查Fragment是否还在活动状态
if (getActivity() == null || !isAdded()) return;
setConfig(type); setConfig(type);
} }
@Override @Override
public void error(String msg) { public void error(String msg) {
// 检查Fragment是否还在活动状态
if (getActivity() == null || !isAdded()) return;
Notify.show(msg); 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 +311,37 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
Notify.dismiss(); Notify.dismiss();
RefreshEvent.video(); RefreshEvent.video();
RefreshEvent.config(); RefreshEvent.config();
mBinding.vodUrl.setText(VodConfig.getDesc()); setSourceHintText(mBinding.vodUrl, VodConfig.getDesc(), R.string.source_hint_setting);
mBinding.liveUrl.setText(LiveConfig.getDesc()); setSourceHintText(mBinding.liveUrl, LiveConfig.getDesc(), R.string.source_hint_live);
mBinding.wallUrl.setText(WallConfig.getDesc()); // setSourceHintText(mBinding.wallUrl, WallConfig.getDesc(), R.string.source_hint_wall); // 壁纸功能已移除
break; break;
case 1: case 1:
setCacheText(); setCacheText();
Notify.dismiss(); Notify.dismiss();
RefreshEvent.config(); RefreshEvent.config();
mBinding.liveUrl.setText(LiveConfig.getDesc()); setSourceHintText(mBinding.liveUrl, LiveConfig.getDesc(), R.string.source_hint_live);
break; break;
case 2: case 2:
setCacheText(); setCacheText();
Notify.dismiss(); Notify.dismiss();
mBinding.wallUrl.setText(WallConfig.getDesc()); // setSourceHintText(mBinding.wallUrl, WallConfig.getDesc(), R.string.source_hint_wall); // 壁纸功能已移除
break; 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 @Override
public void setSite(Site item) { public void setSite(Site item) {
VodConfig.get().setHome(item); VodConfig.get().setHome(item);
@@ -293,13 +401,17 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
} }
private void onPlayer(View view) { private void onPlayer(View view) {
getRoot().change(2); SettingPlayerActivity.start(requireActivity());
} }
private void onVersion(View view) { private void onVersion(View view) {
Updater.create().force().release().start(getActivity()); Updater.create().force().release().start(getActivity());
} }
private void onAbout(View view) {
AboutDialog.show(this);
}
private boolean onVersionDev(View view) { private boolean onVersionDev(View view) {
Updater.create().force().dev().start(getActivity()); Updater.create().force().dev().start(getActivity());
return true; return true;
@@ -323,7 +435,17 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
private void setIncognito(View view) { private void setIncognito(View view) {
boolean isChecked = !Setting.isIncognito(); boolean isChecked = !Setting.isIncognito();
Setting.putIncognito(isChecked); 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) { private void setSize(View view) {
@@ -406,19 +528,43 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
})); }));
} }
private void onWebDAV(View view) {
WebDAVDialog.create(this).show();
}
private void initConfig() { private void initConfig() {
WallConfig.get().init(); WallConfig.get().init();
LiveConfig.get().init().load(); LiveConfig.get().init().load();
VodConfig.get().init().load(getCallback(0)); VodConfig.get().init().load(getCallback(0));
} }
@Override
public void onResume() {
super.onResume();
EventBus.getDefault().register(this);
}
@Override
public void onPause() {
super.onPause();
EventBus.getDefault().unregister(this);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onRefreshEvent(RefreshEvent event) {
if (event.getType() == RefreshEvent.Type.CONFIG) {
setWebDAVStatus();
}
}
@Override @Override
public void onHiddenChanged(boolean hidden) { public void onHiddenChanged(boolean hidden) {
if (hidden) return; if (hidden) return;
mBinding.vodUrl.setText(VodConfig.getDesc()); setSourceHintText(mBinding.vodUrl, VodConfig.getDesc(), R.string.source_hint_setting);
mBinding.liveUrl.setText(LiveConfig.getDesc()); setSourceHintText(mBinding.liveUrl, LiveConfig.getDesc(), R.string.source_hint_live);
mBinding.wallUrl.setText(WallConfig.getDesc()); // setSourceHintText(mBinding.wallUrl, WallConfig.getDesc(), R.string.source_hint_wall); // 壁纸功能已移除
setCacheText(); setCacheText();
setWebDAVStatus(); // 更新WebDAV状态
} }
@Override @Override
@@ -26,6 +26,8 @@ import com.fongmi.android.tv.R;
import com.fongmi.android.tv.Setting; import com.fongmi.android.tv.Setting;
import com.fongmi.android.tv.api.config.VodConfig; import com.fongmi.android.tv.api.config.VodConfig;
import com.fongmi.android.tv.bean.Class; 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.Hot;
import com.fongmi.android.tv.bean.Result; import com.fongmi.android.tv.bean.Result;
import com.fongmi.android.tv.bean.Site; 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.RefreshEvent;
import com.fongmi.android.tv.event.StateEvent; import com.fongmi.android.tv.event.StateEvent;
import com.fongmi.android.tv.impl.Callback; 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.FilterCallback;
import com.fongmi.android.tv.impl.SiteCallback; import com.fongmi.android.tv.impl.SiteCallback;
import com.fongmi.android.tv.model.SiteViewModel; 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.HistoryActivity;
import com.fongmi.android.tv.ui.activity.KeepActivity; import com.fongmi.android.tv.ui.activity.KeepActivity;
import com.fongmi.android.tv.ui.activity.VideoActivity; 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.adapter.TypeAdapter;
import com.fongmi.android.tv.ui.base.BaseFragment; 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.FilterDialog;
import com.fongmi.android.tv.ui.dialog.LastWatchToast;
import com.fongmi.android.tv.ui.dialog.LinkDialog; import com.fongmi.android.tv.ui.dialog.LinkDialog;
import com.fongmi.android.tv.ui.dialog.ReceiveDialog; import com.fongmi.android.tv.ui.dialog.ReceiveDialog;
import com.fongmi.android.tv.ui.dialog.SiteDialog; import com.fongmi.android.tv.ui.dialog.SiteDialog;
import com.fongmi.android.tv.utils.FileChooser; 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.ResUtil;
import com.fongmi.android.tv.utils.UrlUtil; import com.fongmi.android.tv.utils.UrlUtil;
import com.github.catvod.net.OkHttp; import com.github.catvod.net.OkHttp;
@@ -68,7 +75,7 @@ import okhttp3.Call;
import okhttp3.Headers; import okhttp3.Headers;
import okhttp3.Response; 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 FragmentVodBinding mBinding;
private SiteViewModel mViewModel; private SiteViewModel mViewModel;
@@ -99,10 +106,37 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
EventBus.getDefault().register(this); EventBus.getDefault().register(this);
setRecyclerView(); setRecyclerView();
setViewModel(); setViewModel();
showProgress(); initStartupState(); // 根据是否已有配置来设置初始状态
setLogo(); setLogo();
initHot(); initHot();
getHot(); 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 @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() { private void setRecyclerView() {
mBinding.type.setHasFixedSize(true); mBinding.type.setHasFixedSize(true);
mBinding.type.setItemAnimator(null); mBinding.type.setItemAnimator(null);
@@ -173,10 +224,179 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
setFabVisible(0); setFabVisible(0);
hideProgress(); hideProgress();
checkRetry(); 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) { 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.top.setVisibility(View.INVISIBLE);
mBinding.link.setVisibility(View.VISIBLE); mBinding.link.setVisibility(View.VISIBLE);
mBinding.filter.setVisibility(View.GONE); mBinding.filter.setVisibility(View.GONE);
@@ -191,6 +411,13 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
} }
} }
// 隐藏所有悬浮按钮的方法
private void hideFabButtons() {
mBinding.top.setVisibility(View.GONE);
mBinding.link.setVisibility(View.GONE);
mBinding.filter.setVisibility(View.GONE);
}
private void checkRetry() { private void checkRetry() {
mBinding.retry.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); 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() { private void homeContent() {
showProgress(); showProgress();
setFabVisible(0); setFabVisible(0);
// 安全地隐藏空源提示
try {
if (mBinding != null && mBinding.emptySourceHint != null) {
mBinding.emptySourceHint.setVisibility(View.GONE);
}
} catch (Exception e) {
e.printStackTrace();
}
mAdapter.clear(); mAdapter.clear();
mViewModel.homeContent(); mViewModel.homeContent();
mBinding.pager.setAdapter(new PageAdapter(getChildFragmentManager())); mBinding.pager.setAdapter(new PageAdapter(getChildFragmentManager()));
@@ -296,6 +531,7 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
switch (event.getType()) { switch (event.getType()) {
case EMPTY: case EMPTY:
hideProgress(); hideProgress();
checkEmptySource(); // 添加检查是否显示空源提示
break; break;
case PROGRESS: case PROGRESS:
showProgress(); showProgress();
@@ -59,7 +59,7 @@ public class Timer {
public void delay() { public void delay() {
cancel(); cancel();
set(TimeUnit.MINUTES.toMillis(5) + tick); set(TimeUnit.MINUTES.toMillis(15) + tick);
} }
public void reset() { public void reset() {
+1 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <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" /> <item android:color="@color/white" android:state_checked="false" />
</selector> </selector>
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <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/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" /> <item android:color="@color/white" />
</selector> </selector>
+1 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <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" /> <item android:color="@color/white" android:state_checked="false" />
</selector> </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" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?attr/colorControlNormal" android:viewportWidth="24"
android:viewportWidth="960" android:viewportHeight="24">
android:viewportHeight="960">
<path <path
android:fillColor="@android:color/white" android:fillColor="#FF000000"
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: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> </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" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp" android:width="24dp"
android:height="48dp" android:height="24dp"
android:tint="?attr/colorControlNormal" android:viewportWidth="24"
android:viewportWidth="960" android:viewportHeight="24">
android:viewportHeight="960">
<path <path
android:fillColor="@android:color/white" android:fillColor="#FF000000"
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: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> </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" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="960" android:viewportWidth="960"
android:viewportHeight="960"> android:viewportHeight="960">
<path <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"?> <?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android" <ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#802196F3"> android:color="#80FFEB3B">
<item android:id="@android:id/mask"> <item android:id="@android:id/mask">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="#f5f5f5" /> <solid android:color="#f5f5f5" />
@@ -4,9 +4,6 @@
<item android:id="@android:id/background"> <item android:id="@android:id/background">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="@color/black_20" /> <solid android:color="@color/black_20" />
<stroke
android:width="1dp"
android:color="#BDBDBD" />
<corners android:radius="8dp" /> <corners android:radius="8dp" />
<padding <padding
android:bottom="14dp" android:bottom="14dp"
@@ -3,7 +3,7 @@
android:color="?attr/colorControlHighlight"> android:color="?attr/colorControlHighlight">
<item android:id="@android:id/background"> <item android:id="@android:id/background">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="@color/yellow_500" /> <solid android:color="@color/primary" />
<corners android:radius="8dp" /> <corners android:radius="8dp" />
<padding <padding
android:bottom="10dp" android:bottom="10dp"
@@ -3,7 +3,7 @@
android:color="?attr/colorControlHighlight"> android:color="?attr/colorControlHighlight">
<item android:id="@android:id/background"> <item android:id="@android:id/background">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="@color/yellow_500" /> <solid android:color="@color/primary" />
<corners android:radius="8dp" /> <corners android:radius="8dp" />
<padding <padding
android:bottom="18dp" android:bottom="18dp"
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android" <ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#8066BB6A"> android:color="#80FFEB3B">
<item android:id="@android:id/mask"> <item android:id="@android:id/mask">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="#f5f5f5" /> <solid android:color="#f5f5f5" />
+1 -1
View File
@@ -4,7 +4,7 @@
<item android:id="@android:id/background"> <item android:id="@android:id/background">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="@color/black_20" /> <solid android:color="@color/black_20" />
<corners android:radius="8dp" /> <corners android:radius="12dp" />
<padding <padding
android:bottom="14dp" android:bottom="14dp"
android:left="16dp" android:left="16dp"
@@ -3,7 +3,7 @@
android:color="?attr/colorControlHighlight"> android:color="?attr/colorControlHighlight">
<item android:id="@android:id/background"> <item android:id="@android:id/background">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="@color/yellow_500" /> <solid android:color="@color/primary" />
<corners android:radius="8dp" /> <corners android:radius="8dp" />
<padding <padding
android:bottom="18dp" 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" app:spanCount="2"
tools:listitem="@layout/adapter_vod_rect" /> 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> </RelativeLayout>
</LinearLayout> </LinearLayout>
+13 -3
View File
@@ -18,7 +18,7 @@
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="@drawable/shape_action_background"
android:src="@drawable/ic_back" /> android:src="@drawable/ic_back" />
<TextView <TextView
@@ -35,7 +35,7 @@
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="@drawable/shape_action_background"
android:src="@drawable/ic_action_sync" /> android:src="@drawable/ic_action_sync" />
<ImageView <ImageView
@@ -43,7 +43,7 @@
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="@drawable/shape_action_background"
android:src="@drawable/ic_action_delete" android:src="@drawable/ic_action_delete"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
@@ -65,5 +65,15 @@
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:paddingBottom="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> </FrameLayout>
</LinearLayout> </LinearLayout>
+2 -2
View File
@@ -14,14 +14,14 @@
<com.google.android.material.bottomnavigation.BottomNavigationView <com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/navigation" android:id="@+id/navigation"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="68dp" android:layout_height="70dp"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:background="@color/transparent" android:background="@color/transparent"
app:elevation="0dp" app:elevation="0dp"
app:itemIconSize="24dp" app:itemIconSize="24dp"
app:itemIconTint="@color/nav" app:itemIconTint="@color/nav"
app:itemTextColor="@color/nav" app:itemTextColor="@color/nav"
app:labelVisibilityMode="labeled" app:labelVisibilityMode="unlabeled"
app:itemActiveIndicatorStyle="@style/Indicator" app:itemActiveIndicatorStyle="@style/Indicator"
app:menu="@menu/menu_nav" /> app:menu="@menu/menu_nav" />
+12 -3
View File
@@ -18,7 +18,7 @@
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="@drawable/shape_action_background"
android:src="@drawable/ic_back" /> android:src="@drawable/ic_back" />
<TextView <TextView
@@ -35,7 +35,7 @@
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="@drawable/shape_action_background"
android:src="@drawable/ic_action_sync" /> android:src="@drawable/ic_action_sync" />
<ImageView <ImageView
@@ -43,7 +43,7 @@
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="@drawable/shape_action_background"
android:src="@drawable/ic_action_delete" android:src="@drawable/ic_action_delete"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
@@ -65,5 +65,14 @@
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:paddingBottom="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> </FrameLayout>
</LinearLayout> </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:id="@+id/text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" 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:background="@drawable/shape_item"
android:gravity="center" android:gravity="center"
android:padding="8dp" android:padding="8dp"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/text" android:textColor="@color/text"
android:textSize="14sp" android:textSize="12sp"
tools:text="泥巴" /> tools:text="泥巴" />
+24 -4
View File
@@ -6,18 +6,38 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
<com.google.android.material.button.MaterialButton <FrameLayout
android:id="@+id/text"
style="@style/Widget.App.Button.OutlinedButton.SiteDialog"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" 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:ellipsize="middle"
android:singleLine="true" android:singleLine="true"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="14dp"
android:paddingBottom="14dp"
android:textColor="?android:attr/textColorPrimary" android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp" android:textSize="14sp"
tools:text="https://fongmi.github.io/cat.json" /> 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 <ImageView
android:id="@+id/delete" android:id="@+id/delete"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -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项目。"
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:stepSize="1"
android:valueFrom="1" android:valueFrom="1"
android:valueTo="10" android:valueTo="10"
app:thumbColor="@color/yellow_500" app:thumbColor="@color/accent"
app:thumbRadius="9dp"
app:tickVisible="false" app:tickVisible="false"
app:trackColorActive="@color/yellow_500" app:trackColorActive="@color/accent"
app:trackColorInactive="@color/yellow_50" /> app:trackColorInactive="@color/white_30" />
</LinearLayout> </LinearLayout>
+24 -5
View File
@@ -8,13 +8,31 @@
android:padding="16dp" android:padding="16dp"
tools:background="@color/white"> tools:background="@color/white">
<TextView <LinearLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
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:text="@string/control_speed"
android:textColor="?android:attr/textColorPrimary" android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp" /> 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 <com.google.android.material.slider.Slider
android:id="@+id/speed" android:id="@+id/speed"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -24,10 +42,11 @@
android:stepSize="0.25" android:stepSize="0.25"
android:valueFrom="0.5" android:valueFrom="0.5"
android:valueTo="5" android:valueTo="5"
app:thumbColor="@color/yellow_500" app:thumbColor="@color/accent"
app:thumbRadius="9dp"
app:tickVisible="false" app:tickVisible="false"
app:trackColorActive="@color/yellow_500" app:trackColorActive="@color/accent"
app:trackColorInactive="@color/yellow_50" /> app:trackColorInactive="@color/white_30" />
<TextView <TextView
android:id="@+id/parseText" android:id="@+id/parseText"
+3 -1
View File
@@ -15,8 +15,10 @@
android:stepSize="0.5" android:stepSize="0.5"
android:valueFrom="2" android:valueFrom="2"
android:valueTo="5" android:valueTo="5"
app:thumbColor="@color/accent"
app:thumbRadius="9dp"
app:tickVisible="false" app:tickVisible="false"
app:trackColorActive="@color/accent" app:trackColorActive="@color/accent"
app:trackColorInactive="@color/yellow_50" /> app:trackColorInactive="@color/white_30" />
</LinearLayout> </LinearLayout>
+64 -18
View File
@@ -29,50 +29,80 @@
<TextView <TextView
android:id="@+id/time1" android:id="@+id/time1"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="56dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:background="@drawable/shape_accent" android:background="@drawable/shape_accent_no_border"
android:tag="5" android:tag="5"
android:text="@string/timer_5" android:text="@string/timer_5"
android:textColor="?android:attr/textColorPrimary" android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp" /> android:textSize="14sp"
android:gravity="center" />
<TextView <TextView
android:id="@+id/time2" android:id="@+id/time2"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="56dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:background="@drawable/shape_accent" android:background="@drawable/shape_accent_no_border"
android:tag="15" android:tag="15"
android:text="@string/timer_15" android:text="@string/timer_15"
android:textColor="?android:attr/textColorPrimary" android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp" /> android:textSize="14sp"
android:gravity="center" />
<TextView <TextView
android:id="@+id/time3" android:id="@+id/time3"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="56dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:background="@drawable/shape_accent" android:background="@drawable/shape_accent_no_border"
android:tag="30" android:tag="30"
android:text="@string/timer_30" android:text="@string/timer_30"
android:textColor="?android:attr/textColorPrimary" android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp" /> android:textSize="14sp"
android:gravity="center" />
<TextView <TextView
android:id="@+id/time4" android:id="@+id/time4"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="56dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:background="@drawable/shape_accent" android:background="@drawable/shape_accent_no_border"
android:tag="60" android:tag="60"
android:text="@string/timer_60" android:text="@string/timer_60"
android:textColor="?android:attr/textColorPrimary" 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> </LinearLayout>
@@ -96,25 +126,41 @@
android:textStyle="bold" android:textStyle="bold"
tools:text="5:00" /> tools:text="5:00" />
<com.google.android.material.button.MaterialButton <TextView
android:id="@+id/delay" android:id="@+id/delay"
style="?attr/materialButtonStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="8dp" 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:singleLine="true"
android:text="@string/timer_delay" 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" android:id="@+id/reset"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp" 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:singleLine="true"
android:text="@string/timer_cancel" android:text="@string/timer_cancel"
android:textColor="?android:attr/textColorPrimary" /> android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
+206
View File
@@ -0,0 +1,206 @@
<?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="wrap_content"
android:orientation="vertical"
android:paddingStart="24dp"
android:paddingTop="16dp"
android:paddingEnd="24dp"
android:paddingBottom="16dp">
<!-- 说明文字 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="请输入您在WebDAV服务提供商(如坚果云)注册的账号和密码(坚果云的密码为应用密码而非登录密码)"
android:textColor="@color/white"
android:textSize="12sp"
android:alpha="0.7"
android:lineSpacingMultiplier="1.2" />
<!-- 服务提供商选择 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="服务提供商"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/providerText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:text="坚果云"
android:textColor="@color/white"
android:textSize="16sp"
android:background="?attr/selectableItemBackground"
android:padding="12dp"
android:clickable="true"
android:focusable="true" />
</LinearLayout>
<!-- 服务器地址(自定义时显示) -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/urlInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:visibility="gone"
app:hintEnabled="false">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/urlText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="WebDAV服务器地址(如:https://example.com/webdav"
android:imeOptions="actionNext"
android:importantForAutofill="no"
android:inputType="textUri"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<!-- 用户名 -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/usernameInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:hintEnabled="false">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/usernameText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="用户名"
android:imeOptions="actionNext"
android:importantForAutofill="no"
android:inputType="text"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<!-- 密码 -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/passwordInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:hintEnabled="false"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/passwordText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="密码"
android:imeOptions="actionDone"
android:importantForAutofill="no"
android:inputType="textPassword"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<!-- 自动同步开关 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="自动同步"
android:textColor="@color/white"
android:textSize="16sp" />
<com.fongmi.android.tv.ui.custom.CustomSwitch
android:id="@+id/autoSyncSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<!-- 同步间隔 -->
<LinearLayout
android:id="@+id/syncIntervalContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="同步间隔(分钟)"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/syncIntervalText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="30"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
<!-- 操作按钮区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<!-- 测试连接按钮 -->
<com.google.android.material.button.MaterialButton
android:id="@+id/testButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="测试连接"
style="@style/Widget.Material3.Button.OutlinedButton" />
<!-- 立即同步按钮 -->
<com.google.android.material.button.MaterialButton
android:id="@+id/syncButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="立即同步"
style="@style/Widget.Material3.Button.OutlinedButton" />
</LinearLayout>
<!-- 状态提示 -->
<TextView
android:id="@+id/statusText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:text=""
android:textColor="@color/white"
android:textSize="14sp"
android:visibility="gone" />
</LinearLayout>
+251 -103
View File
@@ -44,6 +44,7 @@
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:fillViewport="true" android:fillViewport="true"
android:overScrollMode="never" android:overScrollMode="never"
app:layout_behavior="@string/appbar_scrolling_view_behavior"> app:layout_behavior="@string/appbar_scrolling_view_behavior">
@@ -58,16 +59,32 @@
android:paddingBottom="24dp"> android:paddingBottom="24dp">
<!-- 源管理分组 --> <!-- 源管理分组 -->
<TextView <LinearLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/setting_source" android:text="@string/setting_source"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="14sp" android:textSize="14sp"
android:alpha="0.7" /> 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 <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -77,10 +94,11 @@
<LinearLayout <LinearLayout
android:id="@+id/vod" android:id="@+id/vod"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="56dp"
android:layout_marginEnd="12dp" android:layout_marginEnd="12dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@drawable/shape_item" android:background="@drawable/shape_item"
android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView <TextView
@@ -112,7 +130,8 @@
android:background="@drawable/shape_item" android:background="@drawable/shape_item"
android:padding="16dp" android:padding="16dp"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/ic_setting_home" /> android:src="@drawable/potted_plant_24px"
android:tint="@color/white" />
<ImageView <ImageView
android:id="@+id/vodHistory" android:id="@+id/vodHistory"
@@ -121,11 +140,13 @@
android:background="@drawable/shape_item" android:background="@drawable/shape_item"
android:padding="16dp" android:padding="16dp"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/ic_setting_history" /> android:src="@drawable/ic_m3_list_alt"
android:tint="@color/white" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/liveContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
@@ -135,10 +156,11 @@
<LinearLayout <LinearLayout
android:id="@+id/live" android:id="@+id/live"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="56dp"
android:layout_marginEnd="12dp" android:layout_marginEnd="12dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@drawable/shape_item" android:background="@drawable/shape_item"
android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView <TextView
@@ -170,7 +192,8 @@
android:background="@drawable/shape_item" android:background="@drawable/shape_item"
android:padding="16dp" android:padding="16dp"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/ic_setting_home" /> android:src="@drawable/potted_plant_24px"
android:tint="@color/white" />
<ImageView <ImageView
android:id="@+id/liveHistory" android:id="@+id/liveHistory"
@@ -179,67 +202,11 @@
android:background="@drawable/shape_item" android:background="@drawable/shape_item"
android:padding="16dp" android:padding="16dp"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/ic_setting_history" /> android:src="@drawable/ic_m3_list_alt"
android:tint="@color/white" />
</LinearLayout> </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 <TextView
@@ -253,15 +220,31 @@
android:textSize="14sp" android:textSize="14sp"
android:alpha="0.7" /> android:alpha="0.7" />
<!-- 将三个设置项放在一个共同的背景容器中 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_item"
android:orientation="vertical">
<!-- 播放器设置 -->
<LinearLayout <LinearLayout
android:id="@+id/player" android:id="@+id/player"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/shape_item" android:orientation="horizontal"
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 <TextView
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/setting_player" android:text="@string/setting_player"
android:textColor="@color/white" android:textColor="@color/white"
@@ -269,17 +252,60 @@
</LinearLayout> </LinearLayout>
<!-- WebDAV同步配置 -->
<LinearLayout
android:id="@+id/webdav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="12dp"
android:paddingBottom="12dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_fab_link"
android:tint="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="WebDAV"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/webdavStatusText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:text="未配置"
android:textColor="@color/white"
android:textSize="14sp"
android:alpha="0.7" />
</LinearLayout>
<!-- 无痕模式 -->
<LinearLayout <LinearLayout
android:id="@+id/incognito" android:id="@+id/incognito"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:background="@drawable/shape_item"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingTop="16dp" android:paddingTop="16dp"
android:paddingBottom="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 <TextView
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -288,21 +314,60 @@
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="16sp" /> android:textSize="16sp" />
<androidx.appcompat.widget.SwitchCompat <com.fongmi.android.tv.ui.custom.CustomSwitch
android:id="@+id/incognitoSwitch" android:id="@+id/incognitoSwitch"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content" />
style="@style/M3SwitchStyle" />
</LinearLayout> </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 <LinearLayout
android:id="@+id/size" android:id="@+id/size"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:orientation="horizontal"
android:background="@drawable/shape_item" android:paddingTop="12dp"
android:orientation="horizontal"> 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 <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -322,6 +387,7 @@
tools:text="Medium" /> tools:text="Medium" />
</LinearLayout> </LinearLayout>
</LinearLayout>
<!-- 网络设置分组 --> <!-- 网络设置分组 -->
<TextView <TextView
@@ -335,12 +401,28 @@
android:textSize="14sp" android:textSize="14sp"
android:alpha="0.7" /> android:alpha="0.7" />
<!-- 将三个网络设置项放在一个共同的背景容器中 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_item"
android:orientation="vertical">
<!-- DoH设置 -->
<LinearLayout <LinearLayout
android:id="@+id/doh" android:id="@+id/doh"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/shape_item" android:orientation="horizontal"
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/globe_book_24px"
android:tint="@color/white" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -361,13 +443,21 @@
</LinearLayout> </LinearLayout>
<!-- 代理设置 -->
<LinearLayout <LinearLayout
android:id="@+id/proxy" android:id="@+id/proxy"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:orientation="horizontal"
android:background="@drawable/shape_item" android:paddingTop="16dp"
android:orientation="horizontal"> 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" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -388,13 +478,21 @@
</LinearLayout> </LinearLayout>
<!-- 缓存设置 -->
<LinearLayout <LinearLayout
android:id="@+id/cache" android:id="@+id/cache"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:orientation="horizontal"
android:background="@drawable/shape_item" android:paddingTop="16dp"
android:orientation="horizontal"> 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 <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -414,6 +512,7 @@
tools:text="1.0 MB" /> tools:text="1.0 MB" />
</LinearLayout> </LinearLayout>
</LinearLayout>
<!-- 备份与恢复分组 --> <!-- 备份与恢复分组 -->
<TextView <TextView
@@ -434,63 +533,112 @@
android:orientation="horizontal" android:orientation="horizontal"
android:padding="0dp"> android:padding="0dp">
<TextView <LinearLayout
android:id="@+id/backup"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:background="?android:attr/selectableItemBackground" android:gravity="center_vertical"
android:paddingStart="16dp" 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:paddingTop="16dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp" android:paddingBottom="16dp"
android:text="@string/setting_backup" android:text="@string/setting_backup"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="16sp" /> android:textSize="16sp" />
</LinearLayout>
<TextView <LinearLayout
android:id="@+id/restore"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:background="?android:attr/selectableItemBackground" 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:gravity="end"
android:paddingStart="16dp"
android:paddingTop="16dp" android:paddingTop="16dp"
android:paddingEnd="16dp" android:paddingEnd="16dp"
android:paddingBottom="16dp" android:paddingBottom="16dp"
android:text="@string/setting_restore" android:text="@string/setting_restore"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="16sp" /> android:textSize="16sp" />
</LinearLayout>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/version"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="12dp"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/version"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="12dp"
android:background="@drawable/shape_item" android:background="@drawable/shape_item"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView <ImageView
android:layout_width="wrap_content" android:layout_width="24dp"
android:layout_height="wrap_content" android:layout_height="24dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:text="@string/setting_version" android:src="@drawable/ic_setting_github"
android:textColor="@color/white" android:tint="@color/white" />
android:textSize="16sp" />
<TextView <TextView
android:id="@+id/versionText" android:id="@+id/versionText"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="end" android:text="@string/setting_version"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="16sp" android:textSize="16sp"
tools:text="1.2.1" /> tools:text="版本 3.0.3" />
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/about"
android:layout_width="0dp"
android:layout_height="wrap_content"
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> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -55,14 +55,26 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:animateLayoutChanges="true" android:animateLayoutChanges="true"
android:orientation="vertical" android:orientation="vertical"
android:padding="16dp"> android:paddingStart="24dp"
android:paddingTop="16dp"
android:paddingEnd="24dp"
android:paddingBottom="16dp">
<!-- 播放器基本设置模块 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_item"
android:orientation="vertical">
<!-- 渲染方式 -->
<LinearLayout <LinearLayout
android:id="@+id/render" android:id="@+id/render"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/shape_item" android:orientation="horizontal"
android:orientation="horizontal"> android:padding="16dp"
android:background="?attr/selectableItemBackground">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -83,13 +95,21 @@
</LinearLayout> </LinearLayout>
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:background="#22FFFFFF" />
<!-- 缩放比例 -->
<LinearLayout <LinearLayout
android:id="@+id/scale" android:id="@+id/scale"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:orientation="horizontal"
android:background="@drawable/shape_item" android:padding="16dp"
android:orientation="horizontal"> android:background="?attr/selectableItemBackground">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -110,13 +130,21 @@
</LinearLayout> </LinearLayout>
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:background="#22FFFFFF" />
<!-- 字幕样式 -->
<LinearLayout <LinearLayout
android:id="@+id/caption" android:id="@+id/caption"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:orientation="horizontal"
android:background="@drawable/shape_item" android:padding="16dp"
android:orientation="horizontal"> android:background="?attr/selectableItemBackground">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -139,13 +167,21 @@
</LinearLayout> </LinearLayout>
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:background="#22FFFFFF" />
<!-- 缓冲时间 -->
<LinearLayout <LinearLayout
android:id="@+id/buffer" android:id="@+id/buffer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:orientation="horizontal"
android:background="@drawable/shape_item" android:padding="16dp"
android:orientation="horizontal"> android:background="?attr/selectableItemBackground">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -175,13 +211,21 @@
</LinearLayout> </LinearLayout>
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:background="#22FFFFFF" />
<!-- 长按倍速 -->
<LinearLayout <LinearLayout
android:id="@+id/speed" android:id="@+id/speed"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:orientation="horizontal"
android:background="@drawable/shape_item" android:padding="16dp"
android:orientation="horizontal"> android:background="?attr/selectableItemBackground">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -211,121 +255,21 @@
</LinearLayout> </LinearLayout>
<LinearLayout <!-- 分隔线 -->
android:id="@+id/tunnel" <View
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="1dp"
android:layout_marginTop="16dp" android:layout_marginStart="16dp"
android:background="@drawable/shape_item" android:background="#22FFFFFF" />
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 <LinearLayout
android:id="@+id/background" android:id="@+id/background"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:orientation="horizontal"
android:background="@drawable/shape_item" android:padding="16dp"
android:orientation="horizontal"> android:background="?attr/selectableItemBackground">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -346,13 +290,21 @@
</LinearLayout> </LinearLayout>
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:background="#22FFFFFF" />
<!-- User-Agent -->
<LinearLayout <LinearLayout
android:id="@+id/ua" android:id="@+id/ua"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:orientation="horizontal"
android:background="@drawable/shape_item" android:padding="16dp"
android:orientation="horizontal"> android:background="?attr/selectableItemBackground">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -375,5 +327,136 @@
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<!-- 开关设置模块 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/shape_item"
android:orientation="vertical">
<!-- 隧道模式 -->
<LinearLayout
android:id="@+id/tunnel"
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_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> </androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
+43 -4
View File
@@ -28,7 +28,7 @@
android:id="@+id/logo" android:id="@+id/logo"
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="@drawable/shape_action_background"
android:src="@drawable/ic_logo" /> android:src="@drawable/ic_logo" />
<LinearLayout <LinearLayout
@@ -66,14 +66,14 @@
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_marginEnd="12dp" android:layout_marginEnd="12dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="@drawable/shape_action_background"
android:src="@drawable/ic_action_keep" /> android:src="@drawable/ic_action_keep" />
<ImageView <ImageView
android:id="@+id/history" android:id="@+id/history"
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="@drawable/shape_action_background"
android:src="@drawable/ic_action_history" /> android:src="@drawable/ic_action_history" />
</LinearLayout> </LinearLayout>
@@ -100,12 +100,51 @@
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" /> 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 <ImageView
android:id="@+id/retry" android:id="@+id/retry"
android:layout_width="56dp" android:layout_width="56dp"
android:layout_height="56dp" android:layout_height="56dp"
android:layout_gravity="center" android:layout_gravity="center"
android:background="?attr/selectableItemBackgroundBorderless" android:background="@drawable/shape_action_background"
android:src="@drawable/ic_action_retry" android:src="@drawable/ic_action_retry"
android:visibility="gone" /> android:visibility="gone" />
@@ -23,6 +23,8 @@
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_weight="1" android:layout_weight="1"
app:bar_height="2dp" app:bar_height="2dp"
app:scrubber_enabled_size="14dp"
app:scrubber_disabled_size="14dp"
app:played_color="#FFEB3B" app:played_color="#FFEB3B"
app:scrubber_color="#FFEB3B" app:scrubber_color="#FFEB3B"
app:buffered_color="#80FFEB3B" /> app:buffered_color="#80FFEB3B" />
@@ -47,6 +47,40 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> 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 <ImageView
android:id="@+id/cast" android:id="@+id/cast"
android:layout_width="wrap_content" 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_15">15 分钟</string>
<string name="timer_30">30 分钟</string> <string name="timer_30">30 分钟</string>
<string name="timer_60">1 小时</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> <string name="timer_cancel">取消定时器</string>
<!-- Hint --> <!-- Hint -->
+3 -1
View File
@@ -41,7 +41,9 @@
<string name="timer_15">15 分鐘</string> <string name="timer_15">15 分鐘</string>
<string name="timer_30">30 分鐘</string> <string name="timer_30">30 分鐘</string>
<string name="timer_60">1 小時</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> <string name="timer_cancel">取消定時器</string>
<!-- Hint --> <!-- Hint -->
+3
View File
@@ -5,5 +5,8 @@
<color name="accent">#FFEB3B</color> <color name="accent">#FFEB3B</color>
<color name="indicator">@color/white_80</color> <color name="indicator">@color/white_80</color>
<color name="yellow_50">#4DFFEB3B</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> </resources>
+3 -1
View File
@@ -43,7 +43,9 @@
<string name="timer_15">15 minutes</string> <string name="timer_15">15 minutes</string>
<string name="timer_30">30 minutes</string> <string name="timer_30">30 minutes</string>
<string name="timer_60">1 hour</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> <string name="timer_cancel">Cancel timer</string>
<!-- Hint --> <!-- Hint -->
+14 -16
View File
@@ -17,6 +17,7 @@
<item name="colorPrimary">@color/primary</item> <item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primaryDark</item> <item name="colorPrimaryDark">@color/primaryDark</item>
<item name="colorAccent">@color/accent</item> <item name="colorAccent">@color/accent</item>
<item name="colorControlHighlight">@color/white_20</item>
<item name="android:windowBackground">@null</item> <item name="android:windowBackground">@null</item>
<item name="android:windowDisablePreview">true</item> <item name="android:windowDisablePreview">true</item>
<item name="android:navigationBarColor">@color/transparent</item> <item name="android:navigationBarColor">@color/transparent</item>
@@ -30,12 +31,8 @@
<style name="Control.Action"> <style name="Control.Action">
<item name="android:layout_width">wrap_content</item> <item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">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: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:textColor">@color/white</item>
<item name="android:textSize">14sp</item> <item name="android:textSize">14sp</item>
</style> </style>
@@ -60,7 +57,8 @@
</style> </style>
<style name="Indicator" parent="Widget.Material3.BottomNavigationView.ActiveIndicator"> <style name="Indicator" parent="Widget.Material3.BottomNavigationView.ActiveIndicator">
<item name="android:color">#1F1F1F</item> <item name="android:color">@color/primary</item>
<item name="android:height">42dp</item>
</style> </style>
<style name="BottomNavigationView.TextAppearance" parent="TextAppearance.AppCompat"> <style name="BottomNavigationView.TextAppearance" parent="TextAppearance.AppCompat">
@@ -69,20 +67,20 @@
</style> </style>
<style name="BottomNavigation" parent="Widget.Material3.BottomNavigationView"> <style name="BottomNavigation" parent="Widget.Material3.BottomNavigationView">
<item name="itemPaddingTop">8dp</item> <item name="itemPaddingTop">4dp</item>
<item name="itemPaddingBottom">8dp</item> <item name="itemPaddingBottom">4dp</item>
<item name="itemTextAppearanceActive">@style/BottomNavigationView.TextAppearance</item> <item name="itemTextAppearanceActive">@style/BottomNavigationView.TextAppearance</item>
<item name="itemTextAppearanceInactive">@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> </style>
<!-- M3 Switch Style --> <!-- M3 Switch Style (Material Design) - 使用CustomSwitch完全自定义 -->
<style name="M3SwitchStyle" parent="Widget.AppCompat.CompoundButton.Switch"> <style name="M3SwitchStyle" parent="Widget.AppCompat.CompoundButton.CheckBox">
<item name="android:thumb">@drawable/m3_switch_thumb</item> <item name="android:button">@null</item>
<item name="track">@drawable/m3_switch_track</item> <item name="android:background">@drawable/custom_switch_bg</item>
<item name="android:switchMinWidth">52dp</item> <item name="android:minHeight">30dp</item>
<item name="android:switchPadding">8dp</item> <item name="android:minWidth">50dp</item>
<item name="android:colorControlActivated">#FFEB3B</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style> </style>
<!-- 自定义数据源按钮样式 --> <!-- 自定义数据源按钮样式 -->
+5 -5
View File
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' version '8.8.2' apply false id 'com.android.application' version '8.12.0' apply false
id 'com.android.library' version '8.8.2' apply false id 'com.android.library' version '8.12.0' apply false
id 'com.chaquo.python' version '15.0.1' apply false id 'com.chaquo.python' version '16.1.0' apply false
} }
tasks.register('clean', Delete) { tasks.register('clean', Delete) {
@@ -10,6 +10,6 @@ tasks.register('clean', Delete) {
project.ext { project.ext {
gsonVersion = '2.11.0' gsonVersion = '2.11.0'
media3Version = '1.6.1' media3Version = '1.4.1'
okhttpVersion = '5.0.0-alpha.14' okhttpVersion = '4.12.0'
} }
+69
View File
@@ -0,0 +1,69 @@
#!/bin/bash
VERSION="3.1.1"
DESKTOP_PATH="$HOME/Desktop"
PROJECT_PATH="/Users/chen/Desktop/XMBOX-3.1.0"
cd "$PROJECT_PATH"
echo "========================================="
echo " 构建 XMBOX 所有 Release 包 (v${VERSION})"
echo "========================================="
echo ""
echo "=== 1. 清理旧的构建文件 ==="
./gradlew clean
echo ""
echo "=== 2. 构建所有 Release APK ==="
./gradlew assembleMobileArm64_v8aRelease \
assembleMobileArmeabi_v7aRelease \
assembleLeanbackArm64_v8aRelease \
assembleLeanbackArmeabi_v7aRelease
echo ""
echo "=== 3. 复制 APK 到桌面 ==="
# 定义APK路径和输出文件名
declare -a APKS=(
"app/build/outputs/apk/mobileArm64_v8a/release/mobile-arm64_v8a.apk|mobile-arm64_v8a-v${VERSION}.apk"
"app/build/outputs/apk/mobileArmeabi_v7a/release/mobile-armeabi_v7a.apk|mobile-armeabi_v7a-v${VERSION}.apk"
"app/build/outputs/apk/leanbackArm64_v8a/release/leanback-arm64_v8a.apk|leanback-arm64_v8a-v${VERSION}.apk"
"app/build/outputs/apk/leanbackArmeabi_v7a/release/leanback-armeabi_v7a.apk|leanback-armeabi_v7a-v${VERSION}.apk"
)
SUCCESS_COUNT=0
FAIL_COUNT=0
for apk_info in "${APKS[@]}"; do
IFS='|' read -r source_path target_name <<< "$apk_info"
if [ -f "$source_path" ]; then
cp "$source_path" "$DESKTOP_PATH/$target_name"
if [ $? -eq 0 ]; then
echo "$target_name"
ls -lh "$DESKTOP_PATH/$target_name" | awk '{print " 大小: " $5}'
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
else
echo "❌ 复制失败: $target_name"
FAIL_COUNT=$((FAIL_COUNT + 1))
fi
else
echo "❌ 文件不存在: $source_path"
FAIL_COUNT=$((FAIL_COUNT + 1))
fi
done
echo ""
echo "========================================="
if [ $FAIL_COUNT -eq 0 ]; then
echo "✅ 所有 APK 构建并复制成功!"
echo " 成功: $SUCCESS_COUNT"
echo " 位置: $DESKTOP_PATH"
else
echo "⚠️ 构建完成,但有 $FAIL_COUNT 个失败"
echo " 成功: $SUCCESS_COUNT"
echo " 失败: $FAIL_COUNT"
fi
echo "========================================="
Executable
+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
Executable
+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 { android {
namespace 'com.github.catvod.crawler' namespace 'com.github.catvod.crawler'
compileSdk 35 compileSdk {
version = release(36)
}
defaultConfig { defaultConfig {
minSdk 21 minSdk 24
targetSdk 28 targetSdk 28
} }
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_11 sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_17
} }
} }
dependencies { dependencies {
api 'androidx.annotation:annotation:1.6.0' api 'androidx.annotation:annotation:1.9.1'
api 'androidx.preference:preference:1.2.1' api 'androidx.preference:preference:1.2.1'
api 'com.google.code.gson:gson:' + gsonVersion api 'com.google.code.gson:gson:' + gsonVersion
api 'com.google.net.cronet:cronet-okhttp:0.1.0' api 'com.google.net.cronet:cronet-okhttp:0.1.0'
@@ -1,18 +1,195 @@
package com.github.catvod.utils; 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 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) { private static String getUrl(String path, String name) {
return URL + "/" + path + "/" + 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) { public static String getJson(boolean dev, String name) {
if (useCnMirror()) {
return getCnUrl("apk/" + (dev ? "dev" : "release"), name + ".json");
} else {
return getUrl("apk/" + (dev ? "dev" : "release"), name + ".json"); return getUrl("apk/" + (dev ? "dev" : "release"), name + ".json");
} }
}
public static String getApk(boolean dev, String name) { public static String getApk(boolean dev, String name) {
if (useCnMirror()) {
return getCnUrl("apk/" + (dev ? "dev" : "release"), name + ".apk");
} else {
return getUrl("apk/" + (dev ? "dev" : "release"), name + ".apk"); return getUrl("apk/" + (dev ? "dev" : "release"), name + ".apk");
} }
} }
/**
* 将GitHub Release下载URL转换为jsDelivr CDN URL
* 例如: https://github.com/Tosencen/XMBOX/releases/download/v3.1.0/mobile-arm64_v8a-v3.1.0.apk
* 转换为: https://cdn.jsdelivr.net/gh/Tosencen/XMBOX@v3.1.0/mobile-arm64_v8a-v3.1.0.apk
*
* @param githubUrl GitHub Release下载URL
* @param tagName Release标签名 "v3.1.0"
* @param fileName 文件名
* @return jsDelivr CDN URL
*/
public static String convertToJsDelivrUrl(String githubUrl, String tagName, String fileName) {
try {
// 尝试从GitHub URL中提取信息
// 格式: https://github.com/{owner}/{repo}/releases/download/{tag}/{file}
if (githubUrl.contains("/releases/download/")) {
String[] parts = githubUrl.split("/releases/download/");
if (parts.length == 2) {
String basePath = parts[0]; // https://github.com/Tosencen/XMBOX
String[] baseParts = basePath.split("/");
if (baseParts.length >= 2) {
String owner = baseParts[baseParts.length - 2];
String repo = baseParts[baseParts.length - 1];
// 使用jsDelivr CDN格式
String jsDelivrUrl = "https://cdn.jsdelivr.net/gh/" + owner + "/" + repo + "@" + tagName + "/" + fileName;
Logger.d("Github: URL转换: " + githubUrl + " -> " + jsDelivrUrl);
return jsDelivrUrl;
}
}
}
// 如果无法匹配使用默认仓库信息构建
String jsDelivrUrl = "https://cdn.jsdelivr.net/gh/Tosencen/XMBOX@" + tagName + "/" + fileName;
Logger.d("Github: 使用默认格式构建URL: " + jsDelivrUrl);
return jsDelivrUrl;
} catch (Exception e) {
Logger.e("Github: URL转换失败: " + e.getMessage());
// 转换失败时返回原URL
return githubUrl;
}
}
/**
* 将GitHub raw URL转换为jsDelivr CDN URL
* 例如: https://raw.githubusercontent.com/Tosencen/XMBOX-Release/main/apk/release/mobile-arm64_v8a-v3.1.0.apk
* 转换为: https://cdn.jsdelivr.net/gh/Tosencen/XMBOX-Release@main/apk/release/mobile-arm64_v8a-v3.1.0.apk
*
* @param rawUrl GitHub raw URL
* @return jsDelivr CDN URL如果转换失败则返回原URL
*/
public static String convertRawToJsDelivrUrl(String rawUrl) {
try {
// 格式: https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}
if (rawUrl.contains("raw.githubusercontent.com/")) {
String path = rawUrl.substring(rawUrl.indexOf("raw.githubusercontent.com/") + "raw.githubusercontent.com/".length());
String[] parts = path.split("/", 3);
if (parts.length >= 3) {
String owner = parts[0];
String repo = parts[1];
String filePath = parts[2];
String jsDelivrUrl = "https://cdn.jsdelivr.net/gh/" + owner + "/" + repo + "@main/" + filePath;
Logger.d("Github: Raw URL转换: " + rawUrl + " -> " + jsDelivrUrl);
return jsDelivrUrl;
}
}
// 转换失败时返回原URL
return rawUrl;
} catch (Exception e) {
Logger.e("Github: Raw URL转换失败: " + e.getMessage());
return rawUrl;
}
}
// 智能检测是否使用国内镜像
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; package com.github.catvod.utils;
import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import androidx.preference.PreferenceManager; 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
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
# XMBOX 项目清理脚本
# 清理构建产物和临时文件
echo "开始清理项目..."
# 清理构建目录
echo "清理构建目录..."
./gradlew clean
# 删除系统文件
echo "删除系统文件..."
find . -name ".DS_Store" -type f -delete
find . -name "Thumbs.db" -type f -delete
find . -name "*.swp" -type f -delete
find . -name "*~" -type f -delete
# 删除临时文件
echo "删除临时文件..."
find . -name "*.tmp" -type f -delete
find . -name "*.temp" -type f -delete
# 删除构建缓存
echo "删除构建缓存..."
rm -rf .gradle
rm -rf build
rm -rf app/build
rm -rf quickjs/build
rm -rf catvod/build
rm -rf */build
# 删除IDE配置文件
echo "删除IDE配置文件..."
rm -rf .vscode
rm -rf .vs
rm -rf .idea/workspace.xml
rm -rf .idea/tasks.xml
echo "✅ 清理完成!"
echo "项目已清理,可以提交到Git"
+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 = "XMBOX"
EOF
echo -e "${GREEN}依赖修复完成!${NC}"
echo -e "${YELLOW}现在您可以尝试构建项目:./gradlew clean${NC}"
+5 -1
View File
@@ -6,7 +6,7 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html # http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process. # Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings. # 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. # When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit # 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 # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
@@ -22,3 +22,7 @@ android.enableJetifier=true
android.useFullClasspathForDexingTransform=true android.useFullClasspathForDexingTransform=true
android.nonTransitiveRClass=false android.nonTransitiveRClass=false
android.nonFinalResIds=false android.nonFinalResIds=false
# Library versions
media3Version=1.3.0
okhttpVersion=4.11.0
+1 -1
View File
@@ -1,6 +1,6 @@
#Wed Mar 29 12:54:35 CST 2023 #Wed Mar 29 12:54:35 CST 2023
distributionBase=GRADLE_USER_HOME 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 distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
+1 -1
View File
@@ -499,6 +499,6 @@ public class Hook extends PackageManager {
@Override @Override
public boolean canRequestPackageInstalls() { 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

-75
View File
@@ -1,75 +0,0 @@
{
"lives": [
{
"name": "M3U",
"url": "file://Download/live.m3u"
},
{
"name": "TXT",
"url": "file://Download/live.txt",
"epg": "https://epg.112114.xyz/?ch={name}&date={date}",
"logo": "https://epg.112114.xyz/logo/{name}.png"
},
{
"name": "UA",
"url": "file://Download/live.txt",
"epg": "https://epg.112114.xyz/?ch={name}&date={date}",
"logo": "https://epg.112114.xyz/logo/{name}.png",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
"referer": "https://github.com/"
},
{
"name": "Custom",
"boot": false,
"pass": true,
"url": "file://Download/live.txt",
"epg": "https://epg.112114.xyz/?ch={name}&date={date}&serverTimeZone=Asia/Shanghai",
"logo": "https://epg.112114.xyz/logo/{name}.png",
"header": {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
"Referer": "https://github.com/"
},
"catchup": {
"days": "7",
"type": "append",
"regex": "/PLTV/",
"replace": "/PLTV/,/TVOD/",
"source": "?playseek=${(b)yyyyMMddHHmmss}-${(e)yyyyMMddHHmmss}"
}
},
{
"name": "JSON",
"type": 1,
"url": "file://Download/live.json"
},
{
"name": "Spider-JS",
"type": 3,
"api": "./live.js",
"ext": ""
},
{
"name": "Spider-Python",
"type": 3,
"api": "./live.py",
"ext": ""
}
],
"headers": [
{
"host": "gslbserv.itv.cmvideo.cn",
"header": {
"User-Agent": "okhttp/3.12.13"
}
}
],
"proxy": [
"raw.githubusercontent.com"
],
"hosts": [
"cache.ott.ystenlive.itv.cmvideo.cn=base-v4-free-mghy.e.cdn.chinamobile.com"
],
"ads": [
"static-mozai.4gtv.tv"
]
}
-75
View File
@@ -1,75 +0,0 @@
{
"lives": [
{
"name": "M3U",
"url": "https://github.com/live.m3u"
},
{
"name": "TXT",
"url": "https://github.com/live.txt",
"epg": "https://epg.112114.xyz/?ch={name}&date={date}",
"logo": "https://epg.112114.xyz/logo/{name}.png"
},
{
"name": "UA",
"url": "https://github.com/live.txt",
"epg": "https://epg.112114.xyz/?ch={name}&date={date}",
"logo": "https://epg.112114.xyz/logo/{name}.png",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
"referer": "https://github.com/"
},
{
"name": "Custom",
"boot": false,
"pass": true,
"url": "https://github.com/live.txt",
"epg": "https://epg.112114.xyz/?ch={name}&date={date}&serverTimeZone=Asia/Shanghai",
"logo": "https://epg.112114.xyz/logo/{name}.png",
"header": {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
"Referer": "https://github.com/"
},
"catchup": {
"days": "7",
"type": "append",
"regex": "/PLTV/",
"replace": "/PLTV/,/TVOD/",
"source": "?playseek=${(b)yyyyMMddHHmmss}-${(e)yyyyMMddHHmmss}"
}
},
{
"name": "JSON",
"type": 1,
"url": "https://github.com/live.json"
},
{
"name": "Spider-JS",
"type": 3,
"api": "https://github.com/live.js",
"ext": ""
},
{
"name": "Spider-Python",
"type": 3,
"api": "https://github.com/live.py",
"ext": ""
}
],
"headers": [
{
"host": "gslbserv.itv.cmvideo.cn",
"header": {
"User-Agent": "okhttp/3.12.13"
}
}
],
"proxy": [
"raw.githubusercontent.com"
],
"hosts": [
"cache.ott.ystenlive.itv.cmvideo.cn=base-v4-free-mghy.e.cdn.chinamobile.com"
],
"ads": [
"static-mozai.4gtv.tv"
]
}
-70
View File
@@ -1,70 +0,0 @@
{
"spider": "file://Download/custom_spider.jar",
"sites": [
{
"key": "one",
"name": "One",
"type": 3,
"api": "csp_Csp",
"searchable": 1,
"changeable": 1,
"ext": "file://Download/one.json"
},
{
"key": "two",
"name": "Two",
"type": 3,
"api": "csp_Csp",
"searchable": 1,
"changeable": 1,
"ext": "file://Download/two.json"
},
{
"key": "extend",
"name": "Extend",
"type": 3,
"api": "csp_Csp",
"searchable": 1,
"changeable": 1,
"ext": "file://Download/extend.json",
"jar": "file://Download/extend.jar"
}
],
"parses": [
{
"name": "官方",
"type": 1,
"url": "https://google.com/api/?url="
}
],
"doh": [
{
"name": "Google",
"url": "https://dns.google/dns-query",
"ips": [
"8.8.4.4",
"8.8.8.8"
]
}
],
"headers": [
{
"host": "gslbserv.itv.cmvideo.cn",
"header": {
"User-Agent": "okhttp/3.12.13"
}
}
],
"proxy": [
"raw.githubusercontent.com"
],
"hosts": [
"cache.ott.ystenlive.itv.cmvideo.cn=base-v4-free-mghy.e.cdn.chinamobile.com"
],
"flags": [
"qq"
],
"ads": [
"static-mozai.4gtv.tv"
]
}
-70
View File
@@ -1,70 +0,0 @@
{
"spider": "https://github.com/custom_spider.jar",
"sites": [
{
"key": "one",
"name": "One",
"type": 3,
"api": "csp_Csp",
"searchable": 1,
"changeable": 1,
"ext": "https://github.com/one.json"
},
{
"key": "two",
"name": "Two",
"type": 3,
"api": "csp_Csp",
"searchable": 1,
"changeable": 1,
"ext": "https://github.com/two.json"
},
{
"key": "extend",
"name": "Extend",
"type": 3,
"api": "csp_Csp",
"searchable": 1,
"changeable": 1,
"ext": "https://github.com/extend.json",
"jar": "https://github.com/extend.jar"
}
],
"parses": [
{
"name": "官方",
"type": 1,
"url": "https://google.com/api/?url="
}
],
"doh": [
{
"name": "Google",
"url": "https://dns.google/dns-query",
"ips": [
"8.8.4.4",
"8.8.8.8"
]
}
],
"headers": [
{
"host": "gslbserv.itv.cmvideo.cn",
"header": {
"User-Agent": "okhttp/3.12.13"
}
}
],
"proxy": [
"raw.githubusercontent.com"
],
"hosts": [
"cache.ott.ystenlive.itv.cmvideo.cn=base-v4-free-mghy.e.cdn.chinamobile.com"
],
"flags": [
"qq"
],
"ads": [
"static-mozai.4gtv.tv"
]
}
Binary file not shown.
-7
View File
@@ -1,7 +0,0 @@
git clone --mirror https://github.com/FongMi/Release.git
java -jar bfg.jar --delete-files *.apk Release.git
java -jar bfg.jar --delete-files *.json Release.git
cd Release.git
git reflog expire --expire=now --all && git gc --prune=now --aggressive
git push
git gc
+8 -10
View File
@@ -5,26 +5,24 @@ plugins {
android { android {
namespace 'com.fongmi.android.tv.quickjs' namespace 'com.fongmi.android.tv.quickjs'
compileSdk 35 compileSdk {
version = release(36)
}
defaultConfig { defaultConfig {
minSdk 21 minSdk 24
targetSdk 28 targetSdk 28
} }
lint {
disable 'UnsafeOptInUsageError'
}
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_11 sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_17
} }
} }
dependencies { dependencies {
implementation project(':catvod') implementation project(':catvod')
implementation 'wang.harlon.quickjs:wrapper-java:3.2.0' implementation 'wang.harlon.quickjs:wrapper-java:3.2.3'
implementation 'wang.harlon.quickjs:wrapper-android:3.2.0' implementation 'wang.harlon.quickjs:wrapper-android:3.2.3'
implementation 'net.sourceforge.streamsupport:android-retrofuture:1.7.4' 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 "=== 完成! ==="
+2 -2
View File
@@ -20,6 +20,6 @@ dependencyResolutionManagement {
} }
include ':app' include ':app'
include ':catvod' include ':catvod'
include ':chaquo' // include ':chaquo' // Python支持
include ':quickjs' include ':quickjs'
rootProject.name = "TV" rootProject.name = "XMBOX"