Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 59a4096b37 | |||
| 156cecc848 | |||
| 98da628aee | |||
| e0aee44d5a | |||
| e7e215628b | |||
| a0888f7930 | |||
| 3441bbc8f0 | |||
| 13bc801b12 | |||
| 9f3b631dfb | |||
| 8cfea9ef79 | |||
| 4a8a84a4eb | |||
| 56251db9e7 | |||
| 8357ebefcf | |||
| b16cb4d193 | |||
| df0333d26e | |||
| 0fd0e245d4 | |||
| e95ffad576 | |||
| 78570eed7c | |||
| dcc751c691 | |||
| 7a9dc27835 | |||
| 3407f1f955 | |||
| 54280b68eb | |||
| cf56f091f3 | |||
| 9d6d531ffe | |||
| 0c60ddf63d | |||
| 928a0e9807 | |||
| 93d8c5703b | |||
| 597261ff57 | |||
| d4d30d39c1 | |||
| f49f1cd0b0 | |||
| 8c6275ffe8 | |||
| e094f38423 | |||
| 0734ffc630 | |||
| a8700a8c66 | |||
| ca95128ee9 | |||
| dde56eeedb | |||
| f530ee6407 | |||
| f9ec0334e1 | |||
| 389d548d08 | |||
| fb948dc8c0 | |||
| ce2f46cf5b | |||
| db63949a31 | |||
| f2127ab3a6 | |||
| f525a88668 | |||
| 91a20c8aae | |||
| 59a8c4fd01 | |||
| 3f63cc2416 | |||
| fcdef561ec | |||
| 8d0ae1d5b4 | |||
| afd2d4667d | |||
| ceadb06a64 | |||
| f276fad550 | |||
| b20cf45850 | |||
| 593f1e7444 | |||
| a4d671b394 | |||
| 836e363f94 | |||
| dab1425dea | |||
| 0d7b25710c | |||
| a1a45aeacd | |||
| 6eb7f9139d | |||
| eafc53e8b2 | |||
| 9c7a0fd40e | |||
| b847ff23dd |
@@ -1,8 +1,69 @@
|
||||
.idea
|
||||
.gradle
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Log/OS Files
|
||||
*.log
|
||||
|
||||
# Android Studio generated files and folders
|
||||
captures/
|
||||
.externalNativeBuild/
|
||||
.cxx/
|
||||
*.apk
|
||||
*.aab
|
||||
output.json
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/
|
||||
misc.xml
|
||||
deploymentTargetDropDown.xml
|
||||
render.experimental.xml
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
lib-*.aar
|
||||
*build
|
||||
/media*
|
||||
/Release
|
||||
/local.properties
|
||||
*.keystore
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
google-services.json
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
# APK files
|
||||
apk/release/*.apk
|
||||
|
||||
# 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/
|
||||
@@ -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`
|
||||
@@ -1,276 +1,355 @@
|
||||
# XMBOX
|
||||
<h1 align="center"> 📱 XMBOX - Android视频播放器
|
||||
</h1>
|
||||
<div align="center">
|
||||
|
||||
各模块说明:
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
- app - 主要的应用程序代码
|
||||
- catvod - 视频点播相关功能
|
||||
- forcetech - 强制技术相关功能
|
||||
- hook - 钩子功能
|
||||
- jianpian - 剪片相关功能
|
||||
- quickjs - JavaScript 引擎
|
||||
- thunder - 迅雷下载相关功能
|
||||
- tvbus - TV 总线功能
|
||||
- zlive - 直播相关功能
|
||||
一个操作方便、界面简洁的Android视频播放器盒子,需自行添源,支持TV和手机双平台。
|
||||
|
||||
一个简单的视频播放器应用,支持以下功能:
|
||||
[下载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交互体验全面优化
|
||||
|
||||
| 欄位名稱 | 預設值 | 說明 | 其他 |
|
||||
|------------|------|------|------------|
|
||||
| searchable | 1 | 是否搜索 | 0:關閉;1:啟用 |
|
||||
| changeable | 1 | 是否換源 | 0:關閉;1:啟用 |
|
||||
| quickserch | 1 | 是否快搜 | 0:關閉;1:啟用 |
|
||||
| indexs | 0 | 是否聚搜 | 0:關閉;1:啟用 |
|
||||
| hide | 0 | 是否隱藏 | 0:顯示;1:隱藏 |
|
||||
| timeout | 15 | 播放超時 | 單位:秒 |
|
||||
| header | none | 請求標頭 | 格式:json |
|
||||
| click | none | 點擊js | javascript |
|
||||
### 📦 下载说明
|
||||
- **最新版本**: 根目录的 `mobile.json` 和 `leanback.json` 包含最新版本信息
|
||||
- **历史版本**: 每个版本都有独立的文件夹,包含完整的APK文件和版本信息
|
||||
- **文件结构**: 按版本号组织,便于管理和下载
|
||||
- **签名保护**: 所有APK均使用v1/v2/v3/v4多重签名保护
|
||||
|
||||
### 直播欄位
|
||||
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 | 3:4 |
|
||||
| | 1.33 | 4:3 |
|
||||
### 🔧 快速开始
|
||||
|
||||
直式
|
||||
|
||||
```json
|
||||
{
|
||||
"style": {
|
||||
"type": "rect"
|
||||
}
|
||||
}
|
||||
1. **克隆项目**
|
||||
```bash
|
||||
git clone https://github.com/yourusername/XMBOX.git
|
||||
cd XMBOX
|
||||
```
|
||||
|
||||
橫式
|
||||
|
||||
```json
|
||||
{
|
||||
"style": {
|
||||
"type": "rect",
|
||||
"ratio": 1.33
|
||||
}
|
||||
}
|
||||
2. **配置签名** (可选)
|
||||
```bash
|
||||
# 将你的签名文件放到 keystore/ 目录
|
||||
# 或修改 app/build.gradle 中的签名配置
|
||||
```
|
||||
|
||||
正方
|
||||
3. **构建项目**
|
||||
```bash
|
||||
# 构建所有版本
|
||||
./gradlew assembleRelease
|
||||
|
||||
```json
|
||||
{
|
||||
"style": {
|
||||
"type": "rect",
|
||||
"ratio": 1
|
||||
}
|
||||
}
|
||||
# 构建特定版本
|
||||
./gradlew assembleMobileArm64_v8aRelease # 手机版 ARM64
|
||||
./gradlew assembleLeanbackArm64_v8aRelease # TV版 ARM64
|
||||
./gradlew assembleMobileArmeabi_v7aRelease # 手机版 ARM V7A
|
||||
./gradlew assembleLeanbackArmeabi_v7aRelease # TV版 ARM V7A
|
||||
```
|
||||
|
||||
正圓
|
||||
|
||||
```json
|
||||
{
|
||||
"style": {
|
||||
"type": "oval"
|
||||
}
|
||||
}
|
||||
4. **生成的APK位置**
|
||||
```
|
||||
app/build/outputs/apk/
|
||||
├── mobileArm64_v8a/release/mobile-arm64_v8a.apk
|
||||
├── leanbackArm64_v8a/release/leanback-arm64_v8a.apk
|
||||
├── mobileArmeabi_v7a/release/mobile-armeabi_v7a.apk
|
||||
└── leanbackArmeabi_v7a/release/leanback-armeabi_v7a.apk
|
||||
```
|
||||
|
||||
橢圓
|
||||
## 🏛️ 项目架构
|
||||
|
||||
```json
|
||||
{
|
||||
"style": {
|
||||
"type": "oval",
|
||||
"ratio": 1.1
|
||||
}
|
||||
}
|
||||
### 📂 模块说明
|
||||
```
|
||||
XMBOX/
|
||||
├── app/ # 主应用模块
|
||||
│ ├── src/main/ # 通用代码
|
||||
│ ├── src/mobile/ # 手机版特定代码
|
||||
│ └── src/leanback/ # TV版特定代码
|
||||
├── catvod/ # 视频点播核心
|
||||
├── quickjs/ # JavaScript引擎
|
||||
├── forcetech/ # 强制技术模块
|
||||
├── thunder/ # 迅雷下载模块
|
||||
├── hook/ # 钩子功能
|
||||
├── jianpian/ # 视频剪辑模块
|
||||
├── tvbus/ # TV总线功能
|
||||
└── zlive/ # 直播功能模块
|
||||
```
|
||||
|
||||
### API
|
||||
### 🔧 技术栈
|
||||
- **开发语言**: Java
|
||||
- **UI框架**: Android Views + Material Components
|
||||
- **播放器**: ExoPlayer
|
||||
- **网络库**: OkHttp
|
||||
- **JSON解析**: Gson
|
||||
- **异步处理**: EventBus
|
||||
- **数据库**: Room
|
||||
|
||||
刷新詳情
|
||||
## 📝 更新日志
|
||||
|
||||
```
|
||||
http://127.0.0.1:9978/action?do=refresh&type=detail
|
||||
### v3.1.1 (2025-11-10)
|
||||
|
||||
#### ✨ 新功能
|
||||
* **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!**
|
||||
|
||||
```
|
||||
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)
|
||||
Made with ❤️ by XMBOX Team
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "v3.0.1",
|
||||
"code": 301,
|
||||
"desc": "1. 优化项目结构,重新组织各功能模块\n2. 更新移动端界面,优化用户体验\n3. 强制使用深色模式,提供更好的观看体验"
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "v3.0.1",
|
||||
"code": 301,
|
||||
"desc": "1. 优化项目结构,重新组织各功能模块\n2. 更新移动端界面,优化用户体验\n3. 强制使用深色模式,提供更好的观看体验"
|
||||
}
|
||||
@@ -5,7 +5,7 @@ plugins {
|
||||
android {
|
||||
namespace 'com.fongmi.android.tv'
|
||||
|
||||
compileSdk 35
|
||||
compileSdk 36
|
||||
flavorDimensions = ["mode", "abi"]
|
||||
|
||||
signingConfigs {
|
||||
@@ -14,16 +14,24 @@ android {
|
||||
storePassword "xmbox123"
|
||||
keyAlias "xmbox"
|
||||
keyPassword "xmbox123"
|
||||
// 同时启用v1、v2、v3、v4签名以确保最佳兼容性
|
||||
enableV1Signing true
|
||||
enableV2Signing true
|
||||
enableV3Signing true
|
||||
enableV4Signing true
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.fongmi.android.tv"
|
||||
minSdk 21
|
||||
minSdk 24
|
||||
//noinspection ExpiredTargetSdkVersion
|
||||
targetSdk 28
|
||||
versionCode 302
|
||||
versionName "3.0.2"
|
||||
versionCode 311
|
||||
versionName "3.1.1"
|
||||
// GitHub Token (可选,用于提高API请求限制)
|
||||
def githubToken = project.findProperty("GITHUB_TOKEN") ?: ""
|
||||
buildConfigField "String", "GITHUB_TOKEN", "\"${githubToken}\""
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
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/versions/9/OSGI-INF/MANIFEST.MF'
|
||||
}
|
||||
jniLibs {
|
||||
// 解决重复的JNI库问题,只保留第一个找到的
|
||||
pickFirsts += [
|
||||
'**/libavcodec.so',
|
||||
'**/libavutil.so',
|
||||
'**/libmedia3ext.so',
|
||||
'**/libquickjs-android-wrapper.so',
|
||||
'**/libswresample.so',
|
||||
'**/libswscale.so'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
android.applicationVariants.configureEach { variant ->
|
||||
@@ -87,15 +106,15 @@ android {
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: "libs", include: ["*.aar"])
|
||||
implementation project(':catvod')
|
||||
//implementation project(':chaquo')
|
||||
// implementation project(':chaquo') // 移除Python支持减少8-10MB体积
|
||||
implementation project(':quickjs')
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.media:media:1.7.0'
|
||||
@@ -114,9 +133,9 @@ dependencies {
|
||||
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:' + media3Version
|
||||
implementation 'androidx.media3:media3-extractor:' + media3Version
|
||||
implementation 'androidx.media3:media3-ui:' + media3Version
|
||||
implementation 'androidx.room:room-runtime:2.7.1'
|
||||
implementation 'androidx.room:room-runtime:2.6.1'
|
||||
implementation 'cat.ereza:customactivityoncrash:2.4.0'
|
||||
implementation('com.github.anilbeesetti.nextlib:nextlib-media3ext:0.8.4') { exclude group: 'androidx.media3' }
|
||||
implementation 'io.github.anilbeesetti:nextlib-media3ext:1.8.0-0.9.0'
|
||||
implementation 'com.github.bassaer:materialdesigncolors:1.0.0'
|
||||
implementation 'com.github.bumptech.glide:glide:4.16.0'
|
||||
implementation 'com.github.bumptech.glide:annotations:4.16.0'
|
||||
@@ -124,12 +143,12 @@ dependencies {
|
||||
implementation 'com.github.bumptech.glide:okhttp3-integration:4.16.0'
|
||||
implementation 'com.github.jahirfiquitiva:TextDrawable:1.0.3'
|
||||
implementation 'com.github.thegrizzlylabs:sardine-android:0.9'
|
||||
implementation 'com.github.teamnewpipe:NewPipeExtractor:v0.24.6'
|
||||
implementation 'com.github.teamnewpipe:NewPipeExtractor:v0.24.8'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'com.google.zxing:core:3.5.3'
|
||||
implementation 'com.guolindev.permissionx:permissionx:1.8.0'
|
||||
implementation 'com.hierynomus:smbj:0.14.0'
|
||||
implementation 'io.antmedia:rtmp-client:3.2.0'
|
||||
implementation 'com.guolindev.permissionx:permissionx:1.7.1'
|
||||
implementation 'com.hierynomus:smbj:0.13.0'
|
||||
implementation 'io.antmedia:rtmp-client:3.1.0'
|
||||
implementation 'javax.servlet:javax.servlet-api:3.1.0'
|
||||
implementation 'org.aomedia.avif.android:avif:1.1.1.14d8e3c4'
|
||||
implementation 'org.eclipse.jetty:jetty-client:8.1.21.v20160908'
|
||||
@@ -146,8 +165,10 @@ dependencies {
|
||||
mobileImplementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
mobileImplementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||
mobileImplementation('com.journeyapps:zxing-android-embedded:4.3.0') { transitive = false }
|
||||
annotationProcessor 'androidx.room:room-compiler:2.7.1'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
|
||||
annotationProcessor 'androidx.room:room-compiler:2.6.1'
|
||||
// annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
|
||||
annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.3.1'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.4'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.5'
|
||||
implementation 'io.noties.markwon:core:4.6.2'
|
||||
implementation 'com.airbnb.android:lottie:5.2.0'
|
||||
}
|
||||
@@ -59,12 +59,14 @@
|
||||
-keep class fi.iki.elonen.** { *; }
|
||||
|
||||
# NewPipeExtractor
|
||||
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
||||
-keep class javax.script.** { *; }
|
||||
-keep class jdk.dynalink.** { *; }
|
||||
-keep class org.mozilla.javascript.* { *; }
|
||||
-keep class org.mozilla.javascript.** { *; }
|
||||
-keep class org.mozilla.javascript.engine.** { *; }
|
||||
-keep class javax.script.** { *; }
|
||||
-keep class jdk.dynalink.** { *; }
|
||||
-keep class org.mozilla.classfile.ClassFileWriter
|
||||
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
||||
-keep class org.schabi.newpipe.extractor.services.youtube.protos.** { *; }
|
||||
-dontwarn org.mozilla.javascript.JavaToJSONConverters
|
||||
-dontwarn org.mozilla.javascript.tools.**
|
||||
-dontwarn javax.script.**
|
||||
@@ -90,5 +92,12 @@
|
||||
-keep class com.sun.jna.** { *; }
|
||||
-keep class com.east.android.zlive.** { *; }
|
||||
|
||||
# Media3 DefaultTimeBar - 保护反射访问的字段
|
||||
-keep class androidx.media3.ui.DefaultTimeBar {
|
||||
int barHeight;
|
||||
int scrubberEnabledSize;
|
||||
int scrubberDisabledSize;
|
||||
}
|
||||
|
||||
# Zxing
|
||||
-keep class com.google.zxing.** { *; }
|
||||
@@ -1,21 +1,27 @@
|
||||
package com.fongmi.android.tv;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.fongmi.android.tv.App;
|
||||
import com.fongmi.android.tv.databinding.DialogUpdateBinding;
|
||||
import com.fongmi.android.tv.utils.Download;
|
||||
import com.fongmi.android.tv.utils.FileUtil;
|
||||
import com.fongmi.android.tv.utils.Notify;
|
||||
import com.fongmi.android.tv.utils.ResUtil;
|
||||
import com.fongmi.android.tv.utils.UpdateInstaller;
|
||||
import com.github.catvod.net.OkHttp;
|
||||
import com.github.catvod.utils.Github;
|
||||
import com.github.catvod.utils.Logger;
|
||||
import com.github.catvod.utils.Path;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
@@ -24,20 +30,34 @@ import java.util.Locale;
|
||||
public class Updater implements Download.Callback {
|
||||
|
||||
private DialogUpdateBinding binding;
|
||||
private final Download download;
|
||||
private Download download;
|
||||
private AlertDialog dialog;
|
||||
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() {
|
||||
return Path.cache("update.apk");
|
||||
}
|
||||
|
||||
private String getJson() {
|
||||
return Github.getJson(dev, BuildConfig.FLAVOR_mode);
|
||||
// Android 10+ 无法直接访问外部存储的Download目录
|
||||
// 使用应用的cache目录,FileProvider可以正常访问
|
||||
return Path.cache("XMBOX-update.apk");
|
||||
}
|
||||
|
||||
private String getApk() {
|
||||
return Github.getApk(dev, BuildConfig.FLAVOR_mode + "-" + BuildConfig.FLAVOR_abi);
|
||||
// 使用从 GitHub Release 获取的 APK URL(jsDelivr 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() {
|
||||
@@ -45,12 +65,24 @@ public class Updater implements Download.Callback {
|
||||
}
|
||||
|
||||
public Updater() {
|
||||
this.download = Download.create(getApk(), getFile(), this);
|
||||
this.forceCheck = false;
|
||||
this.autoShow = false;
|
||||
// download对象将在需要时创建
|
||||
}
|
||||
|
||||
public Updater force() {
|
||||
Notify.show(R.string.update_check);
|
||||
Setting.putUpdate(true);
|
||||
this.forceCheck = true; // 标记为手动检查
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置自动检查模式(应用启动时自动检查)
|
||||
*/
|
||||
public Updater auto() {
|
||||
this.forceCheck = false;
|
||||
this.autoShow = true; // 自动显示更新对话框
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -70,6 +102,16 @@ public class Updater implements Download.Callback {
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -78,14 +120,204 @@ public class Updater implements Download.Callback {
|
||||
}
|
||||
|
||||
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 {
|
||||
JSONObject object = new JSONObject(OkHttp.string(getJson()));
|
||||
String name = object.optString("name");
|
||||
String desc = object.optString("desc");
|
||||
int code = object.optInt("code");
|
||||
if (need(code, name)) App.post(() -> show(activity, name, desc));
|
||||
String releasesUrl = "https://api.github.com/repos/Tosencen/XMBOX/releases/latest";
|
||||
Logger.d("Updater: Trying GitHub Releases API: " + releasesUrl);
|
||||
|
||||
// 检查是否有GitHub Token
|
||||
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) {
|
||||
Logger.e("Updater: GitHub API check failed: " + e.getMessage());
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示版本信息弹窗(无更新时)
|
||||
*/
|
||||
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) {
|
||||
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) {
|
||||
Setting.putUpdate(false);
|
||||
if (download != null) {
|
||||
download.cancel();
|
||||
dismiss();
|
||||
}
|
||||
dialog.dismiss();
|
||||
}
|
||||
|
||||
private void confirm(View view) {
|
||||
binding.confirm.setEnabled(false);
|
||||
download.start();
|
||||
// 开始下载更新(使用jsDelivr CDN,失败时回退到GitHub)
|
||||
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() {
|
||||
@@ -133,7 +401,30 @@ public class Updater implements Download.Callback {
|
||||
|
||||
@Override
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package com.fongmi.android.tv.ui.activity;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
@@ -14,22 +18,37 @@ import cat.ereza.customactivityoncrash.CustomActivityOnCrash;
|
||||
public class CrashActivity extends BaseActivity {
|
||||
|
||||
private ActivityCrashBinding mBinding;
|
||||
private String errorDetails;
|
||||
|
||||
@Override
|
||||
protected ViewBinding getBinding() {
|
||||
return mBinding = ActivityCrashBinding.inflate(getLayoutInflater());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initView() {
|
||||
errorDetails = CustomActivityOnCrash.getAllErrorDetailsFromIntent(this, getIntent());
|
||||
mBinding.error.setText(errorDetails);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initEvent() {
|
||||
mBinding.copy.setOnClickListener(v -> showError());
|
||||
mBinding.copy.setOnClickListener(v -> copyErrorToClipboard());
|
||||
mBinding.restart.setOnClickListener(v -> CustomActivityOnCrash.restartApplication(this, Objects.requireNonNull(CustomActivityOnCrash.getConfigFromIntent(getIntent()))));
|
||||
}
|
||||
|
||||
private void copyErrorToClipboard() {
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText(getString(R.string.crash_details_title), errorDetails);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show();
|
||||
showError();
|
||||
}
|
||||
|
||||
private void showError() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.crash_details_title)
|
||||
.setMessage(CustomActivityOnCrash.getAllErrorDetailsFromIntent(this, getIntent()))
|
||||
.setMessage(errorDetails)
|
||||
.setPositiveButton(R.string.crash_details_close, null)
|
||||
.show();
|
||||
}
|
||||
@@ -47,6 +47,7 @@ import com.fongmi.android.tv.ui.base.BaseActivity;
|
||||
import com.fongmi.android.tv.ui.custom.CustomRowPresenter;
|
||||
import com.fongmi.android.tv.ui.custom.CustomSelector;
|
||||
import com.fongmi.android.tv.ui.custom.CustomTitleView;
|
||||
import com.fongmi.android.tv.ui.dialog.LastWatchToast;
|
||||
import com.fongmi.android.tv.ui.dialog.SiteDialog;
|
||||
import com.fongmi.android.tv.ui.presenter.FuncPresenter;
|
||||
import com.fongmi.android.tv.ui.presenter.HeaderPresenter;
|
||||
@@ -244,7 +245,24 @@ public class HomeActivity extends BaseActivity implements CustomTitleView.Listen
|
||||
}
|
||||
|
||||
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 recommendIndex = getRecommendIndex();
|
||||
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.add(historyIndex, new ListRow(mHistoryAdapter));
|
||||
mHistoryAdapter.setItems(items, null);
|
||||
|
||||
// 显示上次播放弹窗
|
||||
checkLastWatchDialog(items);
|
||||
}
|
||||
|
||||
private void checkLastWatchDialog(List<History> items) {
|
||||
if (!items.isEmpty() && App.isAppJustLaunched()) {
|
||||
App.setAppLaunched();
|
||||
LastWatchToast.create(this, items.get(0)).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void setHistoryDelete(boolean delete) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.fongmi.android.tv.ui.activity;
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
|
||||
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.RestoreDialog;
|
||||
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.FileUtil;
|
||||
import com.fongmi.android.tv.utils.Notify;
|
||||
import com.fongmi.android.tv.utils.ResUtil;
|
||||
import com.fongmi.android.tv.utils.UrlUtil;
|
||||
import com.fongmi.android.tv.utils.WebDAVSyncManager;
|
||||
import com.github.catvod.bean.Doh;
|
||||
import com.github.catvod.net.OkHttp;
|
||||
import com.github.catvod.utils.Path;
|
||||
@@ -99,8 +102,40 @@ public class SettingActivity extends BaseActivity implements ConfigCallback, Sit
|
||||
mBinding.dohText.setText(getDohList()[getDohIndex()]);
|
||||
mBinding.proxyText.setText(getProxy(Setting.getProxy()));
|
||||
mBinding.incognitoText.setText(getSwitch(Setting.isIncognito()));
|
||||
mBinding.liveTabVisibleText.setText(getSwitch(Setting.isLiveTabVisible()));
|
||||
mBinding.sizeText.setText((size = ResUtil.getStringArray(R.array.select_size))[Setting.getSize()]);
|
||||
mBinding.qualityText.setText((quality = ResUtil.getStringArray(R.array.select_quality))[Setting.getQuality()]);
|
||||
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() {
|
||||
@@ -134,9 +169,11 @@ public class SettingActivity extends BaseActivity implements ConfigCallback, Sit
|
||||
mBinding.wallDefault.setOnClickListener(this::setWallDefault);
|
||||
mBinding.wallRefresh.setOnClickListener(this::setWallRefresh);
|
||||
mBinding.incognito.setOnClickListener(this::setIncognito);
|
||||
mBinding.liveTabVisible.setOnClickListener(this::setLiveTabVisible);
|
||||
mBinding.quality.setOnClickListener(this::setQuality);
|
||||
mBinding.size.setOnClickListener(this::setSize);
|
||||
mBinding.doh.setOnClickListener(this::setDoh);
|
||||
mBinding.webdav.setOnClickListener(this::onWebDAV);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -304,6 +341,15 @@ public class SettingActivity extends BaseActivity implements ConfigCallback, Sit
|
||||
mBinding.incognitoText.setText(getSwitch(Setting.isIncognito()));
|
||||
}
|
||||
|
||||
private void setLiveTabVisible(View view) {
|
||||
Setting.putLiveTabVisible(!Setting.isLiveTabVisible());
|
||||
mBinding.liveTabVisibleText.setText(getSwitch(Setting.isLiveTabVisible()));
|
||||
// 发送刷新事件,通知主界面更新导航栏
|
||||
RefreshEvent.config();
|
||||
// 更新直播设置项的可见性
|
||||
setLiveSettingsVisibility();
|
||||
}
|
||||
|
||||
private void setQuality(View view) {
|
||||
int index = Setting.getQuality();
|
||||
Setting.putQuality(index = index == quality.length - 1 ? 0 : ++index);
|
||||
@@ -386,12 +432,33 @@ public class SettingActivity extends BaseActivity implements ConfigCallback, Sit
|
||||
}));
|
||||
}
|
||||
|
||||
private void onWebDAV(View view) {
|
||||
WebDAVDialog.create(this).show();
|
||||
}
|
||||
|
||||
private void initConfig() {
|
||||
WallConfig.get().init();
|
||||
LiveConfig.get().init().load();
|
||||
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
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.leanback.widget.ArrayObjectAdapter;
|
||||
import androidx.leanback.widget.ItemBridgeAdapter;
|
||||
@@ -427,8 +428,11 @@ public class VideoActivity extends BaseActivity implements CustomKeyDownVod.List
|
||||
private void setDetail(Result result) {
|
||||
if (result.getList().isEmpty()) setEmpty(result.hasMsg());
|
||||
else setDetail(result.getList().get(0));
|
||||
// 只在有错误或重要消息时显示提示
|
||||
if (result.hasMsg() && result.getList().isEmpty()) {
|
||||
Notify.show(result.getMsg());
|
||||
}
|
||||
}
|
||||
|
||||
private void setEmpty(boolean 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) {
|
||||
view.setText(getSpan(resId, text), TextView.BufferType.SPANNABLE);
|
||||
view.setVisibility(text.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
view.setLinkTextColor(MDColor.YELLOW_500);
|
||||
view.setLinkTextColor(ContextCompat.getColor(view.getContext(), R.color.primary));
|
||||
CustomMovement.bind(view);
|
||||
view.setTag(text);
|
||||
}
|
||||
@@ -1224,7 +1228,10 @@ public class VideoActivity extends BaseActivity implements CustomKeyDownVod.List
|
||||
private void nextSite() {
|
||||
if (mQuickAdapter.size() == 0) return;
|
||||
Vod item = (Vod) mQuickAdapter.get(0);
|
||||
// 只在真正需要切换时显示提示(即当前站点已经失败的情况下)
|
||||
if (mBroken.contains(getId())) {
|
||||
Notify.show(getString(R.string.play_switch_site, item.getSiteName()));
|
||||
}
|
||||
mQuickAdapter.removeItems(0, 1);
|
||||
mBroken.add(getId());
|
||||
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);
|
||||
dialog.getWindow().setAttributes(params);
|
||||
dialog.getWindow().setDimAmount(0);
|
||||
// 设置对话框背景为透明,让布局的深色背景显示
|
||||
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
|
||||
dialog.setOnDismissListener(this);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import android.view.LayoutInflater;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.fongmi.android.tv.R;
|
||||
import com.fongmi.android.tv.databinding.DialogDescBinding;
|
||||
import com.fongmi.android.tv.ui.custom.CustomMovement;
|
||||
import com.github.bassaer.library.MDColor;
|
||||
@@ -21,13 +23,15 @@ public class DescDialog {
|
||||
DialogDescBinding binding = DialogDescBinding.inflate(LayoutInflater.from(activity));
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(activity).setView(binding.getRoot()).create();
|
||||
dialog.getWindow().setDimAmount(0);
|
||||
initView(binding.text, desc);
|
||||
// 设置对话框背景为透明,让布局的深色背景显示
|
||||
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
|
||||
initView(binding.text, desc, activity);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void initView(TextView view, CharSequence desc) {
|
||||
private void initView(TextView view, CharSequence desc, Activity activity) {
|
||||
view.setText(desc, TextView.BufferType.SPANNABLE);
|
||||
view.setLinkTextColor(MDColor.BLUE_500);
|
||||
view.setLinkTextColor(ContextCompat.getColor(activity, R.color.primary));
|
||||
CustomMovement.bind(view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@ public class DohDialog implements DohAdapter.OnClickListener {
|
||||
params.width = (int) (ResUtil.getScreenWidth() * 0.4f);
|
||||
dialog.getWindow().setAttributes(params);
|
||||
dialog.getWindow().setDimAmount(0);
|
||||
// 设置对话框背景为透明,让布局的深色背景显示
|
||||
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,8 @@ public class HistoryDialog implements ConfigAdapter.OnClickListener {
|
||||
params.width = (int) (ResUtil.getScreenWidth() * 0.4f);
|
||||
dialog.getWindow().setAttributes(params);
|
||||
dialog.getWindow().setDimAmount(0);
|
||||
// 设置对话框背景为透明,让布局的深色背景显示
|
||||
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,8 @@ public class LiveDialog implements LiveAdapter.OnClickListener {
|
||||
params.width = (int) (ResUtil.getScreenWidth() * 0.4f);
|
||||
dialog.getWindow().setAttributes(params);
|
||||
dialog.getWindow().setDimAmount(0);
|
||||
// 设置对话框背景为透明,让布局的深色背景显示
|
||||
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,8 @@ public class ProxyDialog implements DialogInterface.OnDismissListener {
|
||||
params.width = (int) (ResUtil.getScreenWidth() * 0.55f);
|
||||
dialog.getWindow().setAttributes(params);
|
||||
dialog.getWindow().setDimAmount(0);
|
||||
// 设置对话框背景为透明,让布局的深色背景显示
|
||||
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
|
||||
dialog.setOnDismissListener(this);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ public class RestoreDialog implements RestoreAdapter.OnClickListener {
|
||||
params.width = (int) (ResUtil.getScreenWidth() * 0.4f);
|
||||
dialog.getWindow().setAttributes(params);
|
||||
dialog.getWindow().setDimAmount(0);
|
||||
// 设置对话框背景为透明,让布局的深色背景显示
|
||||
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ public class SiteDialog implements SiteAdapter.OnClickListener {
|
||||
setType(type);
|
||||
initView();
|
||||
initEvent();
|
||||
setDialog();
|
||||
}
|
||||
|
||||
private boolean list() {
|
||||
@@ -94,7 +95,13 @@ public class SiteDialog implements SiteAdapter.OnClickListener {
|
||||
if (decoration != null) binding.recycler.removeItemDecoration(decoration);
|
||||
binding.recycler.addItemDecoration(decoration = new SpaceItemDecoration(getCount(), 16));
|
||||
binding.recycler.setLayoutManager(new GridLayoutManager(dialog.getContext(), getCount()));
|
||||
if (!binding.mode.hasFocus()) binding.recycler.post(() -> binding.recycler.scrollToPosition(VodConfig.getHomeIndex()));
|
||||
if (!binding.mode.hasFocus()) {
|
||||
binding.recycler.post(() -> {
|
||||
binding.recycler.scrollToPosition(VodConfig.getHomeIndex());
|
||||
// 请求焦点,确保选中项保持高亮状态
|
||||
binding.recycler.post(() -> binding.recycler.requestFocus());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void setDialog() {
|
||||
@@ -103,6 +110,8 @@ public class SiteDialog implements SiteAdapter.OnClickListener {
|
||||
params.width = (int) (ResUtil.getScreenWidth() * getWidth());
|
||||
dialog.getWindow().setAttributes(params);
|
||||
dialog.getWindow().setDimAmount(0);
|
||||
// 设置对话框背景为透明,让布局的深色背景显示
|
||||
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@ public class UaDialog implements DialogInterface.OnDismissListener {
|
||||
params.width = (int) (ResUtil.getScreenWidth() * 0.55f);
|
||||
dialog.getWindow().setAttributes(params);
|
||||
dialog.getWindow().setDimAmount(0);
|
||||
// 设置对话框背景为透明,让布局的深色背景显示
|
||||
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
|
||||
dialog.setOnDismissListener(this);
|
||||
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);
|
||||
dialog.getWindow().setAttributes(params);
|
||||
dialog.getWindow().setDimAmount(0);
|
||||
// 设置对话框背景为透明,让布局的深色背景显示
|
||||
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
|
||||
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>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/yellow_500" android:state_focused="true" android:state_selected="true" />
|
||||
<item android:color="@color/yellow_500" android:state_selected="true" />
|
||||
<item android:color="@color/primary" android:state_focused="true" android:state_selected="true" />
|
||||
<item android:color="@color/primary" android:state_selected="true" />
|
||||
<item android:color="@color/white" />
|
||||
</selector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/primary" android:state_activated="true" />
|
||||
<item android:color="@color/white" />
|
||||
</selector>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/white" android:state_focused="true" android:state_selected="true" />
|
||||
<item android:color="@color/green_400" android:state_selected="true" />
|
||||
<item android:color="@color/primary" android:state_selected="true" />
|
||||
<item android:color="@color/white" />
|
||||
</selector>
|
||||
@@ -2,7 +2,7 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="@color/white_90" />
|
||||
<solid android:color="@color/black_90" />
|
||||
|
||||
<corners
|
||||
android:topLeftRadius="12dp"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="@color/white_90" />
|
||||
<solid android:color="@color/black_80" />
|
||||
|
||||
<corners
|
||||
android:topLeftRadius="8dp"
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="@color/green_400" />
|
||||
<solid android:color="@color/primary" />
|
||||
|
||||
</shape>
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<solid android:color="@color/black_20" />
|
||||
|
||||
<corners android:radius="4dp" />
|
||||
<corners android:radius="12dp" />
|
||||
|
||||
<padding
|
||||
android:bottom="8dp"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="@color/grey_600" />
|
||||
<solid android:color="@color/primary" />
|
||||
|
||||
<corners android:radius="4dp" />
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="@color/grey_800" />
|
||||
<solid android:color="@color/primary" />
|
||||
|
||||
<corners android:radius="4dp" />
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="@color/grey_600" />
|
||||
<solid android:color="@color/black_60" />
|
||||
|
||||
<corners android:radius="4dp" />
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="@color/black_40" />
|
||||
<solid android:color="@color/black_90" />
|
||||
|
||||
<corners android:radius="8dp" />
|
||||
|
||||
|
||||
@@ -235,6 +235,36 @@
|
||||
|
||||
</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
|
||||
android:id="@+id/incognito"
|
||||
android:layout_width="match_parent"
|
||||
@@ -265,6 +295,36 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/liveTabVisible"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/selector_item"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:nextFocusDown="@id/quality"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/setting_live_tab_visible"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/liveTabVisibleText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
tools:text="On" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
android:focusableInTouchMode="true"
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="18sp"
|
||||
tools:text="https://fongmi.github.io/cat.json" />
|
||||
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/text"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
tools:text="愛奇異彈幕" />
|
||||
@@ -9,6 +9,6 @@
|
||||
android:focusableInTouchMode="true"
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/text"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="18sp"
|
||||
tools:text="Google" />
|
||||
@@ -12,6 +12,6 @@
|
||||
android:nextFocusUp="@id/flag"
|
||||
android:nextFocusDown="@id/array"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/text"
|
||||
android:textColor="@color/episode_text"
|
||||
android:textSize="16sp"
|
||||
tools:text="20" />
|
||||
@@ -16,7 +16,7 @@
|
||||
android:focusableInTouchMode="true"
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/text"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="18sp"
|
||||
tools:text="https://fongmi.github.io/live.json" />
|
||||
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/text"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
tools:text="解析" />
|
||||
@@ -24,7 +24,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/green_a_400"
|
||||
android:textColor="@color/primary"
|
||||
android:textSize="14sp"
|
||||
tools:text="泥巴"
|
||||
tools:visibility="visible" />
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
android:focusableInTouchMode="true"
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="18sp"
|
||||
tools:text="tv-2024-12-26.bk.gz" />
|
||||
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
android:ellipsize="marquee"
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/text"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="18sp"
|
||||
android:duplicateParentState="true"
|
||||
tools:text="泥巴" />
|
||||
|
||||
<com.google.android.material.checkbox.MaterialCheckBox
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/text"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
tools:text="中文、哥斯拉.srt" />
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/black_90"
|
||||
android:padding="48dp">
|
||||
|
||||
<com.google.android.material.slider.Slider
|
||||
@@ -12,9 +13,10 @@
|
||||
android:stepSize="1"
|
||||
android:valueFrom="1"
|
||||
android:valueTo="10"
|
||||
app:thumbColor="@color/yellow_500"
|
||||
app:thumbColor="@color/primary"
|
||||
app:thumbRadius="9dp"
|
||||
app:tickVisible="false"
|
||||
app:trackColorActive="@color/yellow_500"
|
||||
app:trackColorInactive="@color/yellow_50" />
|
||||
app:trackColorActive="@color/primary"
|
||||
app:trackColorInactive="@color/white_30" />
|
||||
|
||||
</FrameLayout>
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/black_90"
|
||||
android:padding="16dp">
|
||||
|
||||
<ImageView
|
||||
@@ -23,7 +24,7 @@
|
||||
android:maxLines="3"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:textColor="@color/grey_700"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
tools:text="@string/push_info" />
|
||||
|
||||
@@ -41,11 +42,13 @@
|
||||
android:layout_alignStart="@+id/info"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:hint="@string/dialog_config_hint"
|
||||
android:textColorHint="@color/white_50"
|
||||
android:imeOptions="actionDone"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="text"
|
||||
android:nextFocusDown="@id/positive"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<LinearLayout
|
||||
@@ -68,7 +71,7 @@
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:text="@string/setting_choose"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
@@ -83,7 +86,7 @@
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:text="@string/dialog_positive"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
@@ -97,7 +100,7 @@
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:text="@string/dialog_negative"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/black_90"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
@@ -19,7 +20,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/danmaku_select"
|
||||
android:textColor="@color/grey_900"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<ImageView
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black_90"
|
||||
android:fillViewport="true">
|
||||
|
||||
<TextView
|
||||
@@ -11,7 +12,7 @@
|
||||
android:letterSpacing="0.05"
|
||||
android:lineSpacingExtra="8dp"
|
||||
android:padding="16dp"
|
||||
android:textColor="@color/grey_800"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
@@ -5,6 +5,7 @@
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black_90"
|
||||
android:padding="16dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:maxHeight="296dp"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/black_90"
|
||||
android:padding="16dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:maxHeight="352dp" />
|
||||
@@ -5,6 +5,7 @@
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black_90"
|
||||
android:padding="16dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:maxHeight="296dp"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/black_90"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp">
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/black_90"
|
||||
android:padding="16dp">
|
||||
|
||||
<ImageView
|
||||
@@ -35,11 +36,13 @@
|
||||
android:layout_alignStart="@+id/info"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:hint="socks5://127.0.0.1:9978"
|
||||
android:textColorHint="@color/white_50"
|
||||
android:imeOptions="actionDone"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="text"
|
||||
android:nextFocusDown="@id/positive"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<LinearLayout
|
||||
@@ -62,7 +65,7 @@
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:text="@string/dialog_positive"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
@@ -76,7 +79,7 @@
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:text="@string/dialog_negative"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/black_90"
|
||||
android:padding="16dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:maxHeight="352dp" />
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/black_90"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp">
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/black_90"
|
||||
android:padding="48dp">
|
||||
|
||||
<com.google.android.material.slider.Slider
|
||||
@@ -12,9 +13,12 @@
|
||||
android:stepSize="0.5"
|
||||
android:valueFrom="2"
|
||||
android:valueTo="5"
|
||||
app:thumbColor="@color/yellow_500"
|
||||
app:tickVisible="false"
|
||||
app:trackColorActive="@color/yellow_500"
|
||||
app:trackColorInactive="@color/yellow_50" />
|
||||
app:thumbColor="@color/primary"
|
||||
app:thumbRadius="10dp"
|
||||
app:tickVisible="true"
|
||||
app:tickColor="@color/black_50"
|
||||
app:trackColorActive="@color/primary"
|
||||
app:trackColorInactive="@color/white_20"
|
||||
app:trackHeight="4dp" />
|
||||
|
||||
</FrameLayout>
|
||||
@@ -2,6 +2,7 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/black_90"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp">
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/black_90"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
@@ -18,7 +19,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textColor="@color/grey_900"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
tools:text="選擇字幕" />
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/black_90"
|
||||
android:padding="16dp">
|
||||
|
||||
<ImageView
|
||||
@@ -35,11 +36,13 @@
|
||||
android:layout_alignStart="@+id/info"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:hint="@string/player_ua"
|
||||
android:textColorHint="@color/white_50"
|
||||
android:imeOptions="actionDone"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="text"
|
||||
android:nextFocusDown="@id/positive"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<LinearLayout
|
||||
@@ -62,7 +65,7 @@
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:text="@string/dialog_positive"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
@@ -76,7 +79,7 @@
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:text="@string/dialog_negative"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/black_90"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
@@ -11,7 +12,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:letterSpacing="0.02"
|
||||
android:textColor="@color/grey_900"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
tools:text="@string/update_version" />
|
||||
|
||||
@@ -34,7 +35,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:letterSpacing="0.02"
|
||||
android:lineSpacingExtra="8dp"
|
||||
android:textColor="@color/grey_900"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="16sp"
|
||||
tools:text="1. 新增 ffmpeg 音頻軟解\n2. 詳情頁新增分詞快搜\n3. 修復搜尋閃退問題\n4. 設定支援渲染切換" />
|
||||
|
||||
@@ -56,7 +57,7 @@
|
||||
android:focusableInTouchMode="true"
|
||||
android:gravity="center"
|
||||
android:text="@string/update_confirm"
|
||||
android:textColor="@color/white" />
|
||||
android:textColor="@color/button_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/cancel"
|
||||
@@ -68,7 +69,7 @@
|
||||
android:focusableInTouchMode="true"
|
||||
android:gravity="center"
|
||||
android:text="@string/dialog_negative"
|
||||
android:textColor="@color/white" />
|
||||
android:textColor="@color/button_text" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -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:nextFocusLeft="@id/video"
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
tools:text="刷新" />
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
android:focusableInTouchMode="true"
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
android:text="@string/play_exo"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
@@ -60,7 +60,7 @@
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
tools:text="硬解" />
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
tools:text="1.00" />
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
tools:text="預設" />
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
android:tag="3"
|
||||
android:text="@string/play_track_text"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
@@ -117,7 +117,7 @@
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
android:tag="1"
|
||||
android:text="@string/play_track_audio"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
@@ -134,7 +134,7 @@
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
android:tag="2"
|
||||
android:text="@string/play_track_video"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:nextFocusLeft="@id/change"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
tools:text="首頁" />
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:text="@string/play"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
@@ -51,7 +51,7 @@
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:text="@string/play_exo"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
@@ -62,7 +62,7 @@
|
||||
android:background="@drawable/selector_text"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
tools:text="硬解" />
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
android:background="@drawable/selector_text"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone"
|
||||
tools:text="1.00"
|
||||
@@ -88,7 +88,7 @@
|
||||
android:background="@drawable/selector_text"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
tools:text="預設" />
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
android:background="@drawable/selector_text"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone"
|
||||
tools:text="來源 1"
|
||||
@@ -116,7 +116,7 @@
|
||||
android:focusableInTouchMode="true"
|
||||
android:tag="3"
|
||||
android:text="@string/play_track_text"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
@@ -131,7 +131,7 @@
|
||||
android:focusableInTouchMode="true"
|
||||
android:tag="1"
|
||||
android:text="@string/play_track_audio"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
@@ -146,7 +146,7 @@
|
||||
android:focusableInTouchMode="true"
|
||||
android:tag="2"
|
||||
android:text="@string/play_track_video"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
android:nextFocusUp="@id/next"
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
app:bar_height="2dp"
|
||||
app:scrubber_enabled_size="14dp"
|
||||
app:scrubber_disabled_size="14dp"
|
||||
app:played_color="#FFEB3B"
|
||||
app:scrubber_color="#FFEB3B"
|
||||
app:buffered_color="#80FFEB3B"
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
android:nextFocusLeft="@id/loop"
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
android:text="@string/play_next"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
@@ -58,7 +58,7 @@
|
||||
android:focusableInTouchMode="true"
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
android:text="@string/play_prev"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
@@ -70,7 +70,7 @@
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
tools:text="刷新" />
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
android:focusableInTouchMode="true"
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
android:text="@string/play_change"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
tools:text="換源" />
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
android:focusableInTouchMode="true"
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
android:text="@string/play_exo"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
@@ -110,7 +110,7 @@
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
tools:text="硬解" />
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
tools:text="1.00" />
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
tools:text="預設" />
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
android:tag="3"
|
||||
android:text="@string/play_track_text"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
@@ -167,7 +167,7 @@
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
android:tag="1"
|
||||
android:text="@string/play_track_audio"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
@@ -183,7 +183,7 @@
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
android:tag="2"
|
||||
android:text="@string/play_track_video"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
@@ -197,7 +197,7 @@
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
tools:text="00:00" />
|
||||
|
||||
@@ -210,7 +210,7 @@
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
android:textColor="@color/white"
|
||||
android:textColor="@color/button_text"
|
||||
android:textSize="14sp"
|
||||
tools:text="00:00" />
|
||||
|
||||
|
||||
@@ -1,7 +1,44 @@
|
||||
<resources>
|
||||
|
||||
<color name="primary">@color/black</color>
|
||||
<color name="primaryDark">@color/black</color>
|
||||
<color name="accent">@color/blue_500</color>
|
||||
<color name="primary">#FFEB3B</color>
|
||||
<color name="primaryDark">#FDD835</color>
|
||||
<color name="accent">#FFEB3B</color>
|
||||
<color name="blue_500">#2196F3</color>
|
||||
<color name="green_400">#66BB6A</color>
|
||||
<color name="grey_600">#757575</color>
|
||||
|
||||
<!-- Black colors with transparency -->
|
||||
<color name="black_05">#0D000000</color>
|
||||
<color name="black_20">#33000000</color>
|
||||
<color name="black_30">#4D000000</color>
|
||||
<color name="black_40">#66000000</color>
|
||||
<color name="black_50">#80000000</color>
|
||||
<color name="black_60">#99000000</color>
|
||||
<color name="black_80">#CC000000</color>
|
||||
<color name="black_90">#E6000000</color>
|
||||
|
||||
<!-- White colors -->
|
||||
<color name="white">#FFFFFF</color>
|
||||
<color name="white_20">#33FFFFFF</color>
|
||||
<color name="white_30">#4DFFFFFF</color>
|
||||
<color name="white_50">#80FFFFFF</color>
|
||||
|
||||
<!-- Pure colors -->
|
||||
<color name="black">#000000</color>
|
||||
|
||||
<!-- Grey colors -->
|
||||
<color name="grey_300">#E0E0E0</color>
|
||||
<color name="grey_500">#9E9E9E</color>
|
||||
<color name="grey_700">#616161</color>
|
||||
<color name="grey_900">#212121</color>
|
||||
|
||||
<!-- Text colors -->
|
||||
<color name="text">#FFFFFF</color>
|
||||
|
||||
<!-- UI specific colors -->
|
||||
<color name="green_a_400">#00E676</color>
|
||||
|
||||
<!-- Transparent -->
|
||||
<color name="transparent">#00000000</color>
|
||||
|
||||
</resources>
|
||||
@@ -25,7 +25,7 @@
|
||||
<item name="android:paddingBottom">24dp</item>
|
||||
</style>
|
||||
|
||||
<style name="BottomSheetDialog" parent="Theme.MaterialComponents.Light.BottomSheetDialog">
|
||||
<style name="BottomSheetDialog" parent="Theme.MaterialComponents.DayNight.BottomSheetDialog">
|
||||
<item name="colorPrimary">@color/primary</item>
|
||||
<item name="colorPrimaryDark">@color/primaryDark</item>
|
||||
<item name="colorAccent">@color/accent</item>
|
||||
|
||||
@@ -5469,7 +5469,7 @@ body[data-weui-theme="dark"] .weui-picker__mask {
|
||||
}
|
||||
|
||||
.weui-primary-loading_brand {
|
||||
color: #07c160;
|
||||
color: #FFEB3B;
|
||||
color: var(--weui-BRAND);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.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.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.hook.Hook;
|
||||
import com.github.catvod.Init;
|
||||
@@ -41,13 +45,21 @@ public class App extends Application {
|
||||
private final Gson gson;
|
||||
private final long time;
|
||||
private Hook hook;
|
||||
private final Runnable cleanTask;
|
||||
private final Runnable syncTask;
|
||||
private boolean appJustLaunched;
|
||||
|
||||
public App() {
|
||||
instance = this;
|
||||
executor = Executors.newFixedThreadPool(Constant.THREAD_POOL);
|
||||
handler = HandlerCompat.createAsync(Looper.getMainLooper());
|
||||
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() {
|
||||
@@ -66,6 +78,14 @@ public class App extends Application {
|
||||
return get().activity;
|
||||
}
|
||||
|
||||
public static boolean isAppJustLaunched() {
|
||||
return get().appJustLaunched;
|
||||
}
|
||||
|
||||
public static void setAppLaunched() {
|
||||
get().appJustLaunched = false;
|
||||
}
|
||||
|
||||
public static void execute(Runnable runnable) {
|
||||
get().executor.execute(runnable);
|
||||
}
|
||||
@@ -113,12 +133,18 @@ public class App extends Application {
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
Notify.createChannel();
|
||||
Logger.addLogAdapter(getLogAdapter());
|
||||
OkHttp.get().setProxy(Setting.getProxy());
|
||||
OkHttp.get().setDoh(Doh.objectFrom(Setting.getDoh()));
|
||||
EventBus.builder().addIndex(new EventIndex()).installDefaultEventBus();
|
||||
// EventBus.builder().addIndex(new EventIndex()).installDefaultEventBus(); // 暂时注释,如果EventIndex不存在则删除
|
||||
EventBus.getDefault(); // 使用默认EventBus
|
||||
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() {
|
||||
@Override
|
||||
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
|
||||
@@ -133,6 +159,14 @@ public class App extends Application {
|
||||
@Override
|
||||
public void onActivityResumed(@NonNull Activity activity) {
|
||||
if (activity != activity()) setActivity(activity);
|
||||
// 应用回到前台时检查缓存
|
||||
checkCacheClean();
|
||||
// 检查是否有待安装的更新文件(用户从设置页面返回后)
|
||||
checkPendingInstall();
|
||||
// 检查WebDAV自动同步
|
||||
checkWebDAVSync();
|
||||
// 自动检查更新(如果启用)
|
||||
checkAutoUpdate(activity);
|
||||
}
|
||||
|
||||
@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
|
||||
public PackageManager getPackageManager() {
|
||||
return hook != null ? hook : getBaseContext().getPackageManager();
|
||||
|
||||
@@ -201,6 +201,22 @@ public class Setting {
|
||||
Prefers.put("update", update);
|
||||
}
|
||||
|
||||
public static boolean getAutoUpdateCheck() {
|
||||
return Prefers.getBoolean("auto_update_check", false);
|
||||
}
|
||||
|
||||
public static void putAutoUpdateCheck(boolean autoUpdateCheck) {
|
||||
Prefers.put("auto_update_check", autoUpdateCheck);
|
||||
}
|
||||
|
||||
public static boolean getUseCnMirror() {
|
||||
return Prefers.getBoolean("use_cn_mirror", false);
|
||||
}
|
||||
|
||||
public static void putUseCnMirror(boolean useCnMirror) {
|
||||
Prefers.put("use_cn_mirror", useCnMirror);
|
||||
}
|
||||
|
||||
public static boolean isCaption() {
|
||||
return Prefers.getBoolean("caption");
|
||||
}
|
||||
@@ -300,4 +316,113 @@ public class Setting {
|
||||
public static boolean hasCaption() {
|
||||
return new Intent(Settings.ACTION_CAPTIONING_SETTINGS).resolveActivity(App.get().getPackageManager()) != null;
|
||||
}
|
||||
|
||||
public static boolean isPrivacyAgreed() {
|
||||
return Prefers.getBoolean("privacy_agreed_v1", false);
|
||||
}
|
||||
|
||||
public static void setPrivacyAgreed(boolean agreed) {
|
||||
Prefers.put("privacy_agreed_v1", agreed);
|
||||
}
|
||||
|
||||
public static boolean isLiveTabVisible() {
|
||||
return Prefers.getBoolean("live_tab_visible", true);
|
||||
}
|
||||
|
||||
public static void putLiveTabVisible(boolean visible) {
|
||||
Prefers.put("live_tab_visible", visible);
|
||||
}
|
||||
|
||||
// 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 Live home;
|
||||
|
||||
private LiveConfig() {
|
||||
// 在构造函数中初始化列表,防止空指针异常
|
||||
this.ads = new ArrayList<>();
|
||||
this.rules = new ArrayList<>();
|
||||
this.lives = new ArrayList<>();
|
||||
}
|
||||
|
||||
private static class Loader {
|
||||
static volatile LiveConfig INSTANCE = new LiveConfig();
|
||||
}
|
||||
@@ -97,9 +104,9 @@ public class LiveConfig {
|
||||
|
||||
public LiveConfig clear() {
|
||||
this.home = null;
|
||||
this.ads.clear();
|
||||
this.rules.clear();
|
||||
this.lives.clear();
|
||||
if (this.ads != null) this.ads.clear();
|
||||
if (this.rules != null) this.rules.clear();
|
||||
if (this.lives != null) this.lives.clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,17 @@ public class VodConfig {
|
||||
private Parse parse;
|
||||
private String wall;
|
||||
private Site home;
|
||||
private volatile boolean isLoading = false; // 添加加载状态标记
|
||||
|
||||
private VodConfig() {
|
||||
// 在构造函数中初始化列表,防止空指针异常
|
||||
this.ads = new ArrayList<>();
|
||||
this.doh = new ArrayList<>();
|
||||
this.rules = new ArrayList<>();
|
||||
this.sites = new ArrayList<>();
|
||||
this.flags = new ArrayList<>();
|
||||
this.parses = new ArrayList<>();
|
||||
}
|
||||
|
||||
private static class Loader {
|
||||
static volatile VodConfig INSTANCE = new VodConfig();
|
||||
@@ -67,7 +78,44 @@ public class VodConfig {
|
||||
}
|
||||
|
||||
public static void load(Config config, Callback callback) {
|
||||
get().clear().config(config).load(callback);
|
||||
android.util.Log.d("VodConfig", "load called with config: " + (config != null ? config.toString() : "null"));
|
||||
|
||||
// 参数检查
|
||||
if (config == null || callback == null) {
|
||||
android.util.Log.e("VodConfig", "Invalid parameters: config=" + config + ", callback=" + callback);
|
||||
if (callback != null) {
|
||||
App.post(() -> callback.error("配置参数无效"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
android.util.Log.d("VodConfig", "Parameters valid, proceeding with load");
|
||||
|
||||
// 添加加载状态检查,防止并发加载
|
||||
VodConfig instance = get();
|
||||
synchronized (instance) {
|
||||
if (instance.isLoading) {
|
||||
android.util.Log.d("VodConfig", "Already loading, cancelling previous load");
|
||||
// 如果正在加载,取消之前的加载
|
||||
try {
|
||||
OkHttp.cancel("vod");
|
||||
} catch (Exception e) {
|
||||
android.util.Log.e("VodConfig", "Error cancelling previous load", e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
instance.isLoading = true;
|
||||
}
|
||||
|
||||
android.util.Log.d("VodConfig", "Calling instance.clear().config(config).load(callback)");
|
||||
try {
|
||||
instance.clear().config(config).load(callback);
|
||||
} catch (Exception e) {
|
||||
instance.isLoading = false;
|
||||
android.util.Log.e("VodConfig", "Exception during load", e);
|
||||
e.printStackTrace();
|
||||
App.post(() -> callback.error("配置加载失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
public VodConfig init() {
|
||||
@@ -94,12 +142,12 @@ public class VodConfig {
|
||||
this.wall = null;
|
||||
this.home = null;
|
||||
this.parse = null;
|
||||
this.ads.clear();
|
||||
this.doh.clear();
|
||||
this.rules.clear();
|
||||
this.sites.clear();
|
||||
this.flags.clear();
|
||||
this.parses.clear();
|
||||
if (this.ads != null) this.ads.clear();
|
||||
if (this.doh != null) this.doh.clear();
|
||||
if (this.rules != null) this.rules.clear();
|
||||
if (this.sites != null) this.sites.clear();
|
||||
if (this.flags != null) this.flags.clear();
|
||||
if (this.parses != null) this.parses.clear();
|
||||
this.loadLive = true;
|
||||
BaseLoader.get().clear();
|
||||
return this;
|
||||
@@ -114,15 +162,23 @@ public class VodConfig {
|
||||
OkHttp.cancel("vod");
|
||||
checkJson(Json.parse(Decoder.getJson(UrlUtil.convert(config.getUrl()), "vod")).getAsJsonObject(), callback);
|
||||
} catch (Throwable e) {
|
||||
if (TextUtils.isEmpty(config.getUrl())) App.post(() -> callback.error(""));
|
||||
else loadCache(callback, e);
|
||||
if (TextUtils.isEmpty(config.getUrl())) {
|
||||
isLoading = false;
|
||||
App.post(() -> callback.error(""));
|
||||
} else {
|
||||
loadCache(callback, e);
|
||||
}
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadCache(Callback callback, Throwable e) {
|
||||
if (!TextUtils.isEmpty(config.getJson())) checkJson(Json.parse(config.getJson()).getAsJsonObject(), callback);
|
||||
else App.post(() -> callback.error(Notify.getError(R.string.error_config_get, e)));
|
||||
if (!TextUtils.isEmpty(config.getJson())) {
|
||||
checkJson(Json.parse(config.getJson()).getAsJsonObject(), callback);
|
||||
} else {
|
||||
isLoading = false;
|
||||
App.post(() -> callback.error(Notify.getError(R.string.error_config_get, e)));
|
||||
}
|
||||
}
|
||||
|
||||
private void checkJson(JsonObject object, Callback callback) {
|
||||
@@ -152,11 +208,21 @@ public class VodConfig {
|
||||
if (loadLive && object.has("lives")) initLive(object);
|
||||
String notice = Json.safeString(object, "notice");
|
||||
config.logo(Json.safeString(object, "logo"));
|
||||
App.post(() -> callback.success(notice));
|
||||
config.json(object.toString()).update();
|
||||
|
||||
// 重置加载状态
|
||||
isLoading = false;
|
||||
|
||||
// 只调用一次success回调,优先显示通知消息
|
||||
if (!TextUtils.isEmpty(notice)) {
|
||||
App.post(() -> callback.success(notice));
|
||||
} else {
|
||||
App.post(callback::success);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
// 重置加载状态
|
||||
isLoading = false;
|
||||
App.post(() -> callback.error(Notify.getError(R.string.error_config_parse, e)));
|
||||
}
|
||||
}
|
||||
@@ -167,14 +233,26 @@ public class VodConfig {
|
||||
return;
|
||||
}
|
||||
String spider = Json.safeString(object, "spider");
|
||||
try {
|
||||
BaseLoader.get().parseJar(spider, true);
|
||||
} catch (Throwable e) {
|
||||
android.util.Log.e("VodConfig", "Failed to parse spider jar: " + spider, e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
for (JsonElement element : Json.safeListElement(object, "sites")) {
|
||||
try {
|
||||
Site site = Site.objectFrom(element);
|
||||
if (sites.contains(site)) continue;
|
||||
site.setApi(UrlUtil.convert(site.getApi()));
|
||||
site.setExt(UrlUtil.convert(site.getExt()));
|
||||
site.setJar(parseJar(site, spider));
|
||||
sites.add(site.trans().sync());
|
||||
} catch (Throwable e) {
|
||||
android.util.Log.e("VodConfig", "Failed to add site: " + element, e);
|
||||
e.printStackTrace();
|
||||
// 继续处理下一个站点
|
||||
}
|
||||
}
|
||||
for (Site site : sites) {
|
||||
if (site.getKey().equals(config.getHome())) {
|
||||
@@ -318,10 +396,32 @@ public class VodConfig {
|
||||
}
|
||||
|
||||
public void setHome(Site home) {
|
||||
if (home == null) {
|
||||
// 如果传入null,使用默认站点或创建空站点
|
||||
home = sites.isEmpty() ? new Site() : sites.get(0);
|
||||
}
|
||||
this.home = home;
|
||||
this.home.setActivated(true);
|
||||
|
||||
// 安全地保存配置,防止空指针异常
|
||||
try {
|
||||
if (home.getKey() != null && config != null) {
|
||||
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) {
|
||||
|
||||
@@ -46,10 +46,15 @@ public class JarLoader {
|
||||
}
|
||||
|
||||
private void load(String key, File file) {
|
||||
try {
|
||||
if (!file.setReadOnly()) return;
|
||||
loaders.put(key, dex(file));
|
||||
invokeInit(key);
|
||||
putProxy(key);
|
||||
} catch (Throwable e) {
|
||||
android.util.Log.e("JarLoader", "Failed to load jar for key: " + key, e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private DexClassLoader dex(File file) {
|
||||
@@ -85,6 +90,7 @@ public class JarLoader {
|
||||
}
|
||||
|
||||
public synchronized void parseJar(String key, String jar) {
|
||||
try {
|
||||
if (loaders.containsKey(key)) return;
|
||||
String[] texts = jar.split(";md5;");
|
||||
String md5 = texts.length > 1 ? texts[1].trim() : "";
|
||||
@@ -99,6 +105,10 @@ public class JarLoader {
|
||||
} else if (jar.startsWith("assets")) {
|
||||
parseJar(key, UrlUtil.convert(jar));
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
android.util.Log.e("JarLoader", "Failed to parse jar for key: " + key + ", jar: " + jar, e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public DexClassLoader dex(String jar) {
|
||||
|
||||
@@ -267,6 +267,19 @@ public class Config {
|
||||
Keep.delete(getId());
|
||||
}
|
||||
|
||||
public Config copy() {
|
||||
Config copy = new Config();
|
||||
copy.setType(type);
|
||||
copy.setUrl(url);
|
||||
copy.setJson(json);
|
||||
copy.setName(TextUtils.isEmpty(name) ? url + " 副本" : name + " 副本");
|
||||
copy.setLogo(logo);
|
||||
copy.setHome(home);
|
||||
copy.setParse(parse);
|
||||
copy.setTime(System.currentTimeMillis());
|
||||
return copy;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
@@ -248,6 +248,10 @@ public class History {
|
||||
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) {
|
||||
return AppDatabase.get().getHistoryDao().find(VodConfig.getCid(), key);
|
||||
}
|
||||
@@ -272,8 +276,15 @@ public class History {
|
||||
}
|
||||
|
||||
public void update() {
|
||||
try {
|
||||
com.github.catvod.utils.Logger.d("History.update: 开始更新观看记录 key=" + getKey());
|
||||
merge(find(), false);
|
||||
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) {
|
||||
@@ -287,6 +298,7 @@ public class History {
|
||||
}
|
||||
|
||||
public History save() {
|
||||
com.github.catvod.utils.Logger.d("History.save: key=" + getKey() + ", vodName=" + getVodName());
|
||||
AppDatabase.get().getHistoryDao().insertOrUpdate(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -105,12 +105,37 @@ public class Site implements Parcelable {
|
||||
|
||||
public static Site objectFrom(JsonElement element) {
|
||||
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) {
|
||||
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) {
|
||||
Site site = new Site();
|
||||
site.setKey(key);
|
||||
@@ -133,6 +158,13 @@ public class Site implements Parcelable {
|
||||
|
||||
public void setKey(@NonNull String 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() {
|
||||
@@ -141,6 +173,15 @@ public class Site implements Parcelable {
|
||||
|
||||
public void setName(String 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() {
|
||||
|
||||
@@ -17,7 +17,7 @@ public abstract class ConfigDao extends BaseDao<Config> {
|
||||
@Query("SELECT * FROM Config WHERE type = :type ORDER BY time DESC")
|
||||
public abstract List<Config> findByType(int type);
|
||||
|
||||
@SuppressWarnings(RoomWarnings.QUERY_MISMATCH)
|
||||
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
|
||||
@Query("SELECT id, name, url, type, time FROM Config WHERE type = :type ORDER BY time DESC")
|
||||
public abstract List<Config> findUrlByType(int type);
|
||||
|
||||
|
||||
@@ -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")
|
||||
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")
|
||||
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.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -33,6 +35,7 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
|
||||
private long currentPosition;
|
||||
private long currentBuffered;
|
||||
private boolean scrubbing;
|
||||
private boolean isPressed;
|
||||
|
||||
public CustomSeekView(Context context) {
|
||||
this(context, null);
|
||||
@@ -55,12 +58,72 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
|
||||
timeBar = findViewById(R.id.timeBar);
|
||||
timeBar.addListener(this);
|
||||
refresh = this::refresh;
|
||||
|
||||
// 添加触摸事件处理,实现按住时圆球变大的效果
|
||||
timeBar.setOnTouchListener((v, event) -> {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
if (!isPressed) {
|
||||
isPressed = true;
|
||||
// 按住时:轨道变高到4dp
|
||||
setTimeBarHeight(4);
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
if (isPressed) {
|
||||
isPressed = false;
|
||||
// 松开时:轨道恢复到2dp
|
||||
setTimeBarHeight(2);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return false; // 不拦截事件,让DefaultTimeBar正常处理
|
||||
});
|
||||
}
|
||||
|
||||
public void setListener(Players player) {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
public void setPosition(long position) {
|
||||
timeBar.setPosition(position);
|
||||
}
|
||||
|
||||
public void setDuration(long duration) {
|
||||
timeBar.setDuration(duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态调整进度条高度
|
||||
* @param barHeightDp 轨道高度(dp)
|
||||
*/
|
||||
private void setTimeBarHeight(int barHeightDp) {
|
||||
int barHeightPx = (int) TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
barHeightDp,
|
||||
getContext().getResources().getDisplayMetrics()
|
||||
);
|
||||
|
||||
// 尝试通过反射设置DefaultTimeBar的内部barHeight字段
|
||||
try {
|
||||
java.lang.reflect.Field barHeightField = timeBar.getClass().getDeclaredField("barHeight");
|
||||
barHeightField.setAccessible(true);
|
||||
barHeightField.setInt(timeBar, barHeightPx);
|
||||
|
||||
// 强制刷新
|
||||
timeBar.invalidate();
|
||||
timeBar.requestLayout();
|
||||
} catch (Exception e) {
|
||||
// 如果反射失败,尝试调整布局参数
|
||||
android.util.Log.w("CustomSeekView", "Failed to set bar height via reflection: " + e.getMessage());
|
||||
if (timeBar.getLayoutParams() != null) {
|
||||
timeBar.getLayoutParams().height = barHeightPx;
|
||||
timeBar.requestLayout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void start() {
|
||||
removeCallbacks(refresh);
|
||||
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)) {
|
||||
timeBar.setKeyTimeIncrement(TimeUnit.MINUTES.toMillis(5));
|
||||
} else if (duration > TimeUnit.MINUTES.toMillis(30)) {
|
||||
@@ -124,8 +187,22 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
|
||||
}
|
||||
|
||||
private void seekToTimeBarPosition(long positionMs) {
|
||||
// 先设置播放位置
|
||||
player.seekTo(positionMs);
|
||||
// 延迟刷新进度条,确保播放器已经处理了跳转操作
|
||||
removeCallbacks(refresh);
|
||||
postDelayed(() -> {
|
||||
// 只有在非拖动状态下才刷新进度条位置
|
||||
if (!scrubbing) {
|
||||
refresh();
|
||||
// 确保进度条位置与实际播放位置一致
|
||||
long actualPosition = player.getPosition();
|
||||
if (Math.abs(actualPosition - positionMs) > 100) { // 如果差异超过100ms,再次调整
|
||||
timeBar.setPosition(actualPosition);
|
||||
positionView.setText(player.stringToTime(actualPosition));
|
||||
}
|
||||
}
|
||||
}, 100); // 增加延迟时间,确保拖拽状态完全结束
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -148,6 +225,20 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
|
||||
@Override
|
||||
public void onScrubStop(@NonNull TimeBar timeBar, long position, boolean canceled) {
|
||||
scrubbing = false;
|
||||
if (!canceled) seekToTimeBarPosition(position);
|
||||
|
||||
if (!canceled) {
|
||||
// 立即设置进度条位置到目标位置,避免圆球跳回原始位置
|
||||
timeBar.setPosition(position);
|
||||
positionView.setText(player.stringToTime(position));
|
||||
|
||||
// 调整播放位置
|
||||
seekToTimeBarPosition(position);
|
||||
// 确保播放状态正确
|
||||
if (!player.isPlaying()) {
|
||||
player.play();
|
||||
}
|
||||
}
|
||||
|
||||
// 不干预DefaultTimeBar的圆球绘制,让它自己处理
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import androidx.annotation.NonNull;
|
||||
import com.fongmi.android.tv.App;
|
||||
import com.fongmi.android.tv.Constant;
|
||||
import com.fongmi.android.tv.Setting;
|
||||
import com.fongmi.android.tv.api.AdBlocker;
|
||||
import com.fongmi.android.tv.api.config.LiveConfig;
|
||||
import com.fongmi.android.tv.api.config.VodConfig;
|
||||
import com.fongmi.android.tv.impl.ParseCallback;
|
||||
@@ -177,8 +178,13 @@ public class CustomWebView extends WebView implements DialogInterface.OnDismissL
|
||||
}
|
||||
|
||||
private boolean isAd(String host) {
|
||||
// 1. 首先检查内置广告域名库(包含常见的澳门新葡京等赌博广告)
|
||||
if (AdBlocker.shouldBlock(host)) return true;
|
||||
|
||||
// 2. 然后检查用户自定义的广告域名
|
||||
for (String ad : VodConfig.get().getAds()) if (Util.containOrMatch(host, ad)) return true;
|
||||
for (String ad : LiveConfig.get().getAds()) if (Util.containOrMatch(host, ad)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.widget.RelativeLayout;
|
||||
|
||||
import com.fongmi.android.tv.databinding.ViewEmptyBinding;
|
||||
import com.fongmi.android.tv.databinding.ViewProgressBinding;
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -47,7 +48,8 @@ public class ProgressLayout extends RelativeLayout {
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
mEmptyView = ViewEmptyBinding.inflate(LayoutInflater.from(getContext())).getRoot();
|
||||
// 使用新的Lottie动画空状态布局
|
||||
mEmptyView = LayoutInflater.from(getContext()).inflate(com.fongmi.android.tv.R.layout.view_empty_lottie, null);
|
||||
mEmptyView.setTag(TAG_PROGRESS);
|
||||
mEmptyView.setVisibility(GONE);
|
||||
mProgressView = ViewProgressBinding.inflate(LayoutInflater.from(getContext())).getRoot();
|
||||
@@ -103,21 +105,46 @@ public class ProgressLayout extends RelativeLayout {
|
||||
case CONTENT:
|
||||
mEmptyView.setVisibility(GONE);
|
||||
mProgressView.setVisibility(GONE);
|
||||
pauseLottieAnimation();
|
||||
setContentVisibility(true);
|
||||
break;
|
||||
case PROGRESS:
|
||||
mEmptyView.setVisibility(GONE);
|
||||
mProgressView.setVisibility(VISIBLE);
|
||||
pauseLottieAnimation();
|
||||
setContentVisibility(false);
|
||||
break;
|
||||
case EMPTY:
|
||||
mEmptyView.setVisibility(VISIBLE);
|
||||
mProgressView.setVisibility(GONE);
|
||||
playLottieAnimation();
|
||||
setContentVisibility(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void playLottieAnimation() {
|
||||
try {
|
||||
LottieAnimationView lottieView = mEmptyView.findViewById(com.fongmi.android.tv.R.id.lottieAnimation);
|
||||
if (lottieView != null) {
|
||||
lottieView.playAnimation();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 忽略错误,保持兼容性
|
||||
}
|
||||
}
|
||||
|
||||
private void pauseLottieAnimation() {
|
||||
try {
|
||||
LottieAnimationView lottieView = mEmptyView.findViewById(com.fongmi.android.tv.R.id.lottieAnimation);
|
||||
if (lottieView != null) {
|
||||
lottieView.pauseAnimation();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 忽略错误,保持兼容性
|
||||
}
|
||||
}
|
||||
|
||||
private void setContentVisibility(boolean visible) {
|
||||
for (View view : mContentViews) {
|
||||
if (visible) showView(view);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.fongmi.android.tv.ui.dialog;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.view.LayoutInflater;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.fongmi.android.tv.bean.History;
|
||||
import com.fongmi.android.tv.databinding.DialogLastWatchBinding;
|
||||
import com.fongmi.android.tv.ui.activity.VideoActivity;
|
||||
import com.fongmi.android.tv.utils.ResUtil;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
public class LastWatchDialog {
|
||||
|
||||
private final DialogLastWatchBinding binding;
|
||||
private final AlertDialog dialog;
|
||||
private final Activity activity;
|
||||
private final History history;
|
||||
|
||||
public static LastWatchDialog create(Activity activity, History history) {
|
||||
return new LastWatchDialog(activity, history);
|
||||
}
|
||||
|
||||
private LastWatchDialog(Activity activity, History history) {
|
||||
this.activity = activity;
|
||||
this.history = history;
|
||||
this.binding = DialogLastWatchBinding.inflate(LayoutInflater.from(activity));
|
||||
this.dialog = new MaterialAlertDialogBuilder(activity).setView(binding.getRoot()).create();
|
||||
}
|
||||
|
||||
public void show() {
|
||||
initView();
|
||||
initEvent();
|
||||
dialog.getWindow().setDimAmount(0.5f);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
binding.content.setText(history.getVodName());
|
||||
}
|
||||
|
||||
private void initEvent() {
|
||||
binding.play.setOnClickListener(v -> {
|
||||
dismiss();
|
||||
VideoActivity.start(activity, history.getSiteKey(), history.getVodId(), history.getVodName(), history.getVodPic());
|
||||
});
|
||||
}
|
||||
|
||||
private void dismiss() {
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.fongmi.android.tv.ui.dialog;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AccelerateInterpolator;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.widget.PopupWindow;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.fongmi.android.tv.R;
|
||||
import com.fongmi.android.tv.bean.History;
|
||||
import com.fongmi.android.tv.ui.activity.VideoActivity;
|
||||
|
||||
public class LastWatchToast {
|
||||
|
||||
private final Activity activity;
|
||||
private final History history;
|
||||
private final Handler handler;
|
||||
private PopupWindow popupWindow;
|
||||
private View contentView;
|
||||
private static final int ANIMATION_DURATION = 300; // 动画持续时间(毫秒)
|
||||
private static final int DISPLAY_DURATION = 2500; // 显示持续时间(毫秒)
|
||||
|
||||
public static LastWatchToast create(Activity activity, History history) {
|
||||
return new LastWatchToast(activity, history);
|
||||
}
|
||||
|
||||
private LastWatchToast(Activity activity, History history) {
|
||||
this.activity = activity;
|
||||
this.history = history;
|
||||
this.handler = new Handler(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
public void show() {
|
||||
if (popupWindow != null && popupWindow.isShowing()) {
|
||||
// 如果已经显示,先取消当前显示的,然后重新显示
|
||||
dismiss();
|
||||
}
|
||||
|
||||
contentView = LayoutInflater.from(activity).inflate(R.layout.view_last_watch_toast, null);
|
||||
TextView title = contentView.findViewById(R.id.title);
|
||||
TextView content = contentView.findViewById(R.id.content);
|
||||
|
||||
title.setText(R.string.last_watch);
|
||||
content.setText(history.getVodName());
|
||||
|
||||
// 设置点击事件
|
||||
contentView.setOnClickListener(v -> {
|
||||
dismiss();
|
||||
VideoActivity.start(activity, history.getSiteKey(), history.getVodId(), history.getVodName(), history.getVodPic());
|
||||
});
|
||||
|
||||
// 初始化时设置透明度为0,准备执行淡入动画
|
||||
contentView.setAlpha(0f);
|
||||
|
||||
// 创建PopupWindow
|
||||
popupWindow = new PopupWindow(contentView,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
true);
|
||||
|
||||
// 设置背景为透明,避免PopupWindow有默认背景
|
||||
popupWindow.setBackgroundDrawable(null);
|
||||
popupWindow.setOutsideTouchable(true);
|
||||
|
||||
// 在屏幕中央显示
|
||||
popupWindow.showAtLocation(activity.getWindow().getDecorView(), Gravity.CENTER, 0, 0);
|
||||
|
||||
// 淡入动画
|
||||
animateIn();
|
||||
|
||||
// 一段时间后自动关闭
|
||||
handler.removeCallbacksAndMessages(null);
|
||||
handler.postDelayed(this::animateOut, DISPLAY_DURATION);
|
||||
}
|
||||
|
||||
private void animateIn() {
|
||||
ObjectAnimator fadeIn = ObjectAnimator.ofFloat(contentView, "alpha", 0f, 1f);
|
||||
fadeIn.setDuration(ANIMATION_DURATION);
|
||||
fadeIn.setInterpolator(new DecelerateInterpolator());
|
||||
fadeIn.start();
|
||||
}
|
||||
|
||||
private void animateOut() {
|
||||
if (contentView == null || popupWindow == null || !popupWindow.isShowing()) return;
|
||||
|
||||
ObjectAnimator fadeOut = ObjectAnimator.ofFloat(contentView, "alpha", 1f, 0f);
|
||||
fadeOut.setDuration(ANIMATION_DURATION);
|
||||
fadeOut.setInterpolator(new AccelerateInterpolator());
|
||||
fadeOut.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
fadeOut.start();
|
||||
}
|
||||
|
||||
private void dismiss() {
|
||||
handler.removeCallbacksAndMessages(null);
|
||||
if (popupWindow != null && popupWindow.isShowing()) {
|
||||
popupWindow.dismiss();
|
||||
}
|
||||
popupWindow = null;
|
||||
contentView = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.fongmi.android.tv.utils;
|
||||
|
||||
import android.os.StatFs;
|
||||
|
||||
import com.fongmi.android.tv.App;
|
||||
import com.fongmi.android.tv.impl.Callback;
|
||||
import com.github.catvod.utils.Path;
|
||||
|
||||
/**
|
||||
* 缓存自动清理管理器
|
||||
*/
|
||||
public class CacheCleaner {
|
||||
|
||||
// 默认缓存清理阈值 200MB
|
||||
private static final long DEFAULT_CACHE_THRESHOLD = 200 * 1024 * 1024;
|
||||
// 最小保留空间 500MB
|
||||
private static final long MIN_FREE_SPACE = 500 * 1024 * 1024;
|
||||
// 单例实例
|
||||
private static CacheCleaner instance;
|
||||
// 缓存清理阈值
|
||||
private long cacheThreshold;
|
||||
|
||||
private CacheCleaner() {
|
||||
this.cacheThreshold = DEFAULT_CACHE_THRESHOLD;
|
||||
}
|
||||
|
||||
public static CacheCleaner get() {
|
||||
if (instance == null) {
|
||||
synchronized (CacheCleaner.class) {
|
||||
if (instance == null) {
|
||||
instance = new CacheCleaner();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缓存阈值
|
||||
* @param threshold 阈值大小(字节)
|
||||
*/
|
||||
public void setCacheThreshold(long threshold) {
|
||||
this.cacheThreshold = threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存,如果超过阈值则清理
|
||||
*/
|
||||
public void checkAndClean() {
|
||||
App.execute(() -> {
|
||||
try {
|
||||
// 获取当前缓存大小
|
||||
long cacheSize = FileUtil.getDirectorySize(Path.cache());
|
||||
// 获取剩余存储空间
|
||||
long freeSpace = getAvailableStorageSpace();
|
||||
|
||||
// 如果缓存超过阈值或可用空间低于最小要求,清理缓存
|
||||
if (cacheSize > cacheThreshold || freeSpace < MIN_FREE_SPACE) {
|
||||
cleanCache();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理缓存
|
||||
*/
|
||||
private void cleanCache() {
|
||||
FileUtil.clearCache(new Callback() {
|
||||
@Override
|
||||
public void success() {
|
||||
// 缓存清理成功
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备可用存储空间
|
||||
* @return 可用空间(字节)
|
||||
*/
|
||||
private long getAvailableStorageSpace() {
|
||||
try {
|
||||
StatFs stat = new StatFs(Path.cache().getPath());
|
||||
return stat.getAvailableBlocksLong() * stat.getBlockSizeLong();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,13 @@ package com.fongmi.android.tv.utils;
|
||||
|
||||
import com.fongmi.android.tv.App;
|
||||
import com.github.catvod.net.OkHttp;
|
||||
import com.github.catvod.utils.Logger;
|
||||
import com.github.catvod.utils.Path;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
@@ -16,45 +18,219 @@ public class Download {
|
||||
|
||||
private final File file;
|
||||
private final String url;
|
||||
private final String fallbackUrl;
|
||||
private Callback callback;
|
||||
private static final int MAX_RETRY_COUNT = 3; // 最大重试次数
|
||||
|
||||
public static Download create(String url, File file) {
|
||||
return create(url, file, null);
|
||||
}
|
||||
|
||||
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) {
|
||||
this(url, file, null, callback);
|
||||
}
|
||||
|
||||
public Download(String url, File file, String fallbackUrl, Callback callback) {
|
||||
this.url = url;
|
||||
this.file = file;
|
||||
this.fallbackUrl = fallbackUrl;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (url == null || url.isEmpty()) {
|
||||
if (callback != null) {
|
||||
App.post(() -> callback.error("下载URL为空"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (url.startsWith("file")) return;
|
||||
if (callback == null) doInBackground();
|
||||
else App.execute(this::doInBackground);
|
||||
if (file == null) {
|
||||
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() {
|
||||
OkHttp.cancel(url);
|
||||
if (fallbackUrl != null) {
|
||||
OkHttp.cancel(fallbackUrl);
|
||||
}
|
||||
Path.clear(file);
|
||||
callback = null;
|
||||
}
|
||||
|
||||
private void doInBackground() {
|
||||
try (Response res = OkHttp.newCall(url, url).execute()) {
|
||||
Path.create(file);
|
||||
download(res.body().byteStream(), Double.parseDouble(res.header(HttpHeaders.CONTENT_LENGTH, "1")));
|
||||
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, long length) throws Exception {
|
||||
if (is == null) {
|
||||
throw new Exception("输入流为空,无法下载");
|
||||
}
|
||||
|
||||
private void download(InputStream is, double length) throws Exception {
|
||||
try (BufferedInputStream input = new BufferedInputStream(is); FileOutputStream os = new FileOutputStream(file)) {
|
||||
byte[] buffer = new byte[4096];
|
||||
int readBytes;
|
||||
@@ -62,10 +238,67 @@ public class Download {
|
||||
while ((readBytes = input.read(buffer)) != -1) {
|
||||
totalBytes += 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 {
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.core.content.FileProvider;
|
||||
import com.fongmi.android.tv.App;
|
||||
import com.fongmi.android.tv.R;
|
||||
import com.fongmi.android.tv.impl.Callback;
|
||||
import com.github.catvod.utils.Logger;
|
||||
import com.github.catvod.utils.Path;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
@@ -34,11 +35,35 @@ public class FileUtil {
|
||||
}
|
||||
|
||||
public static void openFile(File file) {
|
||||
try {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intent.setDataAndType(getShareUri(file), FileUtil.getMimeType(file.getName()));
|
||||
|
||||
// 对于APK文件,使用特定的MIME类型
|
||||
String mimeType = file.getName().toLowerCase().endsWith(".apk") ?
|
||||
"application/vnd.android.package-archive" : getMimeType(file.getName());
|
||||
|
||||
intent.setDataAndType(getShareUri(file), mimeType);
|
||||
|
||||
// 添加额外的安装权限检查
|
||||
if (file.getName().toLowerCase().endsWith(".apk")) {
|
||||
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
}
|
||||
|
||||
App.get().startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
Logger.e("Failed to open file: " + e.getMessage());
|
||||
// 如果失败,尝试使用通用方式
|
||||
try {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.setDataAndType(getShareUri(file), "*/*");
|
||||
App.get().startActivity(intent);
|
||||
} catch (Exception ex) {
|
||||
Logger.e("Fallback open file also failed: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void gzipCompress(File target) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.Manifest;
|
||||
import android.app.Notification;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
@@ -40,7 +41,7 @@ public class Notify {
|
||||
|
||||
public static void createChannel() {
|
||||
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) {
|
||||
@@ -49,7 +50,7 @@ public class Notify {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -90,13 +91,20 @@ public class Notify {
|
||||
|
||||
private void makeText(String message) {
|
||||
if (mToast != null) mToast.cancel();
|
||||
if (mHandler == null) mHandler = new Handler(Looper.getMainLooper());
|
||||
if (TextUtils.isEmpty(message)) return;
|
||||
mToast = new Toast(App.get());
|
||||
TextView view = (TextView) LayoutInflater.from(App.get()).inflate(R.layout.view_toast, null);
|
||||
view.setText(message);
|
||||
mToast.setView(view);
|
||||
mToast.setDuration(Toast.LENGTH_LONG);
|
||||
mToast.setDuration(Toast.LENGTH_SHORT);
|
||||
mToast.show();
|
||||
|
||||
// 1秒后取消Toast
|
||||
mHandler.removeCallbacksAndMessages(null);
|
||||
mHandler.postDelayed(() -> {
|
||||
if (mToast != null) mToast.cancel();
|
||||
}, 1000); // 1000毫秒 = 1秒
|
||||
}
|
||||
|
||||
private void makeTextCenter(String message) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件URL(GitHub 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -7,7 +7,7 @@
|
||||
android:topLeftRadius="8dp"
|
||||
android:topRightRadius="8dp" />
|
||||
|
||||
<solid android:color="#B32196F3" />
|
||||
<solid android:color="#B3000000" />
|
||||
|
||||
<padding
|
||||
android:bottom="4dp"
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
android:fitsSystemWindows="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/privacy_agreement_title"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/privacy_agreement_tip"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
android:alpha="0.8" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 协议内容滚动区域 -->
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contentText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/privacy_agreement_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
android:lineSpacingMultiplier="1.4"
|
||||
android:padding="16dp"
|
||||
android:background="@drawable/selector_item_round"
|
||||
android:alpha="0.9" />
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<!-- 按钮区域 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="24dp"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="24dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/disagreeButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:text="@string/privacy_agreement_disagree"
|
||||
android:textColor="@color/white"
|
||||
android:backgroundTint="@color/black_60"
|
||||
android:textSize="13sp"
|
||||
android:maxLines="2"
|
||||
android:gravity="center" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/agreeButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="12dp"
|
||||
android:text="@string/privacy_agreement_agree"
|
||||
android:textColor="@color/black"
|
||||
android:backgroundTint="@color/primary"
|
||||
android:textSize="13sp"
|
||||
android:maxLines="2"
|
||||
android:gravity="center" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/last_watch"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/play"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/play"
|
||||
android:background="@drawable/shape_blue"
|
||||
android:textColor="@color/white" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -19,6 +19,6 @@
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/error_empty"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
android:textSize="14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/lottieAnimation"
|
||||
android:layout_width="180dp"
|
||||
android:layout_height="180dp"
|
||||
app:lottie_fileName="lottie_empty_1.json"
|
||||
app:lottie_loop="true"
|
||||
app:lottie_autoPlay="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/error_keep_empty"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/lottieAnimation"
|
||||
android:layout_width="180dp"
|
||||
android:layout_height="180dp"
|
||||
app:lottie_fileName="lottie_empty_1.json"
|
||||
app:lottie_loop="true"
|
||||
app:lottie_autoPlay="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/error_empty"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
android:alpha="0.8" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/lottieAnimation"
|
||||
android:layout_width="180dp"
|
||||
android:layout_height="180dp"
|
||||
app:lottie_fileName="lottie_empty_1.json"
|
||||
app:lottie_loop="true"
|
||||
app:lottie_autoPlay="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/error_search_empty"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp"
|
||||
android:background="@drawable/bg_toast"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:src="@drawable/ic_notify_play"
|
||||
android:contentDescription="@string/play"
|
||||
android:tint="@color/yellow_500" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:text="@string/last_watch"
|
||||
android:textColor="@color/yellow_500"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/content"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -1,9 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/white" />
|
||||
<!-- 使用纯色背景,自动适配深浅色模式 -->
|
||||
<background android:drawable="@color/launcher_background" />
|
||||
<!-- 前景图标:使用 inset 缩小显示(因为图标铺满了画布)-->
|
||||
<foreground>
|
||||
<inset
|
||||
android:drawable="@mipmap/ic_launcher_foreground"
|
||||
android:inset="20%" />
|
||||
</foreground>
|
||||
<!-- 主题图标:也需要使用 inset 保持大小一致 -->
|
||||
<monochrome>
|
||||
<inset
|
||||
android:drawable="@mipmap/ic_launcher_foreground"
|
||||
android:inset="20%" />
|
||||
</monochrome>
|
||||
</adaptive-icon>
|
||||
@@ -1,9 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/white" />
|
||||
<!-- 使用纯色背景,自动适配深浅色模式 -->
|
||||
<background android:drawable="@color/launcher_background" />
|
||||
<!-- 前景图标:使用 inset 缩小显示(因为图标铺满了画布)-->
|
||||
<foreground>
|
||||
<inset
|
||||
android:drawable="@mipmap/ic_launcher_foreground"
|
||||
android:inset="20%" />
|
||||
</foreground>
|
||||
<!-- 主题图标:也需要使用 inset 保持大小一致 -->
|
||||
<monochrome>
|
||||
<inset
|
||||
android:drawable="@mipmap/ic_launcher_foreground"
|
||||
android:inset="20%" />
|
||||
</monochrome>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 998 B |
|
Before Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 998 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 830 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 830 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 830 B |
|
Before Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 830 B |
|
Before Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,7 @@
|
||||
<resources>
|
||||
|
||||
<!-- Launcher icon background (Dark mode) -->
|
||||
<color name="launcher_background">#222222</color>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -73,11 +73,14 @@
|
||||
<string name="setting_subscription">订阅管理</string>
|
||||
<string name="setting_player">播放设置</string>
|
||||
<string name="setting_incognito">无痕模式</string>
|
||||
<string name="setting_live_tab_visible">隐藏直播</string>
|
||||
<string name="setting_quality">图片品质</string>
|
||||
<string name="setting_size">图片尺寸</string>
|
||||
<string name="setting_doh">DoH</string>
|
||||
<string name="setting_proxy">Proxy</string>
|
||||
<string name="setting_cache">缓存</string>
|
||||
<string name="setting_auto_clean">自动清理缓存</string>
|
||||
<string name="setting_cache_threshold">清理阈值</string>
|
||||
<string name="setting_backup">备份</string>
|
||||
<string name="setting_restore">恢复</string>
|
||||
<string name="setting_version">版本</string>
|
||||
@@ -88,6 +91,8 @@
|
||||
<string name="setting_app">应用设置</string>
|
||||
<string name="setting_network">网络设置</string>
|
||||
<string name="setting_data">数据管理</string>
|
||||
<string name="app_version">v3.1.1</string>
|
||||
<string name="about_github">在GitHub上查看</string>
|
||||
|
||||
<!-- Backup & Restore -->
|
||||
<string name="restore_select">选择备份</string>
|
||||
@@ -121,7 +126,7 @@
|
||||
<string name="dialog_edit">修改</string>
|
||||
<string name="dialog_positive">确定</string>
|
||||
<string name="dialog_negative">取消</string>
|
||||
<string name="dialog_paste">从剪贴板粘贴</string>
|
||||
<string name="dialog_paste">点我粘贴</string>
|
||||
<string name="dialog_config_hint">请输入接口…</string>
|
||||
<string name="dialog_config_name">请输入名称…</string>
|
||||
<string name="dialog_config_url">请输入地址…</string>
|
||||
@@ -143,6 +148,7 @@
|
||||
<string name="error_cast_file">不支持的文件格式</string>
|
||||
<string name="error_device_limit">设备授权数已达上限</string>
|
||||
<string name="error_live_empty">该订阅无直播内容</string>
|
||||
<string name="error_no_live">当前源没有直播内容</string>
|
||||
|
||||
<!-- Update -->
|
||||
<string name="update_version">发现新版本 <xliff:g name="name">%s</xliff:g></string>
|
||||
@@ -166,6 +172,7 @@
|
||||
<string name="none">无</string>
|
||||
<string name="times">倍</string>
|
||||
<string name="lines">行</string>
|
||||
<string name="last_watch">上次播放</string>
|
||||
|
||||
<string-array name="select_decode">
|
||||
<item>软解</item>
|
||||
@@ -209,4 +216,10 @@
|
||||
<item>选择字幕</item>
|
||||
</string-array>
|
||||
|
||||
<string name="source_hint">您还没有添加视频源\n点击下方按钮添加</string>
|
||||
<string name="add_source">添加源</string>
|
||||
<string name="source_hint_setting">添加视频源</string>
|
||||
<string name="source_hint_live">添加直播源</string>
|
||||
<string name="source_hint_wall">添加壁纸源</string>
|
||||
|
||||
</resources>
|
||||
@@ -76,7 +76,9 @@
|
||||
<string name="setting_size">圖片尺寸</string>
|
||||
<string name="setting_doh">DoH</string>
|
||||
<string name="setting_proxy">Proxy</string>
|
||||
<string name="setting_cache">暫存</string>
|
||||
<string name="setting_cache">緩存</string>
|
||||
<string name="setting_auto_clean">自動清理緩存</string>
|
||||
<string name="setting_cache_threshold">清理閾值</string>
|
||||
<string name="setting_backup">備份</string>
|
||||
<string name="setting_restore">還原</string>
|
||||
<string name="setting_version">版本</string>
|
||||
@@ -87,6 +89,8 @@
|
||||
<string name="setting_app">應用設置</string>
|
||||
<string name="setting_network">網絡設置</string>
|
||||
<string name="setting_data">數據管理</string>
|
||||
<string name="app_version">v3.1.1</string>
|
||||
<string name="about_github">在GitHub上查看</string>
|
||||
|
||||
<!-- Backup & Restore -->
|
||||
<string name="restore_select">選擇備份</string>
|
||||
@@ -137,7 +141,11 @@
|
||||
<string name="error_play_flag">暫無線路資料</string>
|
||||
<string name="error_play_timeout">連線逾時</string>
|
||||
<string name="error_detail">暫無播放資料</string>
|
||||
<string name="error_empty">找不到資料</string>
|
||||
<string name="error_empty">這裡撒子內容都沒得~</string>
|
||||
<string name="error_cast_file">不支持的檔案格式</string>
|
||||
<string name="error_device_limit">設備授權數已達上限</string>
|
||||
<string name="error_live_empty">該訂閱無直播內容</string>
|
||||
<string name="error_no_live">當前源沒有直播內容</string>
|
||||
|
||||
<!-- Update -->
|
||||
<string name="update_version">發現新版本 <xliff:g name="name">%s</xliff:g></string>
|
||||
@@ -204,4 +212,8 @@
|
||||
<item>選擇字幕</item>
|
||||
</string-array>
|
||||
|
||||
<string name="config_set_current">已設為當前點播源</string>
|
||||
|
||||
<string name="source_hint">點我添加源</string>
|
||||
|
||||
</resources>
|
||||
@@ -28,4 +28,7 @@
|
||||
<color name="text_toast">#FFEB3B</color>
|
||||
<color name="yellow_500">#FFEB3B</color>
|
||||
|
||||
<!-- Launcher icon background (Light mode) -->
|
||||
<color name="launcher_background">#FFFFFF</color>
|
||||
|
||||
</resources>
|
||||
@@ -65,7 +65,7 @@
|
||||
|
||||
<!-- Push -->
|
||||
<string name="push">Push</string>
|
||||
<string name="push_image" translatable="false">data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCADwAPADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD9U6KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACvN7bxhffFCaRPB+oWZ8KSQX1hd65C7reWd/GwRPKjZdrAHLZPB4OcY3aPxe1j+z/CiWMeo6lo95rN3DpdpqOlWn2ma2mkJ2vt/u/KQT2zXXafafYbGGAyNO6KA8zKqtI38TkKAMsck4AGSafmI42H4S2T7Z7/AFjWNQ1JvDx8OT3rXjI00JOWmKrwJicnzBzzVO78L+JfAdi174a1C88RwaZoK6fZeGb6WNftdyjArPJdPzvK5B6A8dMV6PRRcLGL4a8VWHiaK7S1u7Wa+sJRa6hbWtwJvslwFDPEzDHK7sdB0+orarloPDOo6f47bUtPudPsfD9xbOb7TorBRPdXhYYnaYEHIQbec5/IjqaACiiikMKKimuIrdQZZFjBzjccZwCTj8AT+FcVbfFrTNdl0dPDdpeeI4dYtbq4sdSs4j9g3QEqUln/AOWe5xtBKkZpgd1UX2iIXAg81PPKlxFuG4qCATj0yRz71wVjp/jnxZb2E+sXMPhW0vNImttS0izcS3NvdOSFlhuVPBVeRjPP5i/ovw98PeC5bHXLqV7vWNN0lNJfxDq0+65ktVbdiV+FJLcliM5oEdnRXiev/tWeG21R9G8D6bqPxI11Tg2+gRFreM88yXB+RRx1G6vmb9ov9or4iaes+jar4lsvDmqS5RvDXhKXzbi2BGALq852tkn5Isk4525BNqDZLmkfY/xI+O3gT4Sqn/CUeIrWwuHIC2ke6a4Oe/lICwHuRj3rvq/NT4Xfs1apF8XvhtbeLA41zWJn1++0uYkvZ2MBDJ5+cnfM4K7eq4AJ3MQn6V0pRUbWCMnLcKKKKgsKKKKACiiigAooooAKKKKACiiigAooooA4T4xag2h+G7HW21bVdLtdK1O2u7mPR7X7RLex79n2ZkHOx2dckcjFd0DkA0yaLzoZI9zJvUruQ4YZHUHsa8w03xO3wa06503xXPeDwpo9rB5fjLW75Jpb6eWVgY2RBuypKjOOnX1L3Fsep0VV/tSz3zobuAPAVWVTIAYywyoYdiQRjPrXOeJvil4d8K2eoTz3v219PuILW7ttOQ3M0EkxAjDomSuc55xxSGdbSEgdTiuDvtY8d61caja6Potjof2DVoI0vdYmM0WoWOA0rxLH80b/AMI3gjrz6DfCW11a4d/Eeq3/AIiSHXl17TY7iUxf2fIgxHGhjILIvXaxIJ5INMRcuPiv4eFzp0NjcS62LzVW0Yy6TGbmO3uVXcyzMvCBR1J6d6qaXqHjzxDcaRdzadY+FbW31C5j1HT7qQXct1aqCsLxSJgIWPzEMMjgfXrrPTNP0OK6a0tbXT45pnu7gwxrEHkbl5HwBlj1LHk968s8XftSeDtD1JtG0D7Z468R52jSvDUJumBzj55B8ij15JGDxTWuyFtudP4d+EemaU/h+81W+1DxPrmhSXUljrGq3Ba4j+0Z8xcrtDLtO0BgcAYFTeL/AIjeB/g3o0Z1vVtN8O2aL+5tFwrMOeI4UG49/uivL7yL4y/Ej59Y1bT/AIReH5RkWmnkXurOvXBk+4h6cryOeKs+EPhD4D+H94dQ0/Rn13XmO59e8RSG8u2b+8C3Cn3UCnp1YteiGN8bviB8TPk+GvgWSw0uQceJvGGbW3x03xwD55B1wenHIqnH8ALTxMZNX+K/jW+8di0BuJraSb7BotoB8xJRSAQoH3ieg5Fdf8QPiFo/w98PN4g8caubCw5+z2i8z3bgZ2RRjqTxz0Gckgc14Z8QbzUPHHhgeMvjF9o8E/DS2kX+yfAdk5S+1iT7yC4OQctj7vBABPyY3tUb9NBO3XUrfEv9oaN/CF7pvwrji8DfD2wf7Jc+Kre0ET3UxGPs+nQjaXlIGS/BHUmMYZtf9mL9l218L6pY+N/GGl+Tr87CTR9Bum82TT16i4uWIG+4P3ugCHsrYWLofhf8L72+1XTfiH8QtLt9O1C1jC+FfBMMYW10C36q7RgAed0PIypAJAYARegeOfHLeAfh/wCL/GtzIDPptg7W7Sfda4f5IV+hcov/AAKm5fZiJR+1Iwfgj/xX/wAcvil8QHzJaWlwnhXS3PIEVvhrgqe6tLtYY9TXvtea/s5eA3+HPwX8L6POpW/NqLu8LcsbiYmWQMe5Bfb/AMBFelVnLcuOwUUUVJQUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAVFc2sN5C8NxEk8LjDRyKGVvqDUtFAHGa38HfBviKLXY9Q0OG4XXJoZ9R/eSIbiSH/AFTMVYEFe2MdBXSWeg6bpupahqFpp9tbX+oMjXl1DCqy3BRdqGRgMttXgZ6CvOvHH7SngfwXfHS476XxJ4hJ2ponh+I3l0zehCfKp6feI61yd1q3xn+JibkTTvhB4ek4868K32rSL/sp9yM4zwfmGRV2fUm66HsPjDx54d+H+mtqHiPWrLRrQdJLuYIW9lHVj7AE15G37Q/iX4ifuvhT4Fu9atW4/wCEi8QBrHTR/tKD88o6cDB5qDw78FfA3hfVP7Xu7S68c+JDy+teKJjdPu9URvlUZJxgAj1rubzV7q+ULJKRGOBGnyqPbApaINWec3nwZv8AxlJ5/wAVvHl94rXO4+HNDJstMX/ZbaQ0g9yQeTXd6Db6Z4N0saZ4X0ex8O6eP+WNjCqFvdmxlj7nn3ptWLWza4jlmd0t7WFTJNczMFjiUDJZmPAAAJpXbHZIYqzXk4UB5pXP1JrjfiB8XLPwBrUHhXw9ph8b/Eu8GLXQ7RgYrQkcSXL5wigHcQSOOSVB31zuofFHXfixeXvhz4RSjS/D0BMOs/Ea+iIhhAALpaA43uAcbvcEbRtkq/oz+Av2Z/hze6zEJotMlk/e6tcYfVfEdyckIhODsJz6D7x4+ZjMpKDUXrJ9DSNOVSLmtIrd/p5spTeEdI+C9nJ8WvjZrY8W+OFIFjbp81vaScmO3soTgF85O8gY+98uGdrvgXwLrfivxRbfFT4qW6jxABu8OeE3yYdEiJyskinrOcA8jKkAnDACJvw9+Hus+JPEtt8UvilbKPEIXPh3woR+40OHqrup/wCW3Q8jKkZPzYEfpdxcSXczyyuXkY5JNdlGiqCet5Pd9/8AgHm4rFPFSVo8sF8MVsv82+r6hcXEl3M8srF5GOSTUVFFanIM1bxNB4C8I+JPFlyFaLRdPluVRzgSSBTsT6scL/wIVn/s+2lh8Jfgz4NtfEeow2Ws+IphPI144V7m+uiZRH7vjC4/2a5f47Wp8Taf4A+G0fL+MNbSe/QDOdPtcTTZ9ORGR9DXqXiC4XVPip4Y0O3vtBki0+1m1O90e8t/Mvdn+rt7i3OMRhJAQW9Gx6VfQjqd/RRRUFhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQBDdWsN9byW9zDHcQSDa8UqhlYehB4Irx3xR+yX4D1jUW1bQob3wLr3JXUvC9y1m4J55QfJjPXCgn1r2iimm1sJpPc+eLjQfjr8OU2wXei/F7RY+lvqKDT9SC+iyD92T/tMST6VRs/2iPBf9oJpfjCw1r4Ya2/At/EFowtnP8AsTAEFf8AabaK+la5j4m6VZax8PvENvf2dvfW/wBgnfyrmJZE3CNiDhgRkVV090TZrZngGqfFjXPijql/4S+DBjjtLcmPWPiFeri0tFAyy22fvt2DfUjjEgy9Hk8O/B3TbnTfBHmX2t3m7+1vF18S95fSFizFWOdqluePQHk/Oa2h3c2m/s0/CqytHNta3djNLcQw/KsrB1ILY68sx+pzU3gex0uD+1fEXiLjw5oFq99e5x+82glIxkjLMRwuecY7181mGMrTr/UcPo9rn3uT5XhaWD/tbGvmSu0umjt823t0GSSaJ8O/C7+OvHTudPLH+z9Lzm41SfqFUH+HPU/ieOvRfDr4d6z4i8TW/wAU/inbofEe3Ph7wqw/caJD1R3Q/wDLbocHlTyfnwI/G/CHxK1CP4hWHxa+M/g3W59Gngjfw3eWdv5umaUpJ2v5WeGwAysTu/iCk7GX6o8O+JtD+J1rJqnhXxFZeJYT80gt5MTR56b4zhk+hA+lezhcHDA0+SG73fc+WzDM6uaVvaVNEto9l/XUs3FxJdTPLKxeRjkk1FT5I3hcpIjIw6qwwaZXSecFS20DXVxFEv3pGCj8TUVStrlr4R0XWvEt9/x5aLYzXsgzjdsQnaPc4IHvigDlvAca+PP2nvGWvgb9L8HafD4bsTn5ftD/AL24YD+8vEZ9sV33gu+HiLx54v1OLUdE1Sxs5o9KtvsVvi9s5Ix/pMFxIevz7GCjpnnsa5P9mjSz4G+BFvr/AIglEF7q/wBo8TatcyKRhpsyl2HXiMJnvxXafCFbiTwLZX95f6Vq13qTPfSalo9r9nguw7HZLt6ljGEyx6kVciEdpRRRUFhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFYXjz/kR/EX/YOuP/RTVu1hePP+RH8Rf9g64/8ARTU1uB8p2P8Aybv8H/8AsHT/APoUdRfETQ5vE194J+CNhK0MusSLr3iyeNsG3s0+ZImIOAcLnDD7wiP8VdV8OLewtfgH8LPEGtP5Wg6Bot1qV6/HKoUKoASMszDAHfpWB8IPEln4L8C+LPjj8QIXn13xxdmPTdLQF55rcHEFrCp+YhyoGOR5ccZPFedRwrWNrYlrsl9yPfxOPTyvDYGL01cv/AnZfr9x6H4v+I7/AAysf7SW3mvLnVwNK8J+Crb/AJelUBUdo/4UAIJPYMB1Kiuf8FfsV6fDoZ1nWdXvdE+Il5M17Lqnhmb7JFZuxBEEUajYY1OM8ZPPIGK7r4M/DHWJtcufiT8QVSXxzqkXl29mDui0W0OSttF6Ngnew6knnlifZq7qcfYx5U7vq/M8WvV+sT52rJaJdktvn5nzldt8avhYpTVdMsvjD4bj4FzZKLTV4155MfSQ9OFyT61e8F/GTwF8Rbo2Onay2h68p2yaF4gQ2tyj4GVG7hjz0BNe/wBcZ8RPg74M+K1mbfxR4fs9UYLtS5ZNlxH/ALkq4dfoDitbp7mFmtjGvNNudPbbcQtH6NjIP41518fo5NY8G+FfAFsxS8da3DaTbSQy2ULCWdwRzwFX8GNTyfBX4nfCdd/w38bf8JHoqf8yx4w/ertzkrFcDkdwAQoHcml+F9n4v8AiP8AHCLxZ4w8H3Xg6Hwxop020sZ3WSF7yaQmWWGQcOvlhVyOOcZOKaVtbkt30seofFbVo/C/gCS2s9Y07w3dXTRabp9xqUBmt/NcgLEYwDuDKGXFdhptmun6fa2qrGiwRLEFhQRoAoAwqjhRxwB0p11ZW98ipcwRXCK6yKsqBgGByGGe4PQ1PUFhRRRSGFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAVhePP+RH8Rf9g64/9FNW7WD483HwN4i2RvK/9nXOI413Mx8puAO5NNbgfB/jX4kWV58A/hN8NZdQaw0u40+PVvEl1Dy0NjHI2yMdi7sPlU9XEQ/ir6Q+DPw1vPGOt6f8RfGGljTBZ262/hTwww/d6LZgAK7L/wA92ULk/wAIAHYBfFf2N/2XJtelsfiN47tWeKIRjR9LuoyN4jUIk8in+EBRsB643dMZ+6K1m0tEYwTerCiiisTYKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Z</string>
|
||||
<string name="push_image" translatable="false">data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCADwAPADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD9U6KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACvN7bxhffFCaRPB+oWZ8KSQX1hd65C7reWd/GwRPKjZdrAHLZPB4OcY3aPxe1j+z/CiWMeo6lo95rN3DpdpqOlWn2ma2mkJ2vt/u/KQT2zXXafafYbGGAyNO6KA8zKqtI38TkKAMsck4AGSafmI42H4S2T7Z7/AFjWNQ1JvDx8OT3rXjI00JOWmKrwJicnzBzzVO78L+JfAdi174a1C88RwaZoK6fZeGb6WNftdyjArPJdPzvK5B6A8dMV6PRRcLGL4a8VWHiaK7S1u7Wa+sJRa6hbWtwJvslwFDPEzDHK7sdB0+orarloPDOo6f47bUtPudPsfD9xbOb7TorBRPdXhYYnaYEHIQbec5/IjqaACiiikMKKimuIrdQZZFjBzjccZwCTj8AT+FcVbfFrTNdl0dPDdpeeI4dYtbq4sdSs4j9g3QEqUln/AOWe5xtBKkZpgd1UX2iIXAg81PPKlxFuG4qCATj0yRz71wVjp/jnxZb2E+sXMPhW0vNImttS0izcS3NvdOSFlhuVPBVeRjPP5i/ovw98PeC5bHXLqV7vWNN0lNJfxDq0+65ktVbdiV+FJLcliM5oEdnRXiev/tWeG21R9G8D6bqPxI11Tg2+gRFreM88yXB+RRx1G6vmb9ov9or4iaes+jar4lsvDmqS5RvDXhKXzbi2BGALq852tkn5Isk4525BNqDZLmkfY/xI+O3gT4Sqn/CUeIrWwuHIC2ke6a4Oe/lICwHuRj3rvq/NT4Xfs1apF8XvhtbeLA41zWJn1++0uYkvZ2MBDJ5+cnfM4K7eq4AJ3MQn6V0pRUbWCMnLcKKKKgsKKKKACiiigAooooAKKKKACiiigAooooA4T4xag2h+G7HW21bVdLtdK1O2u7mPR7X7RLex79n2ZkHOx2dckcjFd0DkA0yaLzoZI9zJvUruQ4YZHUHsa8w03xO3wa06503xXPeDwpo9rB5fjLW75Jpb6eWVgY2RBuypKjOOnX1L3Fsep0VV/tSz3zobuAPAVWVTIAYywyoYdiQRjPrXOeJvil4d8K2eoTz3v219PuILW7ttOQ3M0EkxAjDomSuc55xxSGdbSEgdTiuDvtY8d61caja6Potjof2DVoI0vdYmM0WoWOA0rxLH80b/AMI3gjrz6DfCW11a4d/Eeq3/AIiSHXl17TY7iUxf2fIgxHGhjILIvXaxIJ5INMRcuPiv4eFzp0NjcS62LzVW0Yy6TGbmO3uVXcyzMvCBR1J6d6qaXqHjzxDcaRdzadY+FbW31C5j1HT7qQXct1aqCsLxSJgIWPzEMMjgfXrrPTNP0OK6a0tbXT45pnu7gwxrEHkbl5HwBlj1LHk968s8XftSeDtD1JtG0D7Z468R52jSvDUJumBzj55B8ij15JGDxTWuyFtudP4d+EemaU/h+81W+1DxPrmhSXUljrGq3Ba4j+0Z8xcrtDLtO0BgcAYFTeL/AIjeB/g3o0Z1vVtN8O2aL+5tFwrMOeI4UG49/uivL7yL4y/Ej59Y1bT/AIReH5RkWmnkXurOvXBk+4h6cryOeKs+EPhD4D+H94dQ0/Rn13XmO59e8RSG8u2b+8C3Cn3UCnp1YteiGN8bviB8TPk+GvgWSw0uQceJvGGbW3x03xwD55B1wenHIqnH8ALTxMZNX+K/jW+8di0BuJraSb7BotoB8xJRSAQoH3ieg5Fdf8QPiFo/w98PN4g8caubCw5+z2i8z3bgZ2RRjqTxz0Gckgc14Z8QbzUPHHhgeMvjF9o8E/DS2kX+yfAdk5S+1iT7yC4OQctj7vBABPyY3tUb9NBO3XUrfEv9oaN/CF7pvwrji8DfD2wf7Jc+Kre0ET3UxGPs+nQjaXlIGS/BHUmMYZtf9mL9l218L6pY+N/GGl+Tr87CTR9Bum82TT16i4uWIG+4P3ugCHsrYWLofhf8L72+1XTfiH8QtLt9O1C1jC+FfBMMYW10C36q7RgAed0PIypAJAYARegeOfHLeAfh/wCL/GtzIDPptg7W7Sfda4f5IV+hcov/AAKm5fZiJR+1Iwfgj/xX/wAcvil8QHzJaWlwnhXS3PIEVvhrgqe6tLtYY9TXvtea/s5eA3+HPwX8L6POpW/NqLu8LcsbiYmWQMe5Bfb/AMBFelVnLcuOwUUUVJQUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAVFc2sN5C8NxEk8LjDRyKGVvqDUtFAHGa38HfBviKLXY9Q0OG4XXJoZ9R/eSIbiSH/AFTMVYEFe2MdBXSWeg6bpupahqFpp9tbX+oMjXl1DCqy3BRdqGRgMttXgZ6CvOvHH7SngfwXfHS476XxJ4hJ2ponh+I3l0zehCfKp6feI61yd1q3xn+JibkTTvhB4ek4868K32rSL/sp9yM4zwfmGRV2fUm66HsPjDx54d+H+mtqHiPWrLRrQdJLuYIW9lHVj7AE15G37Q/iX4ifuvhT4Fu9atW4/wCEi8QBrHTR/tKD88o6cDB5qDw78FfA3hfVP7Xu7S68c+JDy+teKJjdPu9URvlUZJxgAj1rubzV7q+ULJKRGOBGnyqPbApaINWec3nwZv8AxlJ5/wAVvHl94rXO4+HNDJstMX/ZbaQ0g9yQeTXd6Db6Z4N0saZ4X0ex8O6eP+WNjCqFvdmxlj7nn3ptWLWza4jlmd0t7WFTJNczMFjiUDJZmPAAAJpXbHZIYqzXk4UB5pXP1JrjfiB8XLPwBrUHhXw9ph8b/Eu8GLXQ7RgYrQkcSXL5wigHcQSOOSVB31zuofFHXfixeXvhz4RSjS/D0BMOs/Ea+iIhhAALpaA43uAcbvcEbRtkq/oz+Av2Z/hze6zEJotMlk/e6tcYfVfEdyckIhODsJz6D7x4+ZjMpKDUXrJ9DSNOVSLmtIrd/p5spTeEdI+C9nJ8WvjZrY8W+OFIFjbp81vaScmO3soTgF85O8gY+98uGdrvgXwLrfivxRbfFT4qW6jxABu8OeE3yYdEiJyskinrOcA8jKkAnDACJvw9+Hus+JPEtt8UvilbKPEIXPh3woR+40OHqrup/wCW3Q8jKkZPzYEfpdxcSXczyyuXkY5JNdlGiqCet5Pd9/8AgHm4rFPFSVo8sF8MVsv82+r6hcXEl3M8srF5GOSTUVFFanIM1bxNB4C8I+JPFlyFaLRdPluVRzgSSBTsT6scL/wIVn/s+2lh8Jfgz4NtfEeow2Ws+IphPI144V7m+uiZRH7vjC4/2a5f47Wp8Taf4A+G0fL+MNbSe/QDOdPtcTTZ9ORGR9DXqXiC4XVPip4Y0O3vtBki0+1m1O90e8t/Mvdn+rt7i3OMRhJAQW9Gx6VfQjqd/RRRUFhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFYXjz/kR/EX/YOuP/RTVu1hePP+RH8Rf9g64/9FNW7WD483HwN4i2RvK/9nXOI413Mx8puAO5NNbgfB/jX4kWV58A/hN8NZdQaw0u40+PVvEl1Dy0NjHI2yMdi7sPlU9XEQ/ir6Q+DPw1vPGOt6f8RfGGljTBZ262/hTwww/d6LZgAK7L/wA92ULk/wAIAHYBfFf2N/2XJtelsfiN47tWeKIRjR9LuoyN4jUIk8in+EBRsB643dMZ+6K1m0tEYwTerCiiisTYKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Z</string>
|
||||
|
||||
<!-- Setting -->
|
||||
<string name="setting_vod">Vod</string>
|
||||
@@ -78,17 +78,22 @@
|
||||
<string name="setting_data">Data Management</string>
|
||||
<string name="setting_player">Player setting</string>
|
||||
<string name="setting_incognito">Incognito mode</string>
|
||||
<string name="setting_live_tab_visible">Hide Live Tab</string>
|
||||
<string name="setting_quality">Image quality</string>
|
||||
<string name="setting_size">Image size</string>
|
||||
<string name="setting_doh">DoH</string>
|
||||
<string name="setting_proxy">Proxy</string>
|
||||
<string name="setting_cache">Cache</string>
|
||||
<string name="setting_backup">Backup</string>
|
||||
<string name="setting_restore">Restore</string>
|
||||
<string name="setting_version">Version</string>
|
||||
<string name="setting_cache">缓存</string>
|
||||
<string name="setting_auto_clean">Auto Clean Cache</string>
|
||||
<string name="setting_cache_threshold">Cache Threshold</string>
|
||||
<string name="setting_backup">备份</string>
|
||||
<string name="setting_restore">恢复</string>
|
||||
<string name="setting_version">版本</string>
|
||||
<string name="setting_choose">Choose</string>
|
||||
<string name="setting_off">Off</string>
|
||||
<string name="setting_on">On</string>
|
||||
<string name="app_version">v3.1.1</string>
|
||||
<string name="about_github">View on GitHub</string>
|
||||
|
||||
<!-- Backup & Restore -->
|
||||
<string name="restore_select">Select backup</string>
|
||||
@@ -122,7 +127,7 @@
|
||||
<string name="dialog_edit">Edit</string>
|
||||
<string name="dialog_positive">OK</string>
|
||||
<string name="dialog_negative">Cancel</string>
|
||||
<string name="dialog_paste">Paste from clipboard</string>
|
||||
<string name="dialog_paste">Click to paste</string>
|
||||
<string name="dialog_config_hint">Please enter the config…</string>
|
||||
<string name="dialog_config_name">Please enter the name…</string>
|
||||
<string name="dialog_config_url">Please enter the url…</string>
|
||||
@@ -146,7 +151,10 @@
|
||||
<string name="error_cast_file">Unsupported file format</string>
|
||||
<string name="error_device_limit">Device authorization limit reached</string>
|
||||
<string name="error_live_empty">This subscription has no live content</string>
|
||||
<string name="error_empty">Not found</string>
|
||||
<string name="error_no_live">Current source has no live content</string>
|
||||
<string name="error_empty">空谷待音~</string>
|
||||
<string name="error_keep_empty">老表~没得收藏哈</string>
|
||||
<string name="error_search_empty">搜索无结果,换个关键词试试</string>
|
||||
<string name="error_detail">No play data</string>
|
||||
<string name="error_play_flag">No flag data</string>
|
||||
<string name="error_play_code">Error code: <xliff:g name="name">%s</xliff:g></string>
|
||||
@@ -168,12 +176,14 @@
|
||||
|
||||
<!-- Hint -->
|
||||
<string name="copied">Copied</string>
|
||||
<string name="copied_to_clipboard">Error log copied to clipboard</string>
|
||||
|
||||
<!-- UNIT -->
|
||||
<string name="all">All</string>
|
||||
<string name="none">None</string>
|
||||
<string name="times">times</string>
|
||||
<string name="lines">lines</string>
|
||||
<string name="last_watch">上次播放</string>
|
||||
|
||||
<string-array name="select_decode">
|
||||
<item>Soft</item>
|
||||
@@ -224,4 +234,21 @@
|
||||
|
||||
<string name="config_set_current">已设为当前点播源</string>
|
||||
|
||||
<string name="remember_setting">Remember settings</string>
|
||||
<string name="target_size">Target size</string>
|
||||
<string name="scan_result">Scan result</string>
|
||||
|
||||
<string name="source_hint">空谷无音,待君添源</string>
|
||||
<string name="add_source">添加源</string>
|
||||
|
||||
<!-- 隐私协议相关 -->
|
||||
<string name="privacy_agreement_title">XMBOX软件许可协议</string>
|
||||
<string name="privacy_agreement_tip">请仔细阅读以下协议条款</string>
|
||||
<string name="privacy_agreement_agree">我已阅读并同意</string>
|
||||
<string name="privacy_agreement_disagree">不同意并退出</string>
|
||||
<string name="privacy_agreement_content">XMBOX软件许可协议:\n\n- 以下是对[GPL-3.0](LICENSE.md)开源协议的补充,如有冲突,以以下协议为准。\n- 词语约定: 本协议中的"本软件"指"XMBOX软件","用户"指签署本协议的使用者,"版权数据"指包括但不限于视频、图像、音频、名字等在内的他人拥有所属版权的数据。\n\n1. 本软件仅为技术性多媒体播放器外壳("空壳播放器"),核心功能限于基础媒体文件解析与播放。\n\n2. 本软件自身不包含、不预装、不内置、不集成、不主动推荐、不直接或间接提供任何音视频、直播、图文等媒体资源内容。软件播放的任何资源均非由本软件或其开发者提供。\n\n3. 用户通过本软件播放的任何内容均完全来源于用户自行配置、输入、添加、获取或选择的第三方来源(如网络地址、本地文件、用户安装的插件/扩展/配置源等)。本软件仅作为访问用户自行指定内容的技术工具。\n\n4. 本软件无法控制、筛选、审查或保证用户访问的任何第三方内容的合法性、版权状态、准确性、安全性或适宜性。用户对其播放的内容负全部责任。\n\n5. 关于用户责任与风险承担:\n • 用户必须确保其通过本软件配置、访问或播放的所有内容均已获相关权利人合法授权,或属于法律允许的自由使用范畴。\n • 用户理解并同意,使用本软件访问第三方资源可能涉及侵犯版权、传播非法信息、隐私泄露、网络安全等风险。因用户使用本软件访问、播放或传播内容产生的一切法律责任、纠纷、损失及后果(包括法律诉讼、行政处罚、民事赔偿等),均由用户自行承担,与本软件及其开发者无涉。\n • 开发者不认可、不支持任何利用本软件规避技术保护措施(如DRM)的行为,此类行为导致的侵权责任由用户全权承担。\n\n6. 用户承诺并保证不利用本软件从事任何侵犯他人知识产权或其他合法权益的活动,或进行任何违反法律法规的行为。严禁使用本软件播放、传播盗版、色情、暴力、赌博、诈骗、危害国家安全、危害社会稳定等非法或侵权内容。\n\n7. 在任何情况下,本软件开发者均不就因用户使用或无法使用本软件、用户配置或访问的第三方资源、用户违反本协议或法律法规的行为导致的任何直接、间接、偶然、特殊、惩罚性或结果性损害(包括利润损失、数据丢失、业务中断、声誉损害等)承担任何责任(无论基于合同、侵权、严格责任或其他法律理论)。\n\n8. 本软件运行可能依赖第三方库、服务或技术。开发者不对这些第三方组件的可用性、准确性、功能或合法性负责。\n\n9. 用户理解并同意,使用本软件(包括下载、安装、运行)存在固有技术风险(如软件缺陷、兼容性问题、系统不稳定等),用户应自行承担此风险。\n\n10. 本软件仅用于对技术可行性的探索及研究,不接受任何商业(包括但不限于广告等)合作及捐赠。\n\n11. 本软件内使用的部分包括但不限于字体、图片等资源来源于互联网。如果出现侵权可联系开发者移除。\n\n12. 使用本软件的过程中可能会产生版权数据。对于这些版权数据,本软件不拥有它们的所有权。为了避免侵权,用户务必在 24 小时内 清除使用本项目的过程中所产生的版权数据。\n\n13. 本协议受中华人民共和国法律管辖并据其解释。若用户所在地法律强制规定特定责任条款,应以当地法律要求为准,但其他条款仍保持有效。任何由本协议或使用本软件引起的争议,应首先通过友好协商解决。\n\n14. 若你使用了本软件,即代表你接受本协议。\n\n15. 本协议更新后,继续使用视为接受新协议。</string>
|
||||
<string name="source_hint_setting">Add Source</string>
|
||||
<string name="source_hint_live">Add Live Source</string>
|
||||
<string name="source_hint_wall">Add Wallpaper Source</string>
|
||||
|
||||
</resources>
|
||||
@@ -10,6 +10,13 @@
|
||||
|
||||
<application>
|
||||
|
||||
<activity
|
||||
android:name=".ui.activity.PrivacyAgreementActivity"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode|orientation"
|
||||
android:exported="false"
|
||||
android:screenOrientation="fullUser"
|
||||
android:windowSoftInputMode="adjustPan" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.activity.HomeActivity"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode|orientation"
|
||||
@@ -106,6 +113,11 @@
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode"
|
||||
android:screenOrientation="fullUser" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.activity.SettingPlayerActivity"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode"
|
||||
android:screenOrientation="fullUser" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.activity.VideoActivity"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode|orientation"
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.fongmi.android.tv;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
||||
@@ -12,11 +14,14 @@ import com.fongmi.android.tv.utils.Download;
|
||||
import com.fongmi.android.tv.utils.FileUtil;
|
||||
import com.fongmi.android.tv.utils.Notify;
|
||||
import com.fongmi.android.tv.utils.ResUtil;
|
||||
import com.fongmi.android.tv.utils.UpdateInstaller;
|
||||
import com.github.catvod.net.OkHttp;
|
||||
import com.github.catvod.utils.Github;
|
||||
import com.github.catvod.utils.Logger;
|
||||
import com.github.catvod.utils.Path;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
@@ -25,20 +30,34 @@ import java.util.Locale;
|
||||
public class Updater implements Download.Callback {
|
||||
|
||||
private DialogUpdateBinding binding;
|
||||
private final Download download;
|
||||
private Download download;
|
||||
private AlertDialog dialog;
|
||||
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() {
|
||||
return Path.cache("update.apk");
|
||||
}
|
||||
|
||||
private String getJson() {
|
||||
return Github.getJson(dev, BuildConfig.FLAVOR_mode);
|
||||
// Android 10+ 无法直接访问外部存储的Download目录
|
||||
// 使用应用的cache目录,FileProvider可以正常访问
|
||||
return Path.cache("XMBOX-update.apk");
|
||||
}
|
||||
|
||||
private String getApk() {
|
||||
return Github.getApk(dev, BuildConfig.FLAVOR_mode + "-" + BuildConfig.FLAVOR_abi);
|
||||
// 使用从 GitHub Release 获取的 APK URL(jsDelivr 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() {
|
||||
@@ -46,12 +65,24 @@ public class Updater implements Download.Callback {
|
||||
}
|
||||
|
||||
public Updater() {
|
||||
this.download = Download.create(getApk(), getFile(), this);
|
||||
this.forceCheck = false;
|
||||
this.autoShow = false;
|
||||
// download对象将在需要时创建
|
||||
}
|
||||
|
||||
public Updater force() {
|
||||
Notify.show(R.string.update_check);
|
||||
Setting.putUpdate(true);
|
||||
this.forceCheck = true; // 标记为手动检查
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置自动检查模式(应用启动时自动检查)
|
||||
*/
|
||||
public Updater auto() {
|
||||
this.forceCheck = false;
|
||||
this.autoShow = true; // 自动显示更新对话框
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -71,6 +102,16 @@ public class Updater implements Download.Callback {
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -79,14 +120,205 @@ public class Updater implements Download.Callback {
|
||||
}
|
||||
|
||||
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 {
|
||||
JSONObject object = new JSONObject(OkHttp.string(getJson()));
|
||||
String name = object.optString("name");
|
||||
String desc = object.optString("desc");
|
||||
int code = object.optInt("code");
|
||||
if (need(code, name)) App.post(() -> show(activity, name, desc));
|
||||
String releasesUrl = "https://api.github.com/repos/Tosencen/XMBOX/releases/latest";
|
||||
Logger.d("Updater: Trying GitHub Releases API: " + releasesUrl);
|
||||
|
||||
// 检查是否有GitHub Token
|
||||
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) {
|
||||
Logger.e("Updater: GitHub API check failed: " + e.getMessage());
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示版本信息弹窗(无更新时)
|
||||
*/
|
||||
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) {
|
||||
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) {
|
||||
Setting.putUpdate(false);
|
||||
if (download != null) {
|
||||
download.cancel();
|
||||
}
|
||||
dialog.dismiss();
|
||||
}
|
||||
|
||||
private void confirm(View view) {
|
||||
view.setEnabled(false);
|
||||
download.start();
|
||||
// 开始下载更新(使用jsDelivr CDN,失败时回退到GitHub)
|
||||
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() {
|
||||
@@ -133,7 +397,30 @@ public class Updater implements Download.Callback {
|
||||
|
||||
@Override
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import com.fongmi.android.tv.utils.Util;
|
||||
import com.github.catvod.net.OkHttp;
|
||||
import com.google.android.flexbox.FlexDirection;
|
||||
import com.google.android.flexbox.FlexboxLayoutManager;
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
@@ -151,12 +152,14 @@ public class CollectActivity extends BaseActivity implements CustomScroller.Call
|
||||
if (mCollectAdapter.getPosition() == 0) mSearchAdapter.addAll(result.getList());
|
||||
mCollectAdapter.add(Collect.create(result.getList()));
|
||||
mCollectAdapter.add(result.getList());
|
||||
updateEmptyState();
|
||||
});
|
||||
mViewModel.result.observe(this, result -> {
|
||||
boolean same = !result.getList().isEmpty() && mCollectAdapter.getActivated().getSite().equals(result.getList().get(0).getSite());
|
||||
if (same) mCollectAdapter.getActivated().getList().addAll(result.getList());
|
||||
if (same) mSearchAdapter.addAll(result.getList());
|
||||
mScroller.endLoading(result);
|
||||
updateEmptyState();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -187,6 +190,7 @@ public class CollectActivity extends BaseActivity implements CustomScroller.Call
|
||||
mBinding.agent.setVisibility(View.GONE);
|
||||
mBinding.view.setVisibility(View.VISIBLE);
|
||||
mBinding.result.setVisibility(View.VISIBLE);
|
||||
updateEmptyState(); // 搜索开始时显示空状态
|
||||
if (mExecutor != null) mExecutor.shutdownNow();
|
||||
mExecutor = new PauseExecutor(20);
|
||||
String keyword = mBinding.keyword.getText().toString().trim();
|
||||
@@ -194,6 +198,27 @@ public class CollectActivity extends BaseActivity implements CustomScroller.Call
|
||||
App.post(() -> mRecordAdapter.add(keyword), 250);
|
||||
}
|
||||
|
||||
private void updateEmptyState() {
|
||||
// 只有在结果页面可见且搜索结果为空时才显示空状态动画
|
||||
boolean isResultVisible = isVisible(mBinding.result);
|
||||
boolean isEmpty = mSearchAdapter.getItemCount() == 0;
|
||||
boolean shouldShowEmpty = isResultVisible && isEmpty;
|
||||
|
||||
mBinding.emptyLayout.getRoot().setVisibility(shouldShowEmpty ? View.VISIBLE : View.GONE);
|
||||
|
||||
// 控制Lottie动画播放
|
||||
if (shouldShowEmpty) {
|
||||
try {
|
||||
LottieAnimationView lottieView = mBinding.emptyLayout.getRoot().findViewById(R.id.lottieAnimation);
|
||||
if (lottieView != null) {
|
||||
lottieView.playAnimation();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void search(Site site, String keyword) {
|
||||
try {
|
||||
mViewModel.searchContent(site, keyword, false);
|
||||
@@ -235,6 +260,7 @@ public class CollectActivity extends BaseActivity implements CustomScroller.Call
|
||||
mBinding.result.setVisibility(View.GONE);
|
||||
mBinding.site.setVisibility(View.VISIBLE);
|
||||
mBinding.agent.setVisibility(View.VISIBLE);
|
||||
mBinding.emptyLayout.getRoot().setVisibility(View.GONE); // 隐藏空状态动画
|
||||
if (mExecutor != null) mExecutor.shutdownNow();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.fongmi.android.tv.ui.activity;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import com.fongmi.android.tv.R;
|
||||
import com.fongmi.android.tv.databinding.ActivityCrashBinding;
|
||||
import com.fongmi.android.tv.ui.base.BaseActivity;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import cat.ereza.customactivityoncrash.CustomActivityOnCrash;
|
||||
|
||||
public class CrashActivity extends BaseActivity {
|
||||
|
||||
private ActivityCrashBinding mBinding;
|
||||
private String errorDetails;
|
||||
|
||||
@Override
|
||||
protected ViewBinding getBinding() {
|
||||
return mBinding = ActivityCrashBinding.inflate(getLayoutInflater());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initView(Bundle savedInstanceState) {
|
||||
errorDetails = CustomActivityOnCrash.getAllErrorDetailsFromIntent(this, getIntent());
|
||||
mBinding.error.setText(errorDetails);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initEvent() {
|
||||
mBinding.copy.setOnClickListener(v -> copyErrorToClipboard());
|
||||
mBinding.restart.setOnClickListener(v -> CustomActivityOnCrash.restartApplication(this, Objects.requireNonNull(CustomActivityOnCrash.getConfigFromIntent(getIntent()))));
|
||||
}
|
||||
|
||||
private void copyErrorToClipboard() {
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText(getString(R.string.crash_details_title), errorDetails);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show();
|
||||
showError();
|
||||
}
|
||||
|
||||
private void showError() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.crash_details_title)
|
||||
.setMessage(errorDetails)
|
||||
.setPositiveButton(R.string.crash_details_close, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import com.fongmi.android.tv.ui.adapter.HistoryAdapter;
|
||||
import com.fongmi.android.tv.ui.base.BaseActivity;
|
||||
import com.fongmi.android.tv.ui.dialog.SyncDialog;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
@@ -57,8 +58,27 @@ public class HistoryActivity extends BaseActivity implements HistoryAdapter.OnCl
|
||||
}
|
||||
|
||||
private void getHistory() {
|
||||
mAdapter.addAll(History.get());
|
||||
mAdapter.addAll(History.getAll()); // 显示所有视频源的观看记录
|
||||
mBinding.delete.setVisibility(mAdapter.getItemCount() > 0 ? View.VISIBLE : View.GONE);
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
private void updateEmptyState() {
|
||||
boolean isEmpty = mAdapter.getItemCount() == 0;
|
||||
mBinding.emptyLayout.getRoot().setVisibility(isEmpty ? View.VISIBLE : View.GONE);
|
||||
mBinding.recycler.setVisibility(isEmpty ? View.GONE : View.VISIBLE);
|
||||
|
||||
// 控制Lottie动画播放
|
||||
if (isEmpty) {
|
||||
try {
|
||||
LottieAnimationView lottieView = mBinding.emptyLayout.getRoot().findViewById(R.id.lottieAnimation);
|
||||
if (lottieView != null) {
|
||||
lottieView.playAnimation();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onSync(View view) {
|
||||
@@ -67,7 +87,10 @@ public class HistoryActivity extends BaseActivity implements HistoryAdapter.OnCl
|
||||
|
||||
private void onDelete(View view) {
|
||||
if (mAdapter.isDelete()) {
|
||||
new MaterialAlertDialogBuilder(this).setTitle(R.string.dialog_delete_record).setMessage(R.string.dialog_delete_history).setNegativeButton(R.string.dialog_negative, null).setPositiveButton(R.string.dialog_positive, (dialog, which) -> mAdapter.clear()).show();
|
||||
new MaterialAlertDialogBuilder(this).setTitle(R.string.dialog_delete_record).setMessage(R.string.dialog_delete_history).setNegativeButton(R.string.dialog_negative, null).setPositiveButton(R.string.dialog_positive, (dialog, which) -> {
|
||||
mAdapter.clear();
|
||||
updateEmptyState();
|
||||
}).show();
|
||||
} else if (mAdapter.getItemCount() > 0) {
|
||||
mAdapter.setDelete(true);
|
||||
} else {
|
||||
@@ -91,6 +114,7 @@ public class HistoryActivity extends BaseActivity implements HistoryAdapter.OnCl
|
||||
if (mAdapter.getItemCount() > 0) return;
|
||||
mBinding.delete.setVisibility(View.GONE);
|
||||
mAdapter.setDelete(false);
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import com.fongmi.android.tv.App;
|
||||
import com.fongmi.android.tv.R;
|
||||
import com.fongmi.android.tv.Setting;
|
||||
import com.fongmi.android.tv.Updater;
|
||||
import com.fongmi.android.tv.api.config.LiveConfig;
|
||||
import com.fongmi.android.tv.api.config.VodConfig;
|
||||
@@ -33,7 +34,6 @@ import com.fongmi.android.tv.server.Server;
|
||||
import com.fongmi.android.tv.ui.base.BaseActivity;
|
||||
import com.fongmi.android.tv.ui.custom.FragmentStateManager;
|
||||
import com.fongmi.android.tv.ui.fragment.SettingFragment;
|
||||
import com.fongmi.android.tv.ui.fragment.SettingPlayerFragment;
|
||||
import com.fongmi.android.tv.ui.fragment.VodFragment;
|
||||
import com.fongmi.android.tv.utils.FileChooser;
|
||||
import com.fongmi.android.tv.utils.Notify;
|
||||
@@ -63,6 +63,18 @@ public class HomeActivity extends BaseActivity implements NavigationBarView.OnIt
|
||||
|
||||
@Override
|
||||
protected void initView(Bundle savedInstanceState) {
|
||||
// 检查隐私协议
|
||||
if (!Setting.isPrivacyAgreed()) {
|
||||
Intent intent = new Intent(this, PrivacyAgreementActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保通知渠道已创建(用户已同意协议的情况)
|
||||
com.fongmi.android.tv.utils.Notify.createChannel();
|
||||
|
||||
orientation = getResources().getConfiguration().orientation;
|
||||
Updater.create().release().start(this);
|
||||
initFragment(savedInstanceState);
|
||||
@@ -95,7 +107,6 @@ public class HomeActivity extends BaseActivity implements NavigationBarView.OnIt
|
||||
public Fragment getItem(int position) {
|
||||
if (position == 0) return VodFragment.newInstance();
|
||||
if (position == 1) return SettingFragment.newInstance();
|
||||
if (position == 2) return SettingPlayerFragment.newInstance();
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -143,7 +154,7 @@ public class HomeActivity extends BaseActivity implements NavigationBarView.OnIt
|
||||
private void setNavigation() {
|
||||
mBinding.navigation.getMenu().findItem(R.id.vod).setVisible(true);
|
||||
mBinding.navigation.getMenu().findItem(R.id.setting).setVisible(true);
|
||||
mBinding.navigation.getMenu().findItem(R.id.live).setVisible(LiveConfig.hasUrl());
|
||||
mBinding.navigation.getMenu().findItem(R.id.live).setVisible(LiveConfig.hasUrl() && !Setting.isLiveTabVisible());
|
||||
}
|
||||
|
||||
private boolean openLive() {
|
||||
@@ -179,7 +190,13 @@ public class HomeActivity extends BaseActivity implements NavigationBarView.OnIt
|
||||
if (mBinding.navigation.getSelectedItemId() == item.getItemId()) return false;
|
||||
if (item.getItemId() == R.id.setting) return mManager.change(1);
|
||||
if (item.getItemId() == R.id.vod) return mManager.change(0);
|
||||
if (item.getItemId() == R.id.live) return openLive();
|
||||
if (item.getItemId() == R.id.live) {
|
||||
if (LiveConfig.isEmpty()) {
|
||||
Notify.showCenter(R.string.error_no_live);
|
||||
return false;
|
||||
}
|
||||
return openLive();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -204,8 +221,6 @@ public class HomeActivity extends BaseActivity implements NavigationBarView.OnIt
|
||||
protected void onBackPress() {
|
||||
if (!mBinding.navigation.getMenu().findItem(R.id.vod).isVisible()) {
|
||||
setNavigation();
|
||||
} else if (mManager.isVisible(2)) {
|
||||
change(1);
|
||||
} else if (mManager.isVisible(1)) {
|
||||
mBinding.navigation.setSelectedItemId(R.id.vod);
|
||||
} else if (mManager.canBack(0)) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.fongmi.android.tv.ui.base.BaseActivity;
|
||||
import com.fongmi.android.tv.ui.dialog.SyncDialog;
|
||||
import com.fongmi.android.tv.utils.Notify;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
@@ -63,6 +64,25 @@ public class KeepActivity extends BaseActivity implements KeepAdapter.OnClickLis
|
||||
private void getKeep() {
|
||||
mAdapter.addAll(Keep.getVod());
|
||||
mBinding.delete.setVisibility(mAdapter.getItemCount() > 0 ? View.VISIBLE : View.GONE);
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
private void updateEmptyState() {
|
||||
boolean isEmpty = mAdapter.getItemCount() == 0;
|
||||
mBinding.emptyLayout.getRoot().setVisibility(isEmpty ? View.VISIBLE : View.GONE);
|
||||
mBinding.recycler.setVisibility(isEmpty ? View.GONE : View.VISIBLE);
|
||||
|
||||
// 控制Lottie动画播放
|
||||
if (isEmpty) {
|
||||
try {
|
||||
LottieAnimationView lottieView = mBinding.emptyLayout.getRoot().findViewById(R.id.lottieAnimation);
|
||||
if (lottieView != null) {
|
||||
lottieView.playAnimation();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onSync(View view) {
|
||||
@@ -71,7 +91,10 @@ public class KeepActivity extends BaseActivity implements KeepAdapter.OnClickLis
|
||||
|
||||
private void onDelete(View view) {
|
||||
if (mAdapter.isDelete()) {
|
||||
new MaterialAlertDialogBuilder(this).setTitle(R.string.dialog_delete_record).setMessage(R.string.dialog_delete_keep).setNegativeButton(R.string.dialog_negative, null).setPositiveButton(R.string.dialog_positive, (dialog, which) -> mAdapter.clear()).show();
|
||||
new MaterialAlertDialogBuilder(this).setTitle(R.string.dialog_delete_record).setMessage(R.string.dialog_delete_keep).setNegativeButton(R.string.dialog_negative, null).setPositiveButton(R.string.dialog_positive, (dialog, which) -> {
|
||||
mAdapter.clear();
|
||||
updateEmptyState();
|
||||
}).show();
|
||||
} else if (mAdapter.getItemCount() > 0) {
|
||||
mAdapter.setDelete(true);
|
||||
} else {
|
||||
@@ -114,6 +137,7 @@ public class KeepActivity extends BaseActivity implements KeepAdapter.OnClickLis
|
||||
if (mAdapter.getItemCount() > 0) return;
|
||||
mBinding.delete.setVisibility(View.GONE);
|
||||
mAdapter.setDelete(false);
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.fongmi.android.tv.ui.activity;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.drawable.Drawable;
|
||||
@@ -98,6 +100,8 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
|
||||
private String tag;
|
||||
private int count;
|
||||
private PiP mPiP;
|
||||
private BroadcastReceiver mScreenReceiver;
|
||||
private boolean mPausedByScreen = false;
|
||||
|
||||
public static void start(Context context) {
|
||||
if (!LiveConfig.isEmpty()) context.startActivity(new Intent(context, LiveActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).putExtra("empty", false));
|
||||
@@ -148,6 +152,7 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
|
||||
mR2 = this::setTraffic;
|
||||
mR3 = this::hideInfo;
|
||||
mPiP = new PiP();
|
||||
initScreenReceiver();
|
||||
Server.get().start();
|
||||
setRecyclerView();
|
||||
setVideoView();
|
||||
@@ -155,6 +160,33 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
|
||||
checkLive();
|
||||
}
|
||||
|
||||
private void initScreenReceiver() {
|
||||
// 屏幕开关监听 - 仅用于画中画模式下控制播放
|
||||
mScreenReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent == null || intent.getAction() == null) return;
|
||||
|
||||
// 只在画中画模式下处理屏幕开关
|
||||
if (isInPictureInPictureMode()) {
|
||||
if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
|
||||
// 画中画模式下关屏,暂停播放
|
||||
if (mPlayers.isPlaying()) {
|
||||
onPaused();
|
||||
mPausedByScreen = true;
|
||||
}
|
||||
} else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {
|
||||
// 画中画模式下开屏,恢复播放
|
||||
if (mPausedByScreen) {
|
||||
onPlay();
|
||||
mPausedByScreen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
protected void initEvent() {
|
||||
@@ -1048,6 +1080,8 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
|
||||
hideInfo();
|
||||
hideUI();
|
||||
} else {
|
||||
// 退出画中画模式时,重置屏幕暂停标志
|
||||
mPausedByScreen = false;
|
||||
hideInfo();
|
||||
if (isStop()) finish();
|
||||
}
|
||||
@@ -1075,6 +1109,13 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
// 注册屏幕开关监听
|
||||
if (mScreenReceiver != null) {
|
||||
IntentFilter screenFilter = new IntentFilter();
|
||||
screenFilter.addAction(Intent.ACTION_SCREEN_ON);
|
||||
screenFilter.addAction(Intent.ACTION_SCREEN_OFF);
|
||||
registerReceiver(mScreenReceiver, screenFilter);
|
||||
}
|
||||
if (isRedirect()) onPlay();
|
||||
setRedirect(false);
|
||||
}
|
||||
@@ -1082,6 +1123,14 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
// 注销屏幕开关监听
|
||||
try {
|
||||
if (mScreenReceiver != null) {
|
||||
unregisterReceiver(mScreenReceiver);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignore
|
||||
}
|
||||
if (isRedirect()) onPaused();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.fongmi.android.tv.ui.activity;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import com.fongmi.android.tv.R;
|
||||
import com.fongmi.android.tv.Setting;
|
||||
import com.fongmi.android.tv.databinding.ActivityPrivacyAgreementBinding;
|
||||
import com.fongmi.android.tv.ui.base.BaseActivity;
|
||||
|
||||
public class PrivacyAgreementActivity extends BaseActivity {
|
||||
|
||||
private ActivityPrivacyAgreementBinding mBinding;
|
||||
|
||||
@Override
|
||||
protected ViewBinding getBinding() {
|
||||
return mBinding = ActivityPrivacyAgreementBinding.inflate(getLayoutInflater());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initView(Bundle savedInstanceState) {
|
||||
// 隐私协议页面初始化完成
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initEvent() {
|
||||
if (mBinding != null) {
|
||||
if (mBinding.agreeButton != null) {
|
||||
mBinding.agreeButton.setOnClickListener(this::onAgree);
|
||||
}
|
||||
if (mBinding.disagreeButton != null) {
|
||||
mBinding.disagreeButton.setOnClickListener(this::onDisagree);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onAgree(View view) {
|
||||
// 用户同意协议
|
||||
Setting.setPrivacyAgreed(true);
|
||||
|
||||
// 创建通知渠道(此时才请求通知权限)
|
||||
com.fongmi.android.tv.utils.Notify.createChannel();
|
||||
|
||||
// 跳转到主界面,清除任务栈避免用户通过任务管理器回到协议页面
|
||||
Intent intent = new Intent(this, HomeActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void onDisagree(View view) {
|
||||
// 用户不同意协议,退出应用
|
||||
try {
|
||||
// 清除隐私协议状态(可选,确保下次启动重新询问)
|
||||
Setting.setPrivacyAgreed(false);
|
||||
|
||||
// 优雅地退出应用
|
||||
finishAffinity();
|
||||
|
||||
// 延迟退出,让 Activity 完成销毁
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
|
||||
System.exit(0);
|
||||
}, 100);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
// 备选退出方案
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
// 禁用返回键,用户必须做出选择
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
onDisagree(null);
|
||||
return true;
|
||||
}
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
// 清理 binding 引用
|
||||
mBinding = null;
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,38 @@
|
||||
package com.fongmi.android.tv.ui.fragment;
|
||||
package com.fongmi.android.tv.ui.activity;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import com.fongmi.android.tv.R;
|
||||
import com.fongmi.android.tv.Setting;
|
||||
import com.fongmi.android.tv.databinding.FragmentSettingPlayerBinding;
|
||||
import com.fongmi.android.tv.databinding.ActivitySettingPlayerBinding;
|
||||
import com.fongmi.android.tv.impl.BufferCallback;
|
||||
import com.fongmi.android.tv.impl.SpeedCallback;
|
||||
import com.fongmi.android.tv.impl.UaCallback;
|
||||
import com.fongmi.android.tv.ui.base.BaseFragment;
|
||||
import com.fongmi.android.tv.ui.base.BaseActivity;
|
||||
import com.fongmi.android.tv.ui.dialog.BufferDialog;
|
||||
import com.fongmi.android.tv.ui.dialog.SpeedDialog;
|
||||
import com.fongmi.android.tv.ui.dialog.UaDialog;
|
||||
import com.fongmi.android.tv.utils.ResUtil;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
public class SettingPlayerFragment extends BaseFragment implements UaCallback, BufferCallback, SpeedCallback {
|
||||
public class SettingPlayerActivity extends BaseActivity implements UaCallback, BufferCallback, SpeedCallback {
|
||||
|
||||
private FragmentSettingPlayerBinding mBinding;
|
||||
private ActivitySettingPlayerBinding mBinding;
|
||||
private DecimalFormat format;
|
||||
private String[] background;
|
||||
private String[] caption;
|
||||
private String[] render;
|
||||
private String[] scale;
|
||||
|
||||
public static SettingPlayerFragment newInstance() {
|
||||
return new SettingPlayerFragment();
|
||||
public static void start(Activity activity) {
|
||||
activity.startActivity(new Intent(activity, SettingPlayerActivity.class));
|
||||
}
|
||||
|
||||
private String getSwitch(boolean value) {
|
||||
@@ -43,14 +40,13 @@ public class SettingPlayerFragment extends BaseFragment implements UaCallback, B
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ViewBinding getBinding(@NonNull LayoutInflater inflater, @Nullable ViewGroup container) {
|
||||
return mBinding = FragmentSettingPlayerBinding.inflate(inflater, container, false);
|
||||
protected ViewBinding getBinding() {
|
||||
return mBinding = ActivitySettingPlayerBinding.inflate(getLayoutInflater());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initView() {
|
||||
protected void initView(Bundle savedInstanceState) {
|
||||
format = new DecimalFormat("0.#");
|
||||
mBinding.back.setOnClickListener(v -> requireActivity().onBackPressed());
|
||||
mBinding.uaText.setText(Setting.getUa());
|
||||
mBinding.tunnelSwitch.setChecked(Setting.isTunnel());
|
||||
mBinding.audioDecodeSwitch.setChecked(Setting.isAudioPrefer());
|
||||
@@ -63,44 +59,25 @@ public class SettingPlayerFragment extends BaseFragment implements UaCallback, B
|
||||
mBinding.renderText.setText((render = ResUtil.getStringArray(R.array.select_render))[Setting.getRender()]);
|
||||
mBinding.captionText.setText((caption = ResUtil.getStringArray(R.array.select_caption))[Setting.isCaption() ? 1 : 0]);
|
||||
mBinding.backgroundText.setText((background = ResUtil.getStringArray(R.array.select_background))[Setting.getBackground()]);
|
||||
|
||||
// 设置开关的颜色为黄色
|
||||
int accentColor = getResources().getColor(R.color.accent);
|
||||
android.content.res.ColorStateList colorStateList = new android.content.res.ColorStateList(
|
||||
new int[][]{
|
||||
new int[]{-android.R.attr.state_checked},
|
||||
new int[]{android.R.attr.state_checked}
|
||||
},
|
||||
new int[]{
|
||||
0x66FFFFFF, // 未选中时的颜色
|
||||
accentColor // 选中时的颜色
|
||||
}
|
||||
);
|
||||
|
||||
mBinding.tunnelSwitch.setThumbTintList(android.content.res.ColorStateList.valueOf(android.graphics.Color.WHITE));
|
||||
mBinding.tunnelSwitch.setTrackTintList(colorStateList);
|
||||
mBinding.audioDecodeSwitch.setThumbTintList(android.content.res.ColorStateList.valueOf(android.graphics.Color.WHITE));
|
||||
mBinding.audioDecodeSwitch.setTrackTintList(colorStateList);
|
||||
mBinding.aacSwitch.setThumbTintList(android.content.res.ColorStateList.valueOf(android.graphics.Color.WHITE));
|
||||
mBinding.aacSwitch.setTrackTintList(colorStateList);
|
||||
mBinding.danmakuLoadSwitch.setThumbTintList(android.content.res.ColorStateList.valueOf(android.graphics.Color.WHITE));
|
||||
mBinding.danmakuLoadSwitch.setTrackTintList(colorStateList);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initEvent() {
|
||||
mBinding.back.setOnClickListener(v -> finish());
|
||||
mBinding.ua.setOnClickListener(this::onUa);
|
||||
mBinding.aac.setOnClickListener(this::setAAC);
|
||||
mBinding.scale.setOnClickListener(this::onScale);
|
||||
mBinding.speed.setOnClickListener(this::onSpeed);
|
||||
mBinding.buffer.setOnClickListener(this::onBuffer);
|
||||
mBinding.render.setOnClickListener(this::setRender);
|
||||
mBinding.tunnel.setOnClickListener(this::setTunnel);
|
||||
mBinding.caption.setOnClickListener(this::setCaption);
|
||||
mBinding.caption.setOnLongClickListener(this::onCaption);
|
||||
mBinding.background.setOnClickListener(this::onBackground);
|
||||
mBinding.audioDecode.setOnClickListener(this::setAudioDecode);
|
||||
mBinding.danmakuLoad.setOnClickListener(this::setDanmakuLoad);
|
||||
|
||||
// 直接给开关按钮设置点击监听器,避免双重点击冲突
|
||||
mBinding.tunnelSwitch.setOnClickListener(this::setTunnel);
|
||||
mBinding.audioDecodeSwitch.setOnClickListener(this::setAudioDecode);
|
||||
mBinding.aacSwitch.setOnClickListener(this::setAAC);
|
||||
mBinding.danmakuLoadSwitch.setOnClickListener(this::setDanmakuLoad);
|
||||
}
|
||||
|
||||
private void onUa(View view) {
|
||||
@@ -116,11 +93,11 @@ public class SettingPlayerFragment extends BaseFragment implements UaCallback, B
|
||||
private void setAAC(View view) {
|
||||
boolean isChecked = !Setting.isPreferAAC();
|
||||
Setting.putPreferAAC(isChecked);
|
||||
mBinding.aacSwitch.setChecked(isChecked);
|
||||
// 不需要再次调用 setChecked,因为点击已经触发了状态变化
|
||||
}
|
||||
|
||||
private void onScale(View view) {
|
||||
new MaterialAlertDialogBuilder(getActivity()).setTitle(R.string.player_scale).setNegativeButton(R.string.dialog_negative, null).setSingleChoiceItems(scale, Setting.getScale(), (dialog, which) -> {
|
||||
new com.google.android.material.dialog.MaterialAlertDialogBuilder(this).setTitle(R.string.player_scale).setNegativeButton(R.string.dialog_negative, null).setSingleChoiceItems(scale, Setting.getScale(), (dialog, which) -> {
|
||||
mBinding.scaleText.setText(scale[which]);
|
||||
Setting.putScale(which);
|
||||
dialog.dismiss();
|
||||
@@ -157,7 +134,7 @@ public class SettingPlayerFragment extends BaseFragment implements UaCallback, B
|
||||
private void setTunnel(View view) {
|
||||
boolean isChecked = !Setting.isTunnel();
|
||||
Setting.putTunnel(isChecked);
|
||||
mBinding.tunnelSwitch.setChecked(isChecked);
|
||||
// 不需要再次调用 setChecked,因为点击已经触发了状态变化
|
||||
if (isChecked && Setting.getRender() == 1) setRender(view);
|
||||
}
|
||||
|
||||
@@ -172,7 +149,7 @@ public class SettingPlayerFragment extends BaseFragment implements UaCallback, B
|
||||
}
|
||||
|
||||
private void onBackground(View view) {
|
||||
new MaterialAlertDialogBuilder(getActivity()).setTitle(R.string.player_background).setNegativeButton(R.string.dialog_negative, null).setSingleChoiceItems(background, Setting.getBackground(), (dialog, which) -> {
|
||||
new com.google.android.material.dialog.MaterialAlertDialogBuilder(this).setTitle(R.string.player_background).setNegativeButton(R.string.dialog_negative, null).setSingleChoiceItems(background, Setting.getBackground(), (dialog, which) -> {
|
||||
mBinding.backgroundText.setText(background[which]);
|
||||
Setting.putBackground(which);
|
||||
dialog.dismiss();
|
||||
@@ -182,17 +159,12 @@ public class SettingPlayerFragment extends BaseFragment implements UaCallback, B
|
||||
private void setAudioDecode(View view) {
|
||||
boolean isChecked = !Setting.isAudioPrefer();
|
||||
Setting.putAudioPrefer(isChecked);
|
||||
mBinding.audioDecodeSwitch.setChecked(isChecked);
|
||||
// 不需要再次调用 setChecked,因为点击已经触发了状态变化
|
||||
}
|
||||
|
||||
private void setDanmakuLoad(View view) {
|
||||
boolean isChecked = !Setting.isDanmakuLoad();
|
||||
Setting.putDanmakuLoad(isChecked);
|
||||
mBinding.danmakuLoadSwitch.setChecked(isChecked);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenChanged(boolean hidden) {
|
||||
if (!hidden) initView();
|
||||
// 不需要再次调用 setChecked,因为点击已经触发了状态变化
|
||||
}
|
||||
}
|
||||
@@ -4,24 +4,35 @@ import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.provider.Settings;
|
||||
import android.text.Html;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.format.DateFormat;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.media.AudioManager;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -63,6 +74,7 @@ import com.fongmi.android.tv.event.RefreshEvent;
|
||||
import com.fongmi.android.tv.model.SiteViewModel;
|
||||
import com.fongmi.android.tv.player.Players;
|
||||
import com.fongmi.android.tv.player.exo.ExoUtil;
|
||||
import com.fongmi.android.tv.player.Source;
|
||||
import com.fongmi.android.tv.service.PlaybackService;
|
||||
import com.fongmi.android.tv.ui.adapter.EpisodeAdapter;
|
||||
import com.fongmi.android.tv.ui.adapter.FlagAdapter;
|
||||
@@ -98,6 +110,7 @@ import com.github.catvod.utils.Trans;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
import com.permissionx.guolindev.PermissionX;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
|
||||
@@ -148,6 +161,14 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
|
||||
private Clock mClock;
|
||||
private String tag;
|
||||
private PiP mPiP;
|
||||
private Handler mHandler;
|
||||
private Runnable mTimeUpdateRunnable;
|
||||
private BroadcastReceiver mBatteryReceiver;
|
||||
private BroadcastReceiver mScreenReceiver;
|
||||
private int mBatteryLevel = -1;
|
||||
private boolean mIsCharging = false;
|
||||
private boolean mPausedByScreen = false;
|
||||
private AudioManager mAudioManager;
|
||||
|
||||
public static void push(FragmentActivity activity, String 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<>();
|
||||
mBroken = new ArrayList<>();
|
||||
mClock = Clock.create();
|
||||
mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
|
||||
mR1 = this::hideControl;
|
||||
mR2 = this::setTraffic;
|
||||
mR3 = this::setOrient;
|
||||
@@ -302,6 +324,131 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
|
||||
showProgress();
|
||||
showDanmaku();
|
||||
checkId();
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
initTimeBatteryUpdate();
|
||||
}
|
||||
|
||||
private void initTimeBatteryUpdate() {
|
||||
mBatteryReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {
|
||||
int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
|
||||
int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
|
||||
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
|
||||
|
||||
if (level != -1 && scale != -1) {
|
||||
mBatteryLevel = (int) ((level / (float) scale) * 100);
|
||||
mIsCharging = (status == BatteryManager.BATTERY_STATUS_CHARGING ||
|
||||
status == BatteryManager.BATTERY_STATUS_FULL);
|
||||
updateTimeBattery();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 屏幕开关监听 - 仅用于画中画模式下控制播放
|
||||
mScreenReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent == null || intent.getAction() == null) return;
|
||||
|
||||
// 只在画中画模式下处理屏幕开关
|
||||
if (isInPictureInPictureMode()) {
|
||||
if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
|
||||
// 画中画模式下关屏,暂停播放
|
||||
if (mPlayers.isPlaying()) {
|
||||
onPaused();
|
||||
mPausedByScreen = true;
|
||||
}
|
||||
} else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {
|
||||
// 画中画模式下开屏,恢复播放
|
||||
if (mPausedByScreen) {
|
||||
onPlay();
|
||||
mPausedByScreen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mTimeUpdateRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateTimeBattery();
|
||||
mHandler.postDelayed(this, 30000);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void updateTimeBattery() {
|
||||
TextView timeBattery = findViewById(R.id.time_battery);
|
||||
TextView batteryText = findViewById(R.id.battery_icon);
|
||||
android.widget.ImageView chargingIndicator = findViewById(R.id.charging_indicator);
|
||||
|
||||
// 只在全屏模式下显示
|
||||
if (isFullscreen()) {
|
||||
// 更新时间
|
||||
if (timeBattery != null) {
|
||||
String time = DateFormat.getTimeFormat(this).format(System.currentTimeMillis());
|
||||
timeBattery.setText(time);
|
||||
timeBattery.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
// 更新充电图标
|
||||
if (chargingIndicator != null) {
|
||||
chargingIndicator.setVisibility(mIsCharging && mBatteryLevel >= 0 ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
// 更新电池百分比文字
|
||||
if (batteryText != null && mBatteryLevel >= 0) {
|
||||
batteryText.setText(mBatteryLevel + "%");
|
||||
batteryText.setVisibility(View.VISIBLE);
|
||||
} else if (batteryText != null) {
|
||||
batteryText.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
if (timeBattery != null) {
|
||||
timeBattery.setVisibility(View.GONE);
|
||||
}
|
||||
if (batteryText != null) {
|
||||
batteryText.setVisibility(View.GONE);
|
||||
}
|
||||
if (chargingIndicator != null) {
|
||||
chargingIndicator.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startTimeBatteryUpdates() {
|
||||
registerReceiver(mBatteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
|
||||
|
||||
// 注册屏幕开关监听
|
||||
IntentFilter screenFilter = new IntentFilter();
|
||||
screenFilter.addAction(Intent.ACTION_SCREEN_ON);
|
||||
screenFilter.addAction(Intent.ACTION_SCREEN_OFF);
|
||||
registerReceiver(mScreenReceiver, screenFilter);
|
||||
|
||||
updateTimeBattery();
|
||||
mHandler.post(mTimeUpdateRunnable);
|
||||
}
|
||||
|
||||
private void stopTimeBatteryUpdates() {
|
||||
try {
|
||||
if (mBatteryReceiver != null) {
|
||||
unregisterReceiver(mBatteryReceiver);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
||||
try {
|
||||
if (mScreenReceiver != null) {
|
||||
unregisterReceiver(mScreenReceiver);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
||||
mHandler.removeCallbacks(mTimeUpdateRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -441,8 +588,11 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
|
||||
mBinding.swipeLayout.setRefreshing(false);
|
||||
if (result.getList().isEmpty()) setEmpty(result.hasMsg());
|
||||
else setDetail(result.getList().get(0));
|
||||
// 只在有错误或重要消息时显示提示
|
||||
if (result.hasMsg() && result.getList().isEmpty()) {
|
||||
Notify.show(result.getMsg());
|
||||
}
|
||||
}
|
||||
|
||||
private void setEmpty(boolean 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.top.setVisibility(isLock() ? View.GONE : View.VISIBLE);
|
||||
mBinding.control.getRoot().setVisibility(View.VISIBLE);
|
||||
updateTimeBattery();
|
||||
setR1Callback();
|
||||
checkPlayImg();
|
||||
}
|
||||
@@ -1389,6 +1540,55 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
|
||||
this.rotate = rotate;
|
||||
if (fullscreen && rotate) noPadding(mBinding.control.getRoot());
|
||||
if (fullscreen && !rotate) setPadding(mBinding.control.getRoot());
|
||||
// 检测屏幕方向变化并处理
|
||||
onOrientationChanged();
|
||||
}
|
||||
|
||||
// 添加屏幕方向变化处理方法
|
||||
private void onOrientationChanged() {
|
||||
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
// 切换到横屏模式
|
||||
onLandscapeMode();
|
||||
} else {
|
||||
// 切换到竖屏模式
|
||||
onPortraitMode();
|
||||
}
|
||||
}
|
||||
|
||||
private void onLandscapeMode() {
|
||||
// 横屏模式下的特殊处理
|
||||
// 调整进度条的敏感度
|
||||
if (mPlayers != null) {
|
||||
long duration = mPlayers.getDuration();
|
||||
if (duration > TimeUnit.MINUTES.toMillis(30)) {
|
||||
mBinding.control.seek.setKeyTimeIncrement(TimeUnit.MINUTES.toMillis(1));
|
||||
} else if (duration > TimeUnit.MINUTES.toMillis(10)) {
|
||||
mBinding.control.seek.setKeyTimeIncrement(TimeUnit.SECONDS.toMillis(30));
|
||||
} else if (duration > 0) {
|
||||
mBinding.control.seek.setKeyTimeIncrement(TimeUnit.SECONDS.toMillis(15));
|
||||
}
|
||||
}
|
||||
|
||||
// 确保进度条状态正确
|
||||
if (mPlayers != null) {
|
||||
long position = mPlayers.getPosition();
|
||||
long duration = mPlayers.getDuration();
|
||||
if (position > 0 && duration > 0) {
|
||||
mBinding.control.seek.setPosition(position);
|
||||
mBinding.control.seek.setDuration(duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onPortraitMode() {
|
||||
// 竖屏模式下的处理
|
||||
// 恢复进度条的默认敏感度
|
||||
if (mPlayers != null) {
|
||||
long duration = mPlayers.getDuration();
|
||||
if (duration > 0) {
|
||||
mBinding.control.seek.setKeyTimeIncrement(duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isStop() {
|
||||
@@ -1490,10 +1690,40 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
|
||||
|
||||
@Override
|
||||
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);
|
||||
mPlayers.pause();
|
||||
mPlayers.seek(time);
|
||||
showProgress();
|
||||
App.post(() -> {
|
||||
long actualPosition = mPlayers.getPosition();
|
||||
if (Math.abs(actualPosition - time) > 500) {
|
||||
mPlayers.seek(time);
|
||||
}
|
||||
onPlay();
|
||||
hideProgress();
|
||||
}, 150); // 横屏模式下延迟更长,确保跳转完成
|
||||
} else {
|
||||
// 竖屏模式使用原有逻辑
|
||||
mBinding.widget.seek.setVisibility(View.GONE);
|
||||
mPlayers.pause();
|
||||
mPlayers.seek(time);
|
||||
showProgress();
|
||||
App.post(() -> {
|
||||
long actualPosition = mPlayers.getPosition();
|
||||
if (Math.abs(actualPosition - time) > 500) {
|
||||
mPlayers.seek(time);
|
||||
}
|
||||
onPlay();
|
||||
hideProgress();
|
||||
}, 100); // 竖屏模式下延迟较短
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1544,6 +1774,8 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
|
||||
hideDanmaku();
|
||||
hideSheet();
|
||||
} else {
|
||||
// 退出画中画模式时,重置屏幕暂停标志
|
||||
mPausedByScreen = false;
|
||||
showDanmaku();
|
||||
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_LANDSCAPE) enterFullscreen();
|
||||
if (isFullscreen()) Util.hideSystemUI(this);
|
||||
updateTimeBattery();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1574,6 +1807,7 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
startTimeBatteryUpdates();
|
||||
if (isRedirect()) onPlay();
|
||||
setRedirect(false);
|
||||
}
|
||||
@@ -1581,6 +1815,7 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
stopTimeBatteryUpdates();
|
||||
if (isRedirect()) onPaused();
|
||||
}
|
||||
|
||||
@@ -1592,6 +1827,71 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
|
||||
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
|
||||
public void onBackPressed() {
|
||||
if (isVisible(mBinding.control.getRoot())) {
|
||||
@@ -1608,14 +1908,17 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
stopSearch();
|
||||
mClock.release();
|
||||
mPlayers.release();
|
||||
mClock.release();
|
||||
Timer.get().reset();
|
||||
RefreshEvent.history();
|
||||
PlaybackService.stop();
|
||||
mHandler.removeCallbacksAndMessages(null);
|
||||
App.removeCallbacks(mR1, mR2, mR3, mR4);
|
||||
EventBus.getDefault().unregister(this);
|
||||
mViewModel.result.removeObserver(mObserveDetail);
|
||||
mViewModel.player.removeObserver(mObservePlayer);
|
||||
mViewModel.search.removeObserver(mObserveSearch);
|
||||
stopTimeBatteryUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.fongmi.android.tv.api.config.VodConfig;
|
||||
import com.fongmi.android.tv.bean.Config;
|
||||
import com.fongmi.android.tv.databinding.AdapterConfigBinding;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ConfigAdapter extends RecyclerView.Adapter<ConfigAdapter.ViewHolder> {
|
||||
@@ -26,19 +27,36 @@ public class ConfigAdapter extends RecyclerView.Adapter<ConfigAdapter.ViewHolder
|
||||
|
||||
void onTextClick(Config item);
|
||||
|
||||
void onCopyClick(Config item);
|
||||
|
||||
void onDeleteClick(Config item);
|
||||
}
|
||||
|
||||
public ConfigAdapter addAll(int type) {
|
||||
mItems = Config.getAll(type);
|
||||
mItems.remove(type == 0 ? VodConfig.get().getConfig() : LiveConfig.get().getConfig());
|
||||
mItems = new ArrayList<>();
|
||||
List<Config> configs = Config.getAll(type);
|
||||
Config currentConfig = type == 0 ? VodConfig.get().getConfig() : LiveConfig.get().getConfig();
|
||||
|
||||
for (Config config : configs) {
|
||||
if (config.equals(currentConfig) || config.isEmpty()) continue;
|
||||
mItems.add(config);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public void addItem(Config item) {
|
||||
if (item.isEmpty()) return;
|
||||
|
||||
mItems.add(0, item);
|
||||
notifyItemInserted(0);
|
||||
}
|
||||
|
||||
public int remove(Config item) {
|
||||
int position = mItems.indexOf(item);
|
||||
item.delete();
|
||||
mItems.remove(item);
|
||||
notifyDataSetChanged();
|
||||
notifyItemRemoved(position);
|
||||
return getItemCount();
|
||||
}
|
||||
|
||||
@@ -58,6 +76,7 @@ public class ConfigAdapter extends RecyclerView.Adapter<ConfigAdapter.ViewHolder
|
||||
Config item = mItems.get(position);
|
||||
holder.binding.text.setText(item.getDesc());
|
||||
holder.binding.text.setOnClickListener(v -> mListener.onTextClick(item));
|
||||
holder.binding.copy.setOnClickListener(v -> mListener.onCopyClick(item));
|
||||
holder.binding.delete.setOnClickListener(v -> mListener.onDeleteClick(item));
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ public class TypeAdapter extends RecyclerView.Adapter<TypeAdapter.ViewHolder> {
|
||||
|
||||
public void addAll(Result result) {
|
||||
mItems.addAll(result.getTypes());
|
||||
if (!result.getList().isEmpty()) mItems.add(0, home());
|
||||
if (!mItems.isEmpty()) mItems.get(0).setActivated(true);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.content.res.Configuration;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
@@ -109,6 +110,17 @@ public class CustomKeyDownVod extends GestureDetector.SimpleOnGestureListener im
|
||||
if (isEdge(e1) || changeScale || lock || e1.getPointerCount() > 1) return true;
|
||||
float deltaX = e2.getX() - e1.getX();
|
||||
float deltaY = e1.getY() - e2.getY();
|
||||
|
||||
// 在横屏模式下,调整触摸事件的处理逻辑
|
||||
if (activity.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
// 横屏模式下,增加对水平滑动的敏感度
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY) * 0.5f) {
|
||||
if (touch) checkFunc(distanceX, distanceY, e2);
|
||||
if (changeTime) listener.onSeek(time = (long) (deltaX * 50));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (touch) checkFunc(distanceX, distanceY, e2);
|
||||
if (changeTime) listener.onSeek(time = (long) (deltaX * 50));
|
||||
if (changeBright) setBright(deltaY);
|
||||
@@ -145,9 +157,32 @@ public class CustomKeyDownVod extends GestureDetector.SimpleOnGestureListener im
|
||||
|
||||
private void checkFunc(float distanceX, float distanceY, MotionEvent e2) {
|
||||
int four = ResUtil.getScreenWidth(activity) / 4;
|
||||
if (e2.getX() > four && e2.getX() < four * 3) center = true;
|
||||
else if (Math.abs(distanceX) < Math.abs(distanceY)) checkSide(e2);
|
||||
if (Math.abs(distanceX) >= Math.abs(distanceY)) changeTime = true;
|
||||
|
||||
// 在横屏模式下,调整中心区域的判断
|
||||
if (activity.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
// 横屏模式下,扩大中心区域,更容易触发进度条调整
|
||||
int centerStart = ResUtil.getScreenWidth(activity) / 3;
|
||||
int centerEnd = ResUtil.getScreenWidth(activity) * 2 / 3;
|
||||
if (e2.getX() > centerStart && e2.getX() < centerEnd) {
|
||||
center = true;
|
||||
} else if (Math.abs(distanceX) < Math.abs(distanceY)) {
|
||||
checkSide(e2);
|
||||
}
|
||||
// 横屏模式下,降低触发进度条调整的阈值
|
||||
if (Math.abs(distanceX) >= Math.abs(distanceY) * 0.7f) {
|
||||
changeTime = true;
|
||||
}
|
||||
} else {
|
||||
// 竖屏模式保持原有逻辑
|
||||
if (e2.getX() > four && e2.getX() < four * 3) {
|
||||
center = true;
|
||||
} else if (Math.abs(distanceX) < Math.abs(distanceY)) {
|
||||
checkSide(e2);
|
||||
}
|
||||
if (Math.abs(distanceX) >= Math.abs(distanceY)) {
|
||||
changeTime = true;
|
||||
}
|
||||
}
|
||||
touch = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.fongmi.android.tv.ui.custom;
|
||||
|
||||
import android.animation.ArgbEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.appcompat.widget.AppCompatCheckBox;
|
||||
|
||||
public class CustomSwitch extends AppCompatCheckBox {
|
||||
|
||||
private Paint trackPaint;
|
||||
private Paint thumbPaint;
|
||||
private RectF trackRect;
|
||||
private RectF thumbRect;
|
||||
|
||||
private float thumbPosition = 0f; // 0 = 左边, 1 = 右边
|
||||
private int currentTrackColor;
|
||||
private int currentThumbColor;
|
||||
|
||||
private static final int TRACK_COLOR_OFF = 0xFF555555; // 灰色
|
||||
private static final int TRACK_COLOR_ON = 0xFFFFEB3B; // 黄色
|
||||
private static final int THUMB_COLOR_OFF = 0xFFFFFFFF; // 白色
|
||||
private static final int THUMB_COLOR_ON = 0xFF000000; // 黑色
|
||||
|
||||
private ValueAnimator animator;
|
||||
|
||||
public CustomSwitch(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public CustomSwitch(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public CustomSwitch(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
// 隐藏默认的checkbox样式
|
||||
setButtonDrawable(null);
|
||||
|
||||
trackPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
thumbPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
trackRect = new RectF();
|
||||
thumbRect = new RectF();
|
||||
|
||||
currentTrackColor = TRACK_COLOR_OFF;
|
||||
currentThumbColor = THUMB_COLOR_OFF;
|
||||
|
||||
// 监听状态变化
|
||||
setOnCheckedChangeListener((buttonView, isChecked) -> animateSwitch(isChecked));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
// 固定尺寸:50dp × 30dp
|
||||
int width = (int) (50 * getResources().getDisplayMetrics().density);
|
||||
int height = (int) (30 * getResources().getDisplayMetrics().density);
|
||||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
int width = getWidth();
|
||||
int height = getHeight();
|
||||
float radius = height / 2f;
|
||||
|
||||
// 绘制轨道
|
||||
trackRect.set(0, 0, width, height);
|
||||
trackPaint.setColor(currentTrackColor);
|
||||
canvas.drawRoundRect(trackRect, radius, radius, trackPaint);
|
||||
|
||||
// 计算小圆位置
|
||||
float thumbSize = height - 8 * getResources().getDisplayMetrics().density; // 22dp
|
||||
float padding = 4 * getResources().getDisplayMetrics().density;
|
||||
float thumbLeft = padding + thumbPosition * (width - thumbSize - 2 * padding);
|
||||
float thumbTop = padding;
|
||||
|
||||
// 绘制小圆
|
||||
thumbRect.set(thumbLeft, thumbTop, thumbLeft + thumbSize, thumbTop + thumbSize);
|
||||
thumbPaint.setColor(currentThumbColor);
|
||||
canvas.drawOval(thumbRect, thumbPaint);
|
||||
}
|
||||
|
||||
private void animateSwitch(boolean isChecked) {
|
||||
if (animator != null && animator.isRunning()) {
|
||||
animator.cancel();
|
||||
}
|
||||
|
||||
float targetPosition = isChecked ? 1f : 0f;
|
||||
int targetTrackColor = isChecked ? TRACK_COLOR_ON : TRACK_COLOR_OFF;
|
||||
int targetThumbColor = isChecked ? THUMB_COLOR_ON : THUMB_COLOR_OFF;
|
||||
|
||||
animator = ValueAnimator.ofFloat(thumbPosition, targetPosition);
|
||||
animator.setDuration(250); // 250ms动画时长
|
||||
|
||||
final ArgbEvaluator colorEvaluator = new ArgbEvaluator();
|
||||
|
||||
animator.addUpdateListener(animation -> {
|
||||
thumbPosition = (float) animation.getAnimatedValue();
|
||||
|
||||
// 颜色渐变
|
||||
currentTrackColor = (int) colorEvaluator.evaluate(
|
||||
thumbPosition, TRACK_COLOR_OFF, TRACK_COLOR_ON
|
||||
);
|
||||
currentThumbColor = (int) colorEvaluator.evaluate(
|
||||
thumbPosition, THUMB_COLOR_OFF, THUMB_COLOR_ON
|
||||
);
|
||||
|
||||
invalidate();
|
||||
});
|
||||
|
||||
animator.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChecked(boolean checked) {
|
||||
super.setChecked(checked);
|
||||
// 初始化时不播放动画
|
||||
if (!isAttachedToWindow()) {
|
||||
thumbPosition = checked ? 1f : 0f;
|
||||
currentTrackColor = checked ? TRACK_COLOR_ON : TRACK_COLOR_OFF;
|
||||
currentThumbColor = checked ? THUMB_COLOR_ON : THUMB_COLOR_OFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.fongmi.android.tv.ui.dialog;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import com.fongmi.android.tv.databinding.DialogAboutBinding;
|
||||
|
||||
public class AboutDialog extends BaseDialog {
|
||||
|
||||
private DialogAboutBinding binding;
|
||||
|
||||
public static void show(FragmentActivity activity) {
|
||||
new AboutDialog().show(activity.getSupportFragmentManager(), "AboutDialog");
|
||||
}
|
||||
|
||||
public static void show(Fragment fragment) {
|
||||
new AboutDialog().show(fragment.getChildFragmentManager(), "AboutDialog");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ViewBinding getBinding(@NonNull LayoutInflater inflater, @Nullable ViewGroup container) {
|
||||
binding = DialogAboutBinding.inflate(inflater, container, false);
|
||||
return binding;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initEvent() {
|
||||
binding.github.setOnClickListener(v -> openGitHub());
|
||||
}
|
||||
|
||||
private void openGitHub() {
|
||||
try {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse("https://github.com/Tosencen/XMBOX/releases/latest"));
|
||||
startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.fongmi.android.tv.ui.dialog;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.view.LayoutInflater;
|
||||
|
||||
@@ -22,11 +23,20 @@ public class BufferDialog {
|
||||
return new BufferDialog(fragment);
|
||||
}
|
||||
|
||||
public static BufferDialog create(Activity activity) {
|
||||
return new BufferDialog(activity);
|
||||
}
|
||||
|
||||
public BufferDialog(Fragment fragment) {
|
||||
this.callback = (BufferCallback) fragment;
|
||||
this.binding = DialogBufferBinding.inflate(LayoutInflater.from(fragment.getContext()));
|
||||
}
|
||||
|
||||
public BufferDialog(Activity activity) {
|
||||
this.callback = (BufferCallback) activity;
|
||||
this.binding = DialogBufferBinding.inflate(LayoutInflater.from(activity));
|
||||
}
|
||||
|
||||
public void show() {
|
||||
initDialog();
|
||||
initView();
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.fongmi.android.tv.api.config.WallConfig;
|
||||
import com.fongmi.android.tv.bean.Config;
|
||||
import com.fongmi.android.tv.databinding.DialogConfigBinding;
|
||||
import com.fongmi.android.tv.impl.ConfigCallback;
|
||||
import com.fongmi.android.tv.impl.Callback;
|
||||
import com.fongmi.android.tv.ui.custom.CustomTextListener;
|
||||
import com.fongmi.android.tv.utils.FileChooser;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
@@ -52,6 +53,10 @@ public class ConfigDialog {
|
||||
|
||||
public ConfigDialog(Fragment fragment) {
|
||||
this.fragment = fragment;
|
||||
// 确保fragment实现了ConfigCallback接口
|
||||
if (!(fragment instanceof ConfigCallback)) {
|
||||
throw new IllegalArgumentException("Fragment must implement ConfigCallback");
|
||||
}
|
||||
this.callback = (ConfigCallback) fragment;
|
||||
this.binding = DialogConfigBinding.inflate(LayoutInflater.from(fragment.getContext()));
|
||||
this.append = true;
|
||||
@@ -144,9 +149,89 @@ public class ConfigDialog {
|
||||
private void onPositive(DialogInterface dialog, int which) {
|
||||
String url = binding.url.getText().toString().trim();
|
||||
String name = binding.name.getText().toString().trim();
|
||||
|
||||
android.util.Log.d("ConfigDialog", "onPositive: type=" + type + ", url=" + url + ", name=" + name);
|
||||
|
||||
// 如果是编辑模式,更新现有配置
|
||||
if (edit) Config.find(ori, type).url(url).name(name).update();
|
||||
if (url.isEmpty()) Config.delete(ori, type);
|
||||
callback.setConfig(Config.find(url, type));
|
||||
|
||||
// 如果URL为空,删除配置
|
||||
if (url.isEmpty()) {
|
||||
android.util.Log.d("ConfigDialog", "URL is empty, deleting config");
|
||||
Config.delete(ori, type);
|
||||
dialog.dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
// 只有URL不为空时,才设置配置
|
||||
// 保存原始URL,以便在添加失败时恢复
|
||||
String originalUrl = ori;
|
||||
android.util.Log.d("ConfigDialog", "Calling Config.find with url=" + url + ", type=" + type);
|
||||
|
||||
Config config = Config.find(url, type);
|
||||
android.util.Log.d("ConfigDialog", "Config.find returned: " + (config != null ? config.toString() : "null"));
|
||||
|
||||
android.util.Log.d("ConfigDialog", "Checking callback: " + (callback != null ? callback.getClass().getName() : "null"));
|
||||
android.util.Log.d("ConfigDialog", "Checking fragment: " + (fragment != null ? fragment.getClass().getName() : "null"));
|
||||
|
||||
android.util.Log.d("ConfigDialog", "Calling callback.setConfig");
|
||||
callback.setConfig(config);
|
||||
|
||||
android.util.Log.d("ConfigDialog", "setConfig completed");
|
||||
|
||||
// 添加一个延迟检查,如果配置没有成功加载,则恢复原始URL
|
||||
new android.os.Handler().postDelayed(() -> {
|
||||
// 检查配置是否成功加载
|
||||
Config currentConfig = getConfig();
|
||||
if (currentConfig == null || !currentConfig.getUrl().equals(url)) {
|
||||
// 配置加载失败,恢复原始URL
|
||||
if (!TextUtils.isEmpty(originalUrl)) {
|
||||
// 如果有原始URL,恢复原始URL
|
||||
callback.setConfig(Config.find(originalUrl, type));
|
||||
} else {
|
||||
// 如果没有原始URL,设置为空
|
||||
switch (type) {
|
||||
case 0:
|
||||
VodConfig.get().clear().config(Config.vod()).load(new Callback() {
|
||||
@Override
|
||||
public void success() {}
|
||||
|
||||
@Override
|
||||
public void success(String result) {}
|
||||
|
||||
@Override
|
||||
public void error(String msg) {}
|
||||
});
|
||||
break;
|
||||
case 1:
|
||||
LiveConfig.get().clear().config(Config.live()).load(new Callback() {
|
||||
@Override
|
||||
public void success() {}
|
||||
|
||||
@Override
|
||||
public void success(String result) {}
|
||||
|
||||
@Override
|
||||
public void error(String msg) {}
|
||||
});
|
||||
break;
|
||||
case 2:
|
||||
WallConfig.get().clear().config(Config.wall()).load(new Callback() {
|
||||
@Override
|
||||
public void success() {}
|
||||
|
||||
@Override
|
||||
public void success(String result) {}
|
||||
|
||||
@Override
|
||||
public void error(String msg) {}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 2000); // 2秒后检查
|
||||
|
||||
dialog.dismiss();
|
||||
}
|
||||
|
||||
|
||||
@@ -23,13 +23,16 @@ import com.fongmi.android.tv.ui.base.ViewType;
|
||||
import com.fongmi.android.tv.ui.custom.SpaceItemDecoration;
|
||||
import com.fongmi.android.tv.utils.ResUtil;
|
||||
import com.fongmi.android.tv.utils.Timer;
|
||||
import com.fongmi.android.tv.utils.Util;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
import com.google.android.material.slider.Slider;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Formatter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickListener {
|
||||
public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickListener, Timer.Callback {
|
||||
|
||||
private DialogControlBinding binding;
|
||||
private ActivityVideoBinding parent;
|
||||
@@ -40,6 +43,8 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
|
||||
private History history;
|
||||
private Players player;
|
||||
private boolean parse;
|
||||
private StringBuilder builder;
|
||||
private Formatter formatter;
|
||||
|
||||
public static ControlDialog create() {
|
||||
return new ControlDialog();
|
||||
@@ -47,6 +52,8 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
|
||||
|
||||
public ControlDialog() {
|
||||
this.scale = ResUtil.getStringArray(R.array.select_scale);
|
||||
this.builder = new StringBuilder();
|
||||
this.formatter = new Formatter(builder, Locale.getDefault());
|
||||
}
|
||||
|
||||
public ControlDialog parent(ActivityVideoBinding parent) {
|
||||
@@ -93,6 +100,15 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
|
||||
binding.opening.setText(parent.control.action.opening.getText());
|
||||
binding.loop.setActivated(parent.control.action.loop.isActivated());
|
||||
binding.timer.setActivated(Timer.get().isRunning());
|
||||
|
||||
// 设置定时器回调并更新按钮文字
|
||||
if (Timer.get().isRunning()) {
|
||||
Timer.get().setCallback(this);
|
||||
updateTimerText(Timer.get().getTick());
|
||||
} else {
|
||||
binding.timer.setText(R.string.play_timer);
|
||||
}
|
||||
|
||||
setTrackVisible();
|
||||
setScaleText();
|
||||
setPlayer();
|
||||
@@ -126,6 +142,8 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
|
||||
private void setSpeed(@NonNull Slider slider, float value, boolean fromUser) {
|
||||
parent.control.action.speed.setText(player.setSpeed(value));
|
||||
if (history != null) history.setSpeed(player.getSpeed());
|
||||
// 实时更新倍速数值显示
|
||||
binding.speedValue.setText(String.format("%.1fx", value));
|
||||
}
|
||||
|
||||
private void setScaleText() {
|
||||
@@ -179,6 +197,8 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
|
||||
binding.player.setText(parent.control.action.player.getText());
|
||||
binding.decode.setVisibility(parent.control.action.decode.getVisibility());
|
||||
binding.danmaku.setVisibility(parent.control.action.danmaku.getVisibility());
|
||||
// 初始化倍速数值显示
|
||||
binding.speedValue.setText(String.format("%.1fx", Math.max(player.getSpeed(), 0.5f)));
|
||||
}
|
||||
|
||||
public void setParseVisible(boolean visible) {
|
||||
@@ -199,6 +219,42 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
|
||||
binding.parse.getAdapter().notifyItemRangeChanged(0, binding.parse.getAdapter().getItemCount());
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新定时按钮文字为倒计时
|
||||
*/
|
||||
private void updateTimerText(long tick) {
|
||||
if (tick > 0) {
|
||||
binding.timer.setText(Util.format(builder, formatter, tick));
|
||||
} else {
|
||||
binding.timer.setText(R.string.play_timer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Timer.Callback 接口实现 - 定时器每秒回调
|
||||
*/
|
||||
@Override
|
||||
public void onTick(long tick) {
|
||||
updateTimerText(tick);
|
||||
}
|
||||
|
||||
/**
|
||||
* Timer.Callback 接口实现 - 定时完成回调
|
||||
*/
|
||||
@Override
|
||||
public void onFinish() {
|
||||
// 定时结束,恢复按钮文字
|
||||
binding.timer.setText(R.string.play_timer);
|
||||
binding.timer.setActivated(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dismiss() {
|
||||
// 关闭对话框时取消定时器回调
|
||||
Timer.get().setCallback(null);
|
||||
super.dismiss();
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
|
||||
void onScale(int tag);
|
||||
|
||||
@@ -60,8 +60,35 @@ public class HistoryDialog implements ConfigAdapter.OnClickListener {
|
||||
|
||||
@Override
|
||||
public void onTextClick(Config item) {
|
||||
callback.setConfig(item);
|
||||
// 防止重复点击和空值
|
||||
if (!dialog.isShowing() || item == null) return;
|
||||
|
||||
// 检查callback是否有效
|
||||
if (callback == null) {
|
||||
dialog.dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
// 先关闭对话框,避免时序冲突
|
||||
dialog.dismiss();
|
||||
|
||||
// 延迟执行配置设置,确保对话框完全关闭
|
||||
App.post(() -> {
|
||||
try {
|
||||
// 双重检查callback和item是否仍然有效
|
||||
if (callback != null && item != null && !item.isEmpty()) {
|
||||
callback.setConfig(item);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
// 如果出现异常,显示错误提示
|
||||
try {
|
||||
Notify.show("配置切换失败: " + e.getMessage());
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
}, 150); // 增加延迟到150毫秒
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -73,6 +100,13 @@ public class HistoryDialog implements ConfigAdapter.OnClickListener {
|
||||
|
||||
@Override
|
||||
public void onDeleteClick(Config item) {
|
||||
if (adapter.remove(item) == 0) dialog.dismiss();
|
||||
int count = adapter.remove(item);
|
||||
if (count == 0) {
|
||||
dialog.dismiss();
|
||||
} else {
|
||||
// 强制重新测量布局高度
|
||||
binding.recycler.requestLayout();
|
||||
dialog.getWindow().setLayout(dialog.getWindow().getAttributes().width, android.view.ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.fongmi.android.tv.ui.dialog;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.view.LayoutInflater;
|
||||
|
||||
@@ -22,11 +23,20 @@ public class SpeedDialog {
|
||||
return new SpeedDialog(fragment);
|
||||
}
|
||||
|
||||
public static SpeedDialog create(Activity activity) {
|
||||
return new SpeedDialog(activity);
|
||||
}
|
||||
|
||||
public SpeedDialog(Fragment fragment) {
|
||||
this.callback = (SpeedCallback) fragment;
|
||||
this.binding = DialogSpeedBinding.inflate(LayoutInflater.from(fragment.getContext()));
|
||||
}
|
||||
|
||||
public SpeedDialog(Activity activity) {
|
||||
this.callback = (SpeedCallback) activity;
|
||||
this.binding = DialogSpeedBinding.inflate(LayoutInflater.from(activity));
|
||||
}
|
||||
|
||||
public void show() {
|
||||
initDialog();
|
||||
initView();
|
||||
|
||||
@@ -60,6 +60,8 @@ public class TimerDialog extends BaseDialog implements Timer.Callback {
|
||||
binding.time2.setOnClickListener(this::setTimer);
|
||||
binding.time3.setOnClickListener(this::setTimer);
|
||||
binding.time4.setOnClickListener(this::setTimer);
|
||||
binding.time5.setOnClickListener(this::setTimer);
|
||||
binding.time6.setOnClickListener(this::setTimer);
|
||||
}
|
||||
|
||||
private void setTimer(View view) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.fongmi.android.tv.ui.dialog;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -27,12 +28,22 @@ public class UaDialog {
|
||||
return new UaDialog(fragment);
|
||||
}
|
||||
|
||||
public static UaDialog create(Activity activity) {
|
||||
return new UaDialog(activity);
|
||||
}
|
||||
|
||||
public UaDialog(Fragment fragment) {
|
||||
this.callback = (UaCallback) fragment;
|
||||
this.binding = DialogUaBinding.inflate(LayoutInflater.from(fragment.getContext()));
|
||||
this.append = true;
|
||||
}
|
||||
|
||||
public UaDialog(Activity activity) {
|
||||
this.callback = (UaCallback) activity;
|
||||
this.binding = DialogUaBinding.inflate(LayoutInflater.from(activity));
|
||||
this.append = true;
|
||||
}
|
||||
|
||||
public void show() {
|
||||
initDialog();
|
||||
initView();
|
||||
|
||||
@@ -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.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -31,16 +39,20 @@ import com.fongmi.android.tv.impl.ProxyCallback;
|
||||
import com.fongmi.android.tv.impl.SiteCallback;
|
||||
import com.fongmi.android.tv.player.Source;
|
||||
import com.fongmi.android.tv.ui.activity.HomeActivity;
|
||||
import com.fongmi.android.tv.ui.activity.SettingPlayerActivity;
|
||||
import com.fongmi.android.tv.ui.base.BaseFragment;
|
||||
import com.fongmi.android.tv.ui.dialog.AboutDialog;
|
||||
import com.fongmi.android.tv.ui.dialog.ConfigDialog;
|
||||
import com.fongmi.android.tv.ui.dialog.HistoryDialog;
|
||||
import com.fongmi.android.tv.ui.dialog.LiveDialog;
|
||||
import com.fongmi.android.tv.ui.dialog.ProxyDialog;
|
||||
import com.fongmi.android.tv.ui.dialog.RestoreDialog;
|
||||
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.FileUtil;
|
||||
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.UrlUtil;
|
||||
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.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.List;
|
||||
|
||||
@@ -91,25 +107,10 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
|
||||
|
||||
@Override
|
||||
protected void initView() {
|
||||
mBinding.vodUrl.setText(VodConfig.getDesc());
|
||||
mBinding.liveUrl.setText(LiveConfig.getDesc());
|
||||
mBinding.wallUrl.setText(WallConfig.getDesc());
|
||||
mBinding.versionText.setText(BuildConfig.VERSION_NAME);
|
||||
|
||||
// 设置开关的颜色为黄色
|
||||
int accentColor = getResources().getColor(R.color.accent);
|
||||
android.content.res.ColorStateList colorStateList = new android.content.res.ColorStateList(
|
||||
new int[][]{
|
||||
new int[]{-android.R.attr.state_checked},
|
||||
new int[]{android.R.attr.state_checked}
|
||||
},
|
||||
new int[]{
|
||||
0x66FFFFFF, // 未选中时的颜色
|
||||
accentColor // 选中时的颜色
|
||||
}
|
||||
);
|
||||
mBinding.incognitoSwitch.setThumbTintList(android.content.res.ColorStateList.valueOf(android.graphics.Color.WHITE));
|
||||
mBinding.incognitoSwitch.setTrackTintList(colorStateList);
|
||||
setSourceHintText(mBinding.vodUrl, VodConfig.getDesc(), R.string.source_hint_setting);
|
||||
setSourceHintText(mBinding.liveUrl, LiveConfig.getDesc(), R.string.source_hint_live);
|
||||
// setSourceHintText(mBinding.wallUrl, WallConfig.getDesc(), R.string.source_hint_wall); // 壁纸功能已移除
|
||||
mBinding.versionText.setText(getString(R.string.setting_version) + " " + BuildConfig.VERSION_NAME);
|
||||
|
||||
setOtherText();
|
||||
setCacheText();
|
||||
@@ -122,7 +123,53 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
|
||||
mBinding.dohText.setText(getDohList()[getDohIndex()]);
|
||||
mBinding.proxyText.setText(getProxy(Setting.getProxy()));
|
||||
mBinding.incognitoSwitch.setChecked(Setting.isIncognito());
|
||||
mBinding.liveTabVisibleSwitch.setChecked(Setting.isLiveTabVisible());
|
||||
mBinding.sizeText.setText((size = ResUtil.getStringArray(R.array.select_size))[Setting.getSize()]);
|
||||
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() {
|
||||
@@ -138,73 +185,121 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
|
||||
protected void initEvent() {
|
||||
mBinding.vod.setOnClickListener(this::onVod);
|
||||
mBinding.live.setOnClickListener(this::onLive);
|
||||
mBinding.wall.setOnClickListener(this::onWall);
|
||||
// mBinding.wall.setOnClickListener(this::onWall); // 壁纸功能已移除
|
||||
mBinding.proxy.setOnClickListener(this::onProxy);
|
||||
mBinding.cache.setOnClickListener(this::onCache);
|
||||
mBinding.backup.setOnClickListener(this::onBackup);
|
||||
mBinding.player.setOnClickListener(this::onPlayer);
|
||||
mBinding.restore.setOnClickListener(this::onRestore);
|
||||
mBinding.version.setOnClickListener(this::onVersion);
|
||||
mBinding.about.setOnClickListener(this::onAbout);
|
||||
mBinding.vod.setOnLongClickListener(this::onVodEdit);
|
||||
mBinding.vodHome.setOnClickListener(this::onVodHome);
|
||||
mBinding.live.setOnLongClickListener(this::onLiveEdit);
|
||||
mBinding.liveHome.setOnClickListener(this::onLiveHome);
|
||||
mBinding.wall.setOnLongClickListener(this::onWallEdit);
|
||||
// mBinding.wall.setOnLongClickListener(this::onWallEdit); // 壁纸功能已移除
|
||||
mBinding.vodHistory.setOnClickListener(this::onVodHistory);
|
||||
mBinding.version.setOnLongClickListener(this::onVersionDev);
|
||||
mBinding.liveHistory.setOnClickListener(this::onLiveHistory);
|
||||
mBinding.wallDefault.setOnClickListener(this::setWallDefault);
|
||||
mBinding.wallRefresh.setOnClickListener(this::setWallRefresh);
|
||||
mBinding.incognito.setOnClickListener(this::setIncognito);
|
||||
// mBinding.wallDefault.setOnClickListener(this::setWallDefault); // 壁纸功能已移除
|
||||
// mBinding.wallRefresh.setOnClickListener(this::setWallRefresh); // 壁纸功能已移除
|
||||
mBinding.incognitoSwitch.setOnClickListener(this::setIncognito);
|
||||
mBinding.liveTabVisibleSwitch.setOnClickListener(this::setLiveTabVisible);
|
||||
mBinding.size.setOnClickListener(this::setSize);
|
||||
mBinding.doh.setOnClickListener(this::setDoh);
|
||||
mBinding.webdav.setOnClickListener(this::onWebDAV);
|
||||
}
|
||||
|
||||
@Override
|
||||
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)) {
|
||||
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 {
|
||||
load(config);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void load(Config config) {
|
||||
// 再次检查Fragment状态,防止在异步回调中执行
|
||||
if (getActivity() == null || !isAdded() || isDetached()) return;
|
||||
|
||||
try {
|
||||
switch (config.getType()) {
|
||||
case 0:
|
||||
Notify.progress(getActivity());
|
||||
VodConfig.load(config, getCallback(0));
|
||||
if (mBinding != null && mBinding.vodUrl != null) {
|
||||
mBinding.vodUrl.setText(config.getDesc());
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
Notify.progress(getActivity());
|
||||
LiveConfig.load(config, getCallback(1));
|
||||
if (mBinding != null && mBinding.liveUrl != null) {
|
||||
mBinding.liveUrl.setText(config.getDesc());
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
Notify.progress(getActivity());
|
||||
WallConfig.load(config, getCallback(2));
|
||||
mBinding.wallUrl.setText(config.getDesc());
|
||||
// if (mBinding != null && mBinding.wallUrl != null) { // 壁纸功能已移除
|
||||
// mBinding.wallUrl.setText(config.getDesc());
|
||||
// }
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Notify.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
private Callback getCallback(int type) {
|
||||
return new Callback() {
|
||||
@Override
|
||||
public void success(String result) {
|
||||
// 检查Fragment是否还在活动状态
|
||||
if (getActivity() == null || !isAdded()) return;
|
||||
Notify.show(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void success() {
|
||||
// 检查Fragment是否还在活动状态
|
||||
if (getActivity() == null || !isAdded()) return;
|
||||
setConfig(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(String msg) {
|
||||
// 检查Fragment是否还在活动状态
|
||||
if (getActivity() == null || !isAdded()) return;
|
||||
Notify.show(msg);
|
||||
setConfig(type);
|
||||
Notify.dismiss();
|
||||
switch (type) {
|
||||
case 0:
|
||||
setSourceHintText(mBinding.vodUrl, VodConfig.getDesc(), R.string.source_hint_setting);
|
||||
break;
|
||||
case 1:
|
||||
setSourceHintText(mBinding.liveUrl, LiveConfig.getDesc(), R.string.source_hint_live);
|
||||
break;
|
||||
case 2:
|
||||
// setSourceHintText(mBinding.wallUrl, WallConfig.getDesc(), R.string.source_hint_wall); // 壁纸功能已移除
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -216,24 +311,37 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
|
||||
Notify.dismiss();
|
||||
RefreshEvent.video();
|
||||
RefreshEvent.config();
|
||||
mBinding.vodUrl.setText(VodConfig.getDesc());
|
||||
mBinding.liveUrl.setText(LiveConfig.getDesc());
|
||||
mBinding.wallUrl.setText(WallConfig.getDesc());
|
||||
setSourceHintText(mBinding.vodUrl, VodConfig.getDesc(), R.string.source_hint_setting);
|
||||
setSourceHintText(mBinding.liveUrl, LiveConfig.getDesc(), R.string.source_hint_live);
|
||||
// setSourceHintText(mBinding.wallUrl, WallConfig.getDesc(), R.string.source_hint_wall); // 壁纸功能已移除
|
||||
break;
|
||||
case 1:
|
||||
setCacheText();
|
||||
Notify.dismiss();
|
||||
RefreshEvent.config();
|
||||
mBinding.liveUrl.setText(LiveConfig.getDesc());
|
||||
setSourceHintText(mBinding.liveUrl, LiveConfig.getDesc(), R.string.source_hint_live);
|
||||
break;
|
||||
case 2:
|
||||
setCacheText();
|
||||
Notify.dismiss();
|
||||
mBinding.wallUrl.setText(WallConfig.getDesc());
|
||||
// setSourceHintText(mBinding.wallUrl, WallConfig.getDesc(), R.string.source_hint_wall); // 壁纸功能已移除
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void setSourceHintText(TextView textView, String desc, int hintStringRes) {
|
||||
if (TextUtils.isEmpty(desc)) {
|
||||
SpannableString spannable = new SpannableString(getString(hintStringRes));
|
||||
spannable.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.white)), 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
spannable.setSpan(new RelativeSizeSpan(0.8f), 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
int alpha = (int)(255 * 0.5f);
|
||||
spannable.setSpan(new ForegroundColorSpan(android.graphics.Color.argb(alpha, 255, 255, 255)), 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
textView.setText(spannable);
|
||||
} else {
|
||||
textView.setText(desc);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSite(Site item) {
|
||||
VodConfig.get().setHome(item);
|
||||
@@ -293,13 +401,17 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
|
||||
}
|
||||
|
||||
private void onPlayer(View view) {
|
||||
getRoot().change(2);
|
||||
SettingPlayerActivity.start(requireActivity());
|
||||
}
|
||||
|
||||
private void onVersion(View view) {
|
||||
Updater.create().force().release().start(getActivity());
|
||||
}
|
||||
|
||||
private void onAbout(View view) {
|
||||
AboutDialog.show(this);
|
||||
}
|
||||
|
||||
private boolean onVersionDev(View view) {
|
||||
Updater.create().force().dev().start(getActivity());
|
||||
return true;
|
||||
@@ -323,7 +435,17 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
|
||||
private void setIncognito(View view) {
|
||||
boolean isChecked = !Setting.isIncognito();
|
||||
Setting.putIncognito(isChecked);
|
||||
mBinding.incognitoSwitch.setChecked(isChecked);
|
||||
// 不需要再次调用 setChecked,因为点击已经触发了状态变化
|
||||
}
|
||||
|
||||
private void setLiveTabVisible(View view) {
|
||||
boolean isChecked = !Setting.isLiveTabVisible();
|
||||
Setting.putLiveTabVisible(isChecked);
|
||||
// 发送刷新事件,通知主界面更新导航栏
|
||||
RefreshEvent.config();
|
||||
// 更新直播设置项的可见性
|
||||
setLiveSettingsVisibility();
|
||||
// 不需要再次调用 setChecked,因为点击已经触发了状态变化
|
||||
}
|
||||
|
||||
private void setSize(View view) {
|
||||
@@ -406,19 +528,43 @@ public class SettingFragment extends BaseFragment implements ConfigCallback, Sit
|
||||
}));
|
||||
}
|
||||
|
||||
private void onWebDAV(View view) {
|
||||
WebDAVDialog.create(this).show();
|
||||
}
|
||||
|
||||
private void initConfig() {
|
||||
WallConfig.get().init();
|
||||
LiveConfig.get().init().load();
|
||||
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
|
||||
public void onHiddenChanged(boolean hidden) {
|
||||
if (hidden) return;
|
||||
mBinding.vodUrl.setText(VodConfig.getDesc());
|
||||
mBinding.liveUrl.setText(LiveConfig.getDesc());
|
||||
mBinding.wallUrl.setText(WallConfig.getDesc());
|
||||
setSourceHintText(mBinding.vodUrl, VodConfig.getDesc(), R.string.source_hint_setting);
|
||||
setSourceHintText(mBinding.liveUrl, LiveConfig.getDesc(), R.string.source_hint_live);
|
||||
// setSourceHintText(mBinding.wallUrl, WallConfig.getDesc(), R.string.source_hint_wall); // 壁纸功能已移除
|
||||
setCacheText();
|
||||
setWebDAVStatus(); // 更新WebDAV状态
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -26,6 +26,8 @@ import com.fongmi.android.tv.R;
|
||||
import com.fongmi.android.tv.Setting;
|
||||
import com.fongmi.android.tv.api.config.VodConfig;
|
||||
import com.fongmi.android.tv.bean.Class;
|
||||
import com.fongmi.android.tv.bean.Config;
|
||||
import com.fongmi.android.tv.bean.History;
|
||||
import com.fongmi.android.tv.bean.Hot;
|
||||
import com.fongmi.android.tv.bean.Result;
|
||||
import com.fongmi.android.tv.bean.Site;
|
||||
@@ -35,6 +37,7 @@ import com.fongmi.android.tv.event.CastEvent;
|
||||
import com.fongmi.android.tv.event.RefreshEvent;
|
||||
import com.fongmi.android.tv.event.StateEvent;
|
||||
import com.fongmi.android.tv.impl.Callback;
|
||||
import com.fongmi.android.tv.impl.ConfigCallback;
|
||||
import com.fongmi.android.tv.impl.FilterCallback;
|
||||
import com.fongmi.android.tv.impl.SiteCallback;
|
||||
import com.fongmi.android.tv.model.SiteViewModel;
|
||||
@@ -42,13 +45,17 @@ import com.fongmi.android.tv.ui.activity.CollectActivity;
|
||||
import com.fongmi.android.tv.ui.activity.HistoryActivity;
|
||||
import com.fongmi.android.tv.ui.activity.KeepActivity;
|
||||
import com.fongmi.android.tv.ui.activity.VideoActivity;
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
import com.fongmi.android.tv.ui.adapter.TypeAdapter;
|
||||
import com.fongmi.android.tv.ui.base.BaseFragment;
|
||||
import com.fongmi.android.tv.ui.dialog.ConfigDialog;
|
||||
import com.fongmi.android.tv.ui.dialog.FilterDialog;
|
||||
import com.fongmi.android.tv.ui.dialog.LastWatchToast;
|
||||
import com.fongmi.android.tv.ui.dialog.LinkDialog;
|
||||
import com.fongmi.android.tv.ui.dialog.ReceiveDialog;
|
||||
import com.fongmi.android.tv.ui.dialog.SiteDialog;
|
||||
import com.fongmi.android.tv.utils.FileChooser;
|
||||
import com.fongmi.android.tv.utils.Notify;
|
||||
import com.fongmi.android.tv.utils.ResUtil;
|
||||
import com.fongmi.android.tv.utils.UrlUtil;
|
||||
import com.github.catvod.net.OkHttp;
|
||||
@@ -68,7 +75,7 @@ import okhttp3.Call;
|
||||
import okhttp3.Headers;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class VodFragment extends BaseFragment implements SiteCallback, FilterCallback, TypeAdapter.OnClickListener {
|
||||
public class VodFragment extends BaseFragment implements SiteCallback, FilterCallback, TypeAdapter.OnClickListener, ConfigCallback {
|
||||
|
||||
private FragmentVodBinding mBinding;
|
||||
private SiteViewModel mViewModel;
|
||||
@@ -99,10 +106,37 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
|
||||
EventBus.getDefault().register(this);
|
||||
setRecyclerView();
|
||||
setViewModel();
|
||||
showProgress();
|
||||
initStartupState(); // 根据是否已有配置来设置初始状态
|
||||
setLogo();
|
||||
initHot();
|
||||
getHot();
|
||||
// 检查是否需要显示上次播放弹窗
|
||||
checkLastWatchDialog();
|
||||
}
|
||||
|
||||
// 初始化启动状态:区分已有配置和无配置的情况
|
||||
private void initStartupState() {
|
||||
// 检查是否已经有保存的配置,添加空值检查
|
||||
boolean hasExistingConfig = false;
|
||||
try {
|
||||
Config config = VodConfig.get().getConfig();
|
||||
hasExistingConfig = config != null &&
|
||||
config.getUrl() != null &&
|
||||
!config.getUrl().isEmpty();
|
||||
} catch (Exception e) {
|
||||
// 如果获取配置时出错,认为没有配置
|
||||
hasExistingConfig = false;
|
||||
}
|
||||
|
||||
if (hasExistingConfig) {
|
||||
// 已有配置:显示加载状态,确保不显示添加源提示
|
||||
showProgress();
|
||||
mBinding.emptySourceHint.setVisibility(View.GONE);
|
||||
} else {
|
||||
// 无配置:立即显示空源提示,不显示加载状态
|
||||
hideProgress();
|
||||
checkEmptySource();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -127,6 +161,23 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
|
||||
});
|
||||
}
|
||||
|
||||
// 添加检查上次播放历史并显示弹窗的方法
|
||||
private void checkLastWatchDialog() {
|
||||
if (App.isAppJustLaunched()) {
|
||||
List<History> histories = History.get();
|
||||
if (!histories.isEmpty()) {
|
||||
App.setAppLaunched();
|
||||
App.post(() -> {
|
||||
if (getActivity() != null) {
|
||||
LastWatchToast.create(getActivity(), histories.get(0)).show();
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
App.setAppLaunched();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setRecyclerView() {
|
||||
mBinding.type.setHasFixedSize(true);
|
||||
mBinding.type.setItemAnimator(null);
|
||||
@@ -173,10 +224,179 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
|
||||
setFabVisible(0);
|
||||
hideProgress();
|
||||
checkRetry();
|
||||
checkEmptySource(); // 添加检查是否显示空源提示
|
||||
}
|
||||
|
||||
// 修改checkEmptySource方法,增强鲁棒性
|
||||
private void checkEmptySource() {
|
||||
// 检查是否有基础配置文件,添加空值检查
|
||||
boolean hasBaseConfig = false;
|
||||
try {
|
||||
Config config = VodConfig.get().getConfig();
|
||||
hasBaseConfig = config != null &&
|
||||
config.getUrl() != null &&
|
||||
!config.getUrl().isEmpty();
|
||||
} catch (Exception e) {
|
||||
hasBaseConfig = false;
|
||||
}
|
||||
|
||||
// 检查是否有有效的站点配置
|
||||
boolean hasValidSites = false;
|
||||
boolean hasValidHome = false;
|
||||
try {
|
||||
hasValidSites = VodConfig.get().getSites().size() > 0;
|
||||
Site site = getSite();
|
||||
hasValidHome = site != null && site.getKey() != null && !site.getKey().isEmpty();
|
||||
} catch (Exception e) {
|
||||
hasValidSites = false;
|
||||
hasValidHome = false;
|
||||
}
|
||||
|
||||
// 只有在完全没有配置文件或配置文件无效时才显示空源提示
|
||||
boolean isEmpty = !hasBaseConfig || (!hasValidSites || !hasValidHome);
|
||||
|
||||
if (mBinding.emptySourceHint != null) {
|
||||
mBinding.emptySourceHint.setVisibility(isEmpty ? View.VISIBLE : View.GONE);
|
||||
if (isEmpty) {
|
||||
// 设置整个布局的点击事件
|
||||
mBinding.emptySourceHint.setOnClickListener(this::onAddSource);
|
||||
// 设置按钮的点击事件
|
||||
if (mBinding.addSourceBtn != null) {
|
||||
mBinding.addSourceBtn.setOnClickListener(this::onAddSource);
|
||||
}
|
||||
// 空源状态下隐藏所有悬浮按钮
|
||||
hideFabButtons();
|
||||
// 启动Lottie动画
|
||||
try {
|
||||
LottieAnimationView lottieView = mBinding.emptySourceHint.findViewById(R.id.lottieAnimation);
|
||||
if (lottieView != null) {
|
||||
lottieView.playAnimation();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加源按钮点击事件处理
|
||||
private void onAddSource(View view) {
|
||||
ConfigDialog.create(this).type(0).show();
|
||||
}
|
||||
|
||||
// 实现ConfigCallback接口
|
||||
@Override
|
||||
public void setConfig(Config config) {
|
||||
android.util.Log.d("VodFragment", "setConfig called with: " + (config != null ? config.toString() : "null"));
|
||||
|
||||
if (config == null || config.isEmpty()) {
|
||||
android.util.Log.d("VodFragment", "Config is null or empty, returning");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查Fragment是否还在活动状态,增强检查
|
||||
if (!isValidFragmentState()) {
|
||||
android.util.Log.d("VodFragment", "Fragment state invalid, returning");
|
||||
return;
|
||||
}
|
||||
|
||||
android.util.Log.d("VodFragment", "Fragment state valid, proceeding with config load");
|
||||
|
||||
// 安全地隐藏空源提示
|
||||
try {
|
||||
if (mBinding != null && mBinding.emptySourceHint != null) {
|
||||
mBinding.emptySourceHint.setVisibility(View.GONE);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
Notify.progress(getActivity());
|
||||
android.util.Log.d("VodFragment", "Calling VodConfig.load");
|
||||
VodConfig.load(config, new Callback() {
|
||||
@Override
|
||||
public void success() {
|
||||
android.util.Log.d("VodFragment", "VodConfig.load success callback");
|
||||
// 双重检查Fragment是否还在活动状态
|
||||
if (!isValidFragmentState()) {
|
||||
android.util.Log.d("VodFragment", "Fragment state invalid in success callback");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
android.util.Log.d("VodFragment", "Success: dismissing notify and refreshing");
|
||||
Notify.dismiss();
|
||||
RefreshEvent.config();
|
||||
RefreshEvent.video();
|
||||
homeContent();
|
||||
} catch (Exception e) {
|
||||
android.util.Log.e("VodFragment", "Error in success callback", e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(String msg) {
|
||||
android.util.Log.e("VodFragment", "VodConfig.load error: " + msg);
|
||||
// 双重检查Fragment是否还在活动状态
|
||||
if (!isValidFragmentState()) {
|
||||
android.util.Log.d("VodFragment", "Fragment state invalid in error callback");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Notify.dismiss();
|
||||
Notify.show(msg);
|
||||
// 加载失败时重新显示空源提示
|
||||
checkEmptySource();
|
||||
} catch (Exception e) {
|
||||
android.util.Log.e("VodFragment", "Error in error callback", e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 添加Fragment状态检查方法
|
||||
private boolean isValidFragmentState() {
|
||||
return getActivity() != null &&
|
||||
!getActivity().isFinishing() &&
|
||||
!getActivity().isDestroyed() &&
|
||||
isAdded() &&
|
||||
!isDetached() &&
|
||||
!isRemoving() &&
|
||||
getView() != null &&
|
||||
mBinding != null;
|
||||
}
|
||||
|
||||
private void setFabVisible(int position) {
|
||||
if (mAdapter.getItemCount() == 0) {
|
||||
// 检查是否为空源状态 - 使用与checkEmptySource相同的逻辑,添加空值检查
|
||||
boolean hasBaseConfig = false;
|
||||
boolean hasValidSites = false;
|
||||
boolean hasValidHome = false;
|
||||
|
||||
try {
|
||||
Config config = VodConfig.get().getConfig();
|
||||
hasBaseConfig = config != null &&
|
||||
config.getUrl() != null &&
|
||||
!config.getUrl().isEmpty();
|
||||
|
||||
hasValidSites = VodConfig.get().getSites().size() > 0;
|
||||
|
||||
Site site = getSite();
|
||||
hasValidHome = site != null && site.getKey() != null && !site.getKey().isEmpty();
|
||||
} catch (Exception e) {
|
||||
hasBaseConfig = false;
|
||||
hasValidSites = false;
|
||||
hasValidHome = false;
|
||||
}
|
||||
|
||||
boolean isEmpty = !hasBaseConfig || (!hasValidSites || !hasValidHome);
|
||||
|
||||
if (isEmpty) {
|
||||
// 空源状态下隐藏所有悬浮按钮
|
||||
hideFabButtons();
|
||||
} else if (mAdapter.getItemCount() == 0) {
|
||||
mBinding.top.setVisibility(View.INVISIBLE);
|
||||
mBinding.link.setVisibility(View.VISIBLE);
|
||||
mBinding.filter.setVisibility(View.GONE);
|
||||
@@ -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() {
|
||||
mBinding.retry.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
@@ -247,6 +474,14 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
|
||||
private void homeContent() {
|
||||
showProgress();
|
||||
setFabVisible(0);
|
||||
// 安全地隐藏空源提示
|
||||
try {
|
||||
if (mBinding != null && mBinding.emptySourceHint != null) {
|
||||
mBinding.emptySourceHint.setVisibility(View.GONE);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
mAdapter.clear();
|
||||
mViewModel.homeContent();
|
||||
mBinding.pager.setAdapter(new PageAdapter(getChildFragmentManager()));
|
||||
@@ -296,6 +531,7 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
|
||||
switch (event.getType()) {
|
||||
case EMPTY:
|
||||
hideProgress();
|
||||
checkEmptySource(); // 添加检查是否显示空源提示
|
||||
break;
|
||||
case PROGRESS:
|
||||
showProgress();
|
||||
|
||||
@@ -59,7 +59,7 @@ public class Timer {
|
||||
|
||||
public void delay() {
|
||||
cancel();
|
||||
set(TimeUnit.MINUTES.toMillis(5) + tick);
|
||||
set(TimeUnit.MINUTES.toMillis(15) + tick);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/white" android:state_checked="true" />
|
||||
<item android:color="@color/black" android:state_checked="true" />
|
||||
<item android:color="@color/white" android:state_checked="false" />
|
||||
</selector>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/white" android:state_focused="true" android:state_selected="true" />
|
||||
<item android:color="@color/green_400" android:state_selected="true" />
|
||||
<item android:color="@color/primary" android:state_selected="true" />
|
||||
<item android:color="@color/white" />
|
||||
</selector>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/white" android:state_checked="true" />
|
||||
<item android:color="@color/black" android:state_checked="true" />
|
||||
<item android:color="@color/white" android:state_checked="false" />
|
||||
</selector>
|
||||
|
Before Width: | Height: | Size: 703 KiB |
|
Before Width: | Height: | Size: 890 KiB |
|
Before Width: | Height: | Size: 869 KiB |
|
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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,10 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M438,617L613,503Q627,494 627,478Q627,462 613,453L438,339Q423,329 407.5,337.37Q392,345.74 392,364L392,592Q392,610.26 407.5,618.63Q423,627 438,617ZM140,800Q116,800 98,782Q80,764 80,740L80,220Q80,196 98,178Q116,160 140,160L820,160Q844.75,160 862.38,178Q880,196 880,220L880,740Q880,764 862.38,782Q844.75,800 820,800L140,800ZM140,740L140,740Q140,740 140,740Q140,740 140,740L140,220Q140,220 140,220Q140,220 140,220L140,220Q140,220 140,220Q140,220 140,220L140,740Q140,740 140,740Q140,740 140,740ZM140,740L820,740Q820,740 820,740Q820,740 820,740L820,220Q820,220 820,220Q820,220 820,220L140,220Q140,220 140,220Q140,220 140,220L140,740Q140,740 140,740Q140,740 140,740Z" />
|
||||
android:fillColor="#FF000000"
|
||||
android:fillAlpha="0"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeColor="#FF000000"
|
||||
android:pathData="M3,6C3,4.89543 3.89543,4 5,4H19C20.1046,4 21,4.89543 21,6V18C21,19.1046 20.1046,20 19,20H5C3.89543,20 3,19.1046 3,18V6Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:fillAlpha="0"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeColor="#FF000000"
|
||||
android:pathData="M10,9l5,3l-5,3l0,-6z"/>
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeColor="#FF000000"
|
||||
android:pathData="M3,6C3,4.89543 3.89543,4 5,4H19C20.1046,4 21,4.89543 21,6V18C21,19.1046 20.1046,20 19,20H5C3.89543,20 3,19.1046 3,18V6Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeColor="#FF000000"
|
||||
android:pathData="M10,9l5,3l-5,3l0,-6z"/>
|
||||
</vector>
|
||||
@@ -1,10 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M546,880L414,880Q403,880 394.5,873Q386,866 384,855L368,754Q349,747 328,735Q307,723 291,710L198,753Q187,758 176,754.5Q165,751 159,740L93,623Q87,613 90,602Q93,591 102,584L188,521Q186,512 185.5,500.5Q185,489 185,480Q185,471 185.5,459.5Q186,448 188,439L102,376Q93,369 90,358Q87,347 93,337L159,220Q165,209 176,205.5Q187,202 198,207L291,250Q307,237 328,225Q349,213 368,207L384,105Q386,94 394.5,87Q403,80 414,80L546,80Q557,80 565.5,87Q574,94 576,105L592,206Q611,213 632.5,224.5Q654,236 669,250L762,207Q773,202 784,205.5Q795,209 801,220L867,336Q873,346 870.5,357.5Q868,369 858,376L772,437Q774,447 774.5,458.5Q775,470 775,480Q775,490 774.5,501Q774,512 772,522L858,584Q867,591 870,602Q873,613 867,623L801,740Q795,751 784,754.5Q773,758 762,753L669,710Q653,723 632.5,735.5Q612,748 592,754L576,855Q574,866 565.5,873Q557,880 546,880ZM480,610Q534,610 572,572Q610,534 610,480Q610,426 572,388Q534,350 480,350Q426,350 388,388Q350,426 350,480Q350,534 388,572Q426,610 480,610ZM480,550Q451,550 430.5,529.5Q410,509 410,480Q410,451 430.5,430.5Q451,410 480,410Q509,410 529.5,430.5Q550,451 550,480Q550,509 529.5,529.5Q509,550 480,550ZM480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480ZM436,820L524,820L538,708Q571,700 600.5,683Q630,666 654,642L760,688L800,616L706,547Q710,530 712.5,513.5Q715,497 715,480Q715,463 713,446.5Q711,430 706,413L800,344L760,272L654,318Q631,292 602,274.5Q573,257 538,252L524,140L436,140L422,252Q388,259 358.5,276Q329,293 306,318L200,272L160,344L254,413Q250,430 247.5,446.5Q245,463 245,480Q245,497 247.5,513.5Q250,530 254,547L160,616L200,688L306,642Q330,666 359.5,683Q389,700 422,708L436,820Z" />
|
||||
android:fillColor="#FF000000"
|
||||
android:fillAlpha="0"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeColor="#FF000000"
|
||||
android:pathData="M12,12m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:fillAlpha="0"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeColor="#FF000000"
|
||||
android:pathData="M19.14,12.94C19.18,12.64 19.2,12.32 19.2,12C19.2,11.68 19.18,11.36 19.14,11.06L21.16,9.48C21.34,9.34 21.4,9.08 21.28,8.88L19.36,5.52C19.24,5.32 18.98,5.24 18.76,5.32L16.36,6.34C15.84,5.94 15.28,5.62 14.66,5.38L14.3,2.8C14.26,2.58 14.08,2.4 13.84,2.4H10.16C9.92,2.4 9.74,2.58 9.7,2.8L9.34,5.38C8.72,5.62 8.16,5.94 7.64,6.34L5.24,5.32C5.02,5.24 4.76,5.32 4.64,5.52L2.72,8.88C2.6,9.08 2.66,9.34 2.84,9.48L4.86,11.06C4.82,11.36 4.8,11.68 4.8,12C4.8,12.32 4.82,12.64 4.86,12.94L2.84,14.52C2.66,14.66 2.6,14.92 2.72,15.12L4.64,18.48C4.76,18.68 5.02,18.76 5.24,18.68L7.64,17.66C8.16,18.06 8.72,18.38 9.34,18.62L9.7,21.2C9.74,21.42 9.92,21.6 10.16,21.6H13.84C14.08,21.6 14.26,21.42 14.3,21.2L14.66,18.62C15.28,18.38 15.84,18.06 16.36,17.66L18.76,18.68C18.98,18.76 19.24,18.68 19.36,18.48L21.28,15.12C21.4,14.92 21.34,14.66 21.16,14.52L19.14,12.94Z"/>
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeColor="#FF000000"
|
||||
android:pathData="M12,12m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeColor="#FF000000"
|
||||
android:pathData="M19.14,12.94C19.18,12.64 19.2,12.32 19.2,12C19.2,11.68 19.18,11.36 19.14,11.06L21.16,9.48C21.34,9.34 21.4,9.08 21.28,8.88L19.36,5.52C19.24,5.32 18.98,5.24 18.76,5.32L16.36,6.34C15.84,5.94 15.28,5.62 14.66,5.38L14.3,2.8C14.26,2.58 14.08,2.4 13.84,2.4H10.16C9.92,2.4 9.74,2.58 9.7,2.8L9.34,5.38C8.72,5.62 8.16,5.94 7.64,6.34L5.24,5.32C5.02,5.24 4.76,5.32 4.64,5.52L2.72,8.88C2.6,9.08 2.66,9.34 2.84,9.48L4.86,11.06C4.82,11.36 4.8,11.68 4.8,12C4.8,12.32 4.82,12.64 4.86,12.94L2.84,14.52C2.66,14.66 2.6,14.92 2.72,15.12L4.64,18.48C4.76,18.68 5.02,18.76 5.24,18.68L7.64,17.66C8.16,18.06 8.72,18.38 9.34,18.62L9.7,21.2C9.74,21.42 9.92,21.6 10.16,21.6H13.84C14.08,21.6 14.26,21.42 14.3,21.2L14.66,18.62C15.28,18.38 15.84,18.06 16.36,17.66L18.76,18.68C18.98,18.76 19.24,18.68 19.36,18.48L21.28,15.12C21.4,14.92 21.34,14.66 21.16,14.52L19.14,12.94Z"/>
|
||||
</vector>
|
||||
@@ -1,7 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M140,160L214,312L344,312L270,160L359,160L433,312L563,312L489,160L578,160L652,312L782,312L708,160L820,160Q844,160 862,178Q880,196 880,220L880,740Q880,764 862,782Q844,800 820,800L140,800Q116,800 98,782Q80,764 80,740L80,220Q80,196 98,178Q116,160 140,160L140,160ZM140,372L140,740Q140,740 140,740Q140,740 140,740L820,740Q820,740 820,740Q820,740 820,740L820,372L140,372ZM140,372L140,372L140,740Q140,740 140,740Q140,740 140,740L140,740Q140,740 140,740Q140,740 140,740L140,372Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M20,3H4C2.9,3 2,3.9 2,5v14c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V5C22,3.9 21.1,3 20,3zM9,17H7v-5h2V17zM13,17h-2V7h2V17zM17,17h-2v-9h2V17z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4zM12,11.99h7c-0.53,4.12 -3.28,7.79 -7,8.94V12H5V6.3l7,-3.11v8.8z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,17v2h6v-2H3zM3,5v2h10V5H3zM13,21v-2h8v-2h-8v-2h-2v6H13zM7,9v2H3v2h4v2h2V9H7zM21,13v-2H11v2H21zM15,9h2V7h4V5h-4V3h-2V9z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,880Q346,880 253,787Q160,694 160,560L160,360Q160,238 256,159Q352,80 480,80Q608,80 704,159Q800,238 800,360L800,880L480,880ZM480,800L560,800Q541,775 530.5,744.5Q520,714 520,680L520,638Q510,639 500,639.5Q490,640 480,640Q413,640 350.5,616.5Q288,593 240,545L240,560Q240,660 310,730Q380,800 480,800ZM600,680Q600,730 635,765Q670,800 720,800L720,545Q694,571 664,589.5Q634,608 600,620L600,680ZM440,400Q440,334 395,289Q350,244 286,241Q264,265 252,295Q240,325 240,360Q240,449 312.5,504.5Q385,560 480,560Q575,560 647.5,504.5Q720,449 720,360Q720,325 708,294.5Q696,264 674,240Q610,242 565,288Q520,334 520,400L440,400ZM340,400Q323,400 311.5,388.5Q300,377 300,360Q300,343 311.5,331.5Q323,320 340,320Q357,320 368.5,331.5Q380,343 380,360Q380,377 368.5,388.5Q357,400 340,400ZM620,400Q603,400 591.5,388.5Q580,377 580,360Q580,343 591.5,331.5Q603,320 620,320Q637,320 648.5,331.5Q660,343 660,360Q660,377 648.5,388.5Q637,400 620,400ZM370,182Q404,196 432,219Q460,242 480,271Q500,242 527.5,219Q555,196 589,182Q564,171 536.5,165.5Q509,160 480,160Q451,160 423.5,165.5Q396,171 370,182ZM800,800L800,800L720,800Q670,800 635,800Q600,800 600,800L600,800Q580,800 560,800Q540,800 520,800L520,800Q520,800 578.5,800Q637,800 720,800L800,800ZM480,800Q380,800 310,730Q240,660 240,560L240,560Q240,660 310,730Q380,800 480,800Q540,800 550,800Q560,800 560,800L560,800Q560,800 560,800Q560,800 560,800L480,800ZM600,680L600,680Q600,730 635,765Q670,800 720,800L720,800Q670,800 635,765Q600,730 600,680ZM480,271Q480,271 480,271Q480,271 480,271Q480,271 480,271Q480,271 480,271Q480,271 480,271Q480,271 480,271Q480,271 480,271Q480,271 480,271Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M508,760L732,760Q725,786 708,802Q691,818 664,822L228,875Q195,880 168.5,859.5Q142,839 138,806L85,369Q81,336 101,310Q121,284 154,280L200,274L200,354L164,359Q164,359 164,359Q164,359 164,359L218,796Q218,796 218,796Q218,796 218,796L508,760ZM360,680Q327,680 303.5,656.5Q280,633 280,600L280,160Q280,127 303.5,103.5Q327,80 360,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,600Q880,633 856.5,656.5Q833,680 800,680L360,680ZM360,600L800,600Q800,600 800,600Q800,600 800,600L800,160Q800,160 800,160Q800,160 800,160L360,160Q360,160 360,160Q360,160 360,160L360,600Q360,600 360,600Q360,600 360,600ZM580,380Q580,380 580,380Q580,380 580,380L580,380Q580,380 580,380Q580,380 580,380L580,380Q580,380 580,380Q580,380 580,380L580,380Q580,380 580,380Q580,380 580,380ZM218,796L218,796L218,796L218,796L218,796Q218,796 218,796Q218,796 218,796ZM581,560Q649,560 696.5,513Q744,466 749,400Q681,400 632.5,447Q584,494 581,560ZM581,560Q578,494 529.5,447Q481,400 413,400Q418,466 465.5,513Q513,560 581,560ZM581,440Q598,440 609.5,428.5Q621,417 621,400L621,390L631,394Q646,400 661.5,397Q677,394 685,380Q694,365 691,348Q688,331 671,324L661,320L671,316Q688,309 690.5,291.5Q693,274 685,260Q676,245 661,242.5Q646,240 631,246L621,250L621,240Q621,223 609.5,211.5Q598,200 581,200Q564,200 552.5,211.5Q541,223 541,240L541,250L531,246Q516,240 501,242.5Q486,245 477,260Q469,274 471.5,291.5Q474,309 491,316L501,320L491,324Q474,331 471,348Q468,365 477,380Q485,394 500.5,397Q516,400 531,394L541,390L541,400Q541,417 552.5,428.5Q564,440 581,440ZM581,360Q564,360 552.5,348.5Q541,337 541,320Q541,303 552.5,291.5Q564,280 581,280Q598,280 609.5,291.5Q621,303 621,320Q621,337 609.5,348.5Q598,360 581,360Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M342,800L618,800Q618,800 618,800Q618,800 618,800L658,640L302,640L342,800Q342,800 342,800Q342,800 342,800ZM342,880Q314,880 293,863Q272,846 265,819L220,640L740,640L695,819Q688,846 667,863Q646,880 618,880L342,880ZM200,560L760,560Q760,560 760,560Q760,560 760,560L760,480L200,480L200,560Q200,560 200,560Q200,560 200,560ZM480,320Q480,220 550,150Q620,80 720,80Q720,170 663,236Q606,302 520,316L520,400L840,400L840,560Q840,593 816.5,616.5Q793,640 760,640L200,640Q167,640 143.5,616.5Q120,593 120,560L120,400L440,400L440,316Q354,302 297,236Q240,170 240,80Q340,80 410,150Q480,220 480,320Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M280,680L280,280L680,280L680,680L280,680ZM360,600L600,600L600,360L360,360L360,600ZM200,760L200,840Q167,840 143.5,816.5Q120,793 120,760L200,760ZM120,680L120,600L200,600L200,680L120,680ZM120,520L120,440L200,440L200,520L120,520ZM120,360L120,280L200,280L200,360L120,360ZM200,200L120,200Q120,167 143.5,143.5Q167,120 200,120L200,200ZM280,840L280,760L360,760L360,840L280,840ZM280,200L280,120L360,120L360,200L280,200ZM440,840L440,760L520,760L520,840L440,840ZM440,200L440,120L520,120L520,200L440,200ZM600,840L600,760L680,760L680,840L600,840ZM600,200L600,120L680,120L680,200L600,200ZM760,840L760,760L840,760Q840,793 816.5,816.5Q793,840 760,840ZM760,680L760,600L840,600L840,680L760,680ZM760,520L760,440L840,440L840,520L760,520ZM760,360L760,280L840,280L840,360L760,360ZM760,200L760,120Q793,120 816.5,143.5Q840,167 840,200L760,200Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/ic_nav_live_selected" android:state_checked="true" />
|
||||
<item android:drawable="@drawable/ic_nav_live" android:state_checked="false" />
|
||||
</selector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/ic_nav_setting_selected" android:state_checked="true" />
|
||||
<item android:drawable="@drawable/ic_nav_setting" android:state_checked="false" />
|
||||
</selector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/ic_nav_vod_selected" android:state_checked="true" />
|
||||
<item android:drawable="@drawable/ic_nav_vod" android:state_checked="false" />
|
||||
</selector>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="?attr/colorControlHighlight">
|
||||
<item android:id="@android:id/background">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/primary" />
|
||||
<corners android:radius="8dp" />
|
||||
<padding
|
||||
android:bottom="14dp"
|
||||
android:left="12dp"
|
||||
android:right="12dp"
|
||||
android:top="14dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="14dp" />
|
||||
<solid android:color="#212121" />
|
||||
</shape>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="?attr/colorControlHighlight">
|
||||
<item android:id="@android:id/mask">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="8dp" />
|
||||
<solid android:color="#f5f5f5" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/black_20" />
|
||||
<corners android:radius="8dp" />
|
||||
<padding
|
||||
android:bottom="8dp"
|
||||
android:left="12dp"
|
||||
android:right="12dp"
|
||||
android:top="8dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="?attr/colorControlHighlight">
|
||||
<item android:id="@android:id/background">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@android:color/transparent" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="#802196F3">
|
||||
android:color="#80FFEB3B">
|
||||
<item android:id="@android:id/mask">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#f5f5f5" />
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
<item android:id="@android:id/background">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/black_20" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#BDBDBD" />
|
||||
<corners android:radius="8dp" />
|
||||
<padding
|
||||
android:bottom="14dp"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
android:color="?attr/colorControlHighlight">
|
||||
<item android:id="@android:id/background">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/yellow_500" />
|
||||
<solid android:color="@color/primary" />
|
||||
<corners android:radius="8dp" />
|
||||
<padding
|
||||
android:bottom="10dp"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
android:color="?attr/colorControlHighlight">
|
||||
<item android:id="@android:id/background">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/yellow_500" />
|
||||
<solid android:color="@color/primary" />
|
||||
<corners android:radius="8dp" />
|
||||
<padding
|
||||
android:bottom="18dp"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="#8066BB6A">
|
||||
android:color="#80FFEB3B">
|
||||
<item android:id="@android:id/mask">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#f5f5f5" />
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<item android:id="@android:id/background">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/black_20" />
|
||||
<corners android:radius="8dp" />
|
||||
<corners android:radius="12dp" />
|
||||
<padding
|
||||
android:bottom="14dp"
|
||||
android:left="16dp"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
android:color="?attr/colorControlHighlight">
|
||||
<item android:id="@android:id/background">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/yellow_500" />
|
||||
<solid android:color="@color/primary" />
|
||||
<corners android:radius="8dp" />
|
||||
<padding
|
||||
android:bottom="18dp"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="14dp" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/white_10" />
|
||||
</shape>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="?attr/colorControlHighlight">
|
||||
<item android:id="@android:id/background">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/primary" />
|
||||
<corners android:radius="8dp" />
|
||||
<padding
|
||||
android:bottom="12dp"
|
||||
android:left="16dp"
|
||||
android:right="16dp"
|
||||
android:top="12dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="?attr/colorControlHighlight">
|
||||
<item android:id="@android:id/background">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/black_20" />
|
||||
<corners android:radius="8dp" />
|
||||
<padding
|
||||
android:bottom="12dp"
|
||||
android:left="16dp"
|
||||
android:right="16dp"
|
||||
android:top="12dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
@@ -0,0 +1,258 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black_20">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/top"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
tools:text="慶餘年第一季:第一集" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/size"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="12sp"
|
||||
tools:text="1920 x 1080" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time_battery"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
tools:text="21:30" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/charging_indicator"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:src="@drawable/ic_charging_bolt"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/battery_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingEnd="8dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone"
|
||||
tools:text="85%"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/cast"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_control_cast" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/info"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_control_info" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/keep"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_control_keep_off" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/setting"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_control_setting" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/center"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/prevRoot"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/shape_control"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/prev"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/exo_icon_previous" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="48dp"
|
||||
android:layout_marginEnd="48dp"
|
||||
android:background="@drawable/shape_control">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/play"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/exo_icon_play" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/nextRoot"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/shape_control"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/next"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/exo_icon_next" />
|
||||
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/danmaku"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_control_danmaku_on" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<include
|
||||
android:id="@+id/right"
|
||||
layout="@layout/view_control_right"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginEnd="8dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/bottom"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/parse"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp"
|
||||
android:visibility="gone"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:itemCount="5"
|
||||
tools:listitem="@layout/adapter_parse_dark" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp">
|
||||
|
||||
<com.fongmi.android.tv.ui.custom.CustomSeekView
|
||||
android:id="@+id/seek"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/full"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_control_full" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<include
|
||||
android:id="@+id/action"
|
||||
layout="@layout/view_control_vod_action"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
@@ -154,5 +154,15 @@
|
||||
app:spanCount="2"
|
||||
tools:listitem="@layout/adapter_vod_rect" />
|
||||
|
||||
<!-- 搜索结果空状态Lottie动画 -->
|
||||
<include
|
||||
android:id="@+id/emptyLayout"
|
||||
layout="@layout/view_empty_search"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_marginTop="-40dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
@@ -18,7 +18,7 @@
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:background="@drawable/shape_action_background"
|
||||
android:src="@drawable/ic_back" />
|
||||
|
||||
<TextView
|
||||
@@ -35,7 +35,7 @@
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:background="@drawable/shape_action_background"
|
||||
android:src="@drawable/ic_action_sync" />
|
||||
|
||||
<ImageView
|
||||
@@ -43,7 +43,7 @@
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:background="@drawable/shape_action_background"
|
||||
android:src="@drawable/ic_action_delete"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
@@ -65,5 +65,15 @@
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="8dp" />
|
||||
|
||||
<!-- 空状态Lottie动画 -->
|
||||
<include
|
||||
android:id="@+id/emptyLayout"
|
||||
layout="@layout/view_empty_lottie"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="-40dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
@@ -14,14 +14,14 @@
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/navigation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="68dp"
|
||||
android:layout_height="70dp"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:background="@color/transparent"
|
||||
app:elevation="0dp"
|
||||
app:itemIconSize="24dp"
|
||||
app:itemIconTint="@color/nav"
|
||||
app:itemTextColor="@color/nav"
|
||||
app:labelVisibilityMode="labeled"
|
||||
app:labelVisibilityMode="unlabeled"
|
||||
app:itemActiveIndicatorStyle="@style/Indicator"
|
||||
app:menu="@menu/menu_nav" />
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:background="@drawable/shape_action_background"
|
||||
android:src="@drawable/ic_back" />
|
||||
|
||||
<TextView
|
||||
@@ -35,7 +35,7 @@
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:background="@drawable/shape_action_background"
|
||||
android:src="@drawable/ic_action_sync" />
|
||||
|
||||
<ImageView
|
||||
@@ -43,7 +43,7 @@
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:background="@drawable/shape_action_background"
|
||||
android:src="@drawable/ic_action_delete"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
@@ -65,5 +65,14 @@
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="8dp" />
|
||||
|
||||
<include
|
||||
android:id="@+id/emptyLayout"
|
||||
layout="@layout/view_empty_keep"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="-40dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,367 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/back"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/shape_action_background"
|
||||
android:src="@drawable/ic_back" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/setting_player"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
android:overScrollMode="never">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/render"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/player_render"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/renderText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
tools:text="Surface" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/scale"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/player_scale"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/scaleText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
tools:text="預設" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/caption"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/player_caption"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/captionText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="middle"
|
||||
android:gravity="end"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
tools:text="預設" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/buffer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/player_buffer"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bufferText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="end"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
tools:text="1" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="@string/times"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/speed"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/player_speed"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/speedText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="end"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
tools:text="1" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="@string/times"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/tunnel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/player_tunnel"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<com.fongmi.android.tv.ui.custom.CustomSwitch
|
||||
android:id="@+id/tunnelSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/audioDecode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/player_audio_decode"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<com.fongmi.android.tv.ui.custom.CustomSwitch
|
||||
android:id="@+id/audioDecodeSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/aac"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/player_aac"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<com.fongmi.android.tv.ui.custom.CustomSwitch
|
||||
android:id="@+id/aacSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/danmakuLoad"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/player_danmaku_load"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<com.fongmi.android.tv.ui.custom.CustomSwitch
|
||||
android:id="@+id/danmakuLoadSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/player_background"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/backgroundText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
tools:text="畫中畫" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ua"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/player_ua"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/uaText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="middle"
|
||||
android:gravity="end"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
tools:text="okhttp/4.11.0" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</LinearLayout>
|
||||
@@ -4,11 +4,14 @@
|
||||
android:id="@+id/text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:gravity="center"
|
||||
android:padding="8dp"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="14sp"
|
||||
android:textSize="12sp"
|
||||
tools:text="泥巴" />
|
||||
|
||||
@@ -6,18 +6,38 @@
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/text"
|
||||
style="@style/Widget.App.Button.OutlinedButton.SiteDialog"
|
||||
<FrameLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_weight="1">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/shape_site_dialog"
|
||||
android:gravity="center"
|
||||
android:ellipsize="middle"
|
||||
android:singleLine="true"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="14dp"
|
||||
android:paddingBottom="14dp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="14sp"
|
||||
tools:text="https://fongmi.github.io/cat.json" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/copy"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_setting_copy" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/delete"
|
||||
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>
|
||||
@@ -15,9 +15,10 @@
|
||||
android:stepSize="1"
|
||||
android:valueFrom="1"
|
||||
android:valueTo="10"
|
||||
app:thumbColor="@color/yellow_500"
|
||||
app:thumbColor="@color/accent"
|
||||
app:thumbRadius="9dp"
|
||||
app:tickVisible="false"
|
||||
app:trackColorActive="@color/yellow_500"
|
||||
app:trackColorInactive="@color/yellow_50" />
|
||||
app:trackColorActive="@color/accent"
|
||||
app:trackColorInactive="@color/white_30" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -8,13 +8,31 @@
|
||||
android:padding="16dp"
|
||||
tools:background="@color/white">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/control_speed"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/speedValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="1.0x"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/speed"
|
||||
android:layout_width="match_parent"
|
||||
@@ -24,10 +42,11 @@
|
||||
android:stepSize="0.25"
|
||||
android:valueFrom="0.5"
|
||||
android:valueTo="5"
|
||||
app:thumbColor="@color/yellow_500"
|
||||
app:thumbColor="@color/accent"
|
||||
app:thumbRadius="9dp"
|
||||
app:tickVisible="false"
|
||||
app:trackColorActive="@color/yellow_500"
|
||||
app:trackColorInactive="@color/yellow_50" />
|
||||
app:trackColorActive="@color/accent"
|
||||
app:trackColorInactive="@color/white_30" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/parseText"
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
android:stepSize="0.5"
|
||||
android:valueFrom="2"
|
||||
android:valueTo="5"
|
||||
app:thumbColor="@color/accent"
|
||||
app:thumbRadius="9dp"
|
||||
app:tickVisible="false"
|
||||
app:trackColorActive="@color/accent"
|
||||
app:trackColorInactive="@color/yellow_50" />
|
||||
app:trackColorInactive="@color/white_30" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -29,50 +29,80 @@
|
||||
<TextView
|
||||
android:id="@+id/time1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/shape_accent"
|
||||
android:background="@drawable/shape_accent_no_border"
|
||||
android:tag="5"
|
||||
android:text="@string/timer_5"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="14sp" />
|
||||
android:textSize="14sp"
|
||||
android:gravity="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/shape_accent"
|
||||
android:background="@drawable/shape_accent_no_border"
|
||||
android:tag="15"
|
||||
android:text="@string/timer_15"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="14sp" />
|
||||
android:textSize="14sp"
|
||||
android:gravity="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/shape_accent"
|
||||
android:background="@drawable/shape_accent_no_border"
|
||||
android:tag="30"
|
||||
android:text="@string/timer_30"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="14sp" />
|
||||
android:textSize="14sp"
|
||||
android:gravity="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/shape_accent"
|
||||
android:background="@drawable/shape_accent_no_border"
|
||||
android:tag="60"
|
||||
android:text="@string/timer_60"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="14sp" />
|
||||
android:textSize="14sp"
|
||||
android:gravity="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time5"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/shape_accent_no_border"
|
||||
android:tag="120"
|
||||
android:text="@string/timer_120"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="14sp"
|
||||
android:gravity="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time6"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/shape_accent_no_border"
|
||||
android:tag="180"
|
||||
android:text="@string/timer_180"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="14sp"
|
||||
android:gravity="center" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -96,25 +126,41 @@
|
||||
android:textStyle="bold"
|
||||
tools:text="5:00" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
<TextView
|
||||
android:id="@+id/delay"
|
||||
style="?attr/materialButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/shape_timer_delay_button"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:paddingStart="60dp"
|
||||
android:paddingEnd="60dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:singleLine="true"
|
||||
android:text="@string/timer_delay"
|
||||
android:textColor="@color/white" />
|
||||
android:textColor="@color/black"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
<TextView
|
||||
android:id="@+id/reset"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@drawable/shape_timer_reset_button"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:paddingStart="60dp"
|
||||
android:paddingEnd="60dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:singleLine="true"
|
||||
android:text="@string/timer_cancel"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -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>
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="10dp"
|
||||
android:fillViewport="true"
|
||||
android:overScrollMode="never"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
@@ -58,16 +59,32 @@
|
||||
android:paddingBottom="24dp">
|
||||
|
||||
<!-- 源管理分组 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/setting_source"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
android:alpha="0.7" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="(单击视频输入框可加源)"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="12sp"
|
||||
android:alpha="0.5" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -77,10 +94,11 @@
|
||||
<LinearLayout
|
||||
android:id="@+id/vod"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/shape_item"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
@@ -112,7 +130,8 @@
|
||||
android:background="@drawable/shape_item"
|
||||
android:padding="16dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_setting_home" />
|
||||
android:src="@drawable/potted_plant_24px"
|
||||
android:tint="@color/white" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/vodHistory"
|
||||
@@ -121,11 +140,13 @@
|
||||
android:background="@drawable/shape_item"
|
||||
android:padding="16dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_setting_history" />
|
||||
android:src="@drawable/ic_m3_list_alt"
|
||||
android:tint="@color/white" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/liveContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
@@ -135,10 +156,11 @@
|
||||
<LinearLayout
|
||||
android:id="@+id/live"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/shape_item"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
@@ -170,7 +192,8 @@
|
||||
android:background="@drawable/shape_item"
|
||||
android:padding="16dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_setting_home" />
|
||||
android:src="@drawable/potted_plant_24px"
|
||||
android:tint="@color/white" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/liveHistory"
|
||||
@@ -179,67 +202,11 @@
|
||||
android:background="@drawable/shape_item"
|
||||
android:padding="16dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_setting_history" />
|
||||
android:src="@drawable/ic_m3_list_alt"
|
||||
android:tint="@color/white" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/wall"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/setting_wall"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/wallUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="middle"
|
||||
android:gravity="end"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
tools:text="https://" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/wallDefault"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:padding="16dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_setting_switch" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/wallRefresh"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:padding="16dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_setting_refresh" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 应用设置分组 -->
|
||||
<TextView
|
||||
@@ -253,15 +220,31 @@
|
||||
android:textSize="14sp"
|
||||
android:alpha="0.7" />
|
||||
|
||||
<!-- 将三个设置项放在一个共同的背景容器中 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- 播放器设置 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/player"
|
||||
android:layout_width="match_parent"
|
||||
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
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/setting_player"
|
||||
android:textColor="@color/white"
|
||||
@@ -269,17 +252,60 @@
|
||||
|
||||
</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
|
||||
android:id="@+id/incognito"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:src="@drawable/domino_mask_24px"
|
||||
android:tint="@color/white" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -288,21 +314,60 @@
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
<com.fongmi.android.tv.ui.custom.CustomSwitch
|
||||
android:id="@+id/incognitoSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/M3SwitchStyle" />
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 显示直播 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/liveTabVisible"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:src="@drawable/ic_nav_live"
|
||||
android:tint="@color/white" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/setting_live_tab_visible"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<com.fongmi.android.tv.ui.custom.CustomSwitch
|
||||
android:id="@+id/liveTabVisibleSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 图片尺寸 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/size"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:src="@drawable/select_all_24px"
|
||||
android:tint="@color/white" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
@@ -322,6 +387,7 @@
|
||||
tools:text="Medium" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 网络设置分组 -->
|
||||
<TextView
|
||||
@@ -335,12 +401,28 @@
|
||||
android:textSize="14sp"
|
||||
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
|
||||
android:id="@+id/doh"
|
||||
android:layout_width="match_parent"
|
||||
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
|
||||
android:layout_width="wrap_content"
|
||||
@@ -361,13 +443,21 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 代理设置 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/proxy"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:src="@drawable/ic_fab_link"
|
||||
android:tint="@color/white" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
@@ -388,13 +478,21 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 缓存设置 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/cache"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:src="@drawable/owl_24px"
|
||||
android:tint="@color/white" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
@@ -414,6 +512,7 @@
|
||||
tools:text="1.0 MB" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 备份与恢复分组 -->
|
||||
<TextView
|
||||
@@ -434,63 +533,112 @@
|
||||
android:orientation="horizontal"
|
||||
android:padding="0dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/backup"
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:paddingStart="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:src="@drawable/photo_prints_24px"
|
||||
android:tint="@color/white" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/backup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:text="@string/setting_backup"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/restore"
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/restore"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:text="@string/setting_restore"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/version"
|
||||
android:layout_width="match_parent"
|
||||
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:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/setting_version"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
android:src="@drawable/ic_setting_github"
|
||||
android:tint="@color/white" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/versionText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:text="@string/setting_version"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
tools:text="1.2.1" />
|
||||
tools:text="版本 3.0.3" />
|
||||
|
||||
</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>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -55,14 +55,26 @@
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
android:paddingStart="24dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<!-- 播放器基本设置模块 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- 渲染方式 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/render"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
@@ -83,13 +95,21 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:background="#22FFFFFF" />
|
||||
|
||||
<!-- 缩放比例 -->
|
||||
<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">
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
@@ -110,13 +130,21 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:background="#22FFFFFF" />
|
||||
|
||||
<!-- 字幕样式 -->
|
||||
<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">
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
@@ -139,13 +167,21 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:background="#22FFFFFF" />
|
||||
|
||||
<!-- 缓冲时间 -->
|
||||
<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">
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
@@ -175,13 +211,21 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:background="#22FFFFFF" />
|
||||
|
||||
<!-- 长按倍速 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/speed"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
@@ -211,121 +255,21 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/tunnel"
|
||||
<!-- 分隔线 -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/player_tunnel"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/tunnelSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/M3SwitchStyle" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/audioDecode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/player_audio_decode"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/audioDecodeSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/M3SwitchStyle" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/aac"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/player_aac"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/aacSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/M3SwitchStyle" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/danmakuLoad"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/player_danmaku_load"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/danmakuLoadSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/M3SwitchStyle" />
|
||||
|
||||
</LinearLayout>
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:background="#22FFFFFF" />
|
||||
|
||||
<!-- 后台播放 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
@@ -346,13 +290,21 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:background="#22FFFFFF" />
|
||||
|
||||
<!-- User-Agent -->
|
||||
<LinearLayout
|
||||
android:id="@+id/ua"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/shape_item"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
@@ -375,5 +327,136 @@
|
||||
|
||||
</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.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -28,7 +28,7 @@
|
||||
android:id="@+id/logo"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:background="@drawable/shape_action_background"
|
||||
android:src="@drawable/ic_logo" />
|
||||
|
||||
<LinearLayout
|
||||
@@ -66,14 +66,14 @@
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:background="@drawable/shape_action_background"
|
||||
android:src="@drawable/ic_action_keep" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/history"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:background="@drawable/shape_action_background"
|
||||
android:src="@drawable/ic_action_history" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -100,12 +100,51 @@
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
<!-- 新增空源提示文本 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/empty_source_hint"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/lottieAnimation"
|
||||
android:layout_width="180dp"
|
||||
android:layout_height="180dp"
|
||||
app:lottie_fileName="lottie_empty_1.json"
|
||||
app:lottie_loop="true"
|
||||
app:lottie_autoPlay="true" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/source_hint"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/add_source_btn"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:backgroundTint="@color/primary"
|
||||
android:text="@string/add_source"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="16sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/retry"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:background="@drawable/shape_action_background"
|
||||
android:src="@drawable/ic_action_retry"
|
||||
android:visibility="gone" />
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_weight="1"
|
||||
app:bar_height="2dp"
|
||||
app:scrubber_enabled_size="14dp"
|
||||
app:scrubber_disabled_size="14dp"
|
||||
app:played_color="#FFEB3B"
|
||||
app:scrubber_color="#FFEB3B"
|
||||
app:buffered_color="#80FFEB3B" />
|
||||
|
||||
@@ -47,6 +47,40 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time_battery"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
tools:text="21:30" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/charging_indicator"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:src="@drawable/ic_charging_bolt"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/battery_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingEnd="8dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
android:visibility="gone"
|
||||
tools:text="85%"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/cast"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:src="@drawable/ic_empty" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
android:text="@string/error_no_live" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -43,7 +43,9 @@
|
||||
<string name="timer_15">15 分钟</string>
|
||||
<string name="timer_30">30 分钟</string>
|
||||
<string name="timer_60">1 小时</string>
|
||||
<string name="timer_delay">延长 5 分钟</string>
|
||||
<string name="timer_120">2 小时</string>
|
||||
<string name="timer_180">3 小时</string>
|
||||
<string name="timer_delay">延长 15 分钟</string>
|
||||
<string name="timer_cancel">取消定时器</string>
|
||||
|
||||
<!-- Hint -->
|
||||
|
||||
@@ -41,7 +41,9 @@
|
||||
<string name="timer_15">15 分鐘</string>
|
||||
<string name="timer_30">30 分鐘</string>
|
||||
<string name="timer_60">1 小時</string>
|
||||
<string name="timer_delay">延長 5 分鐘</string>
|
||||
<string name="timer_120">2 小時</string>
|
||||
<string name="timer_180">3 小時</string>
|
||||
<string name="timer_delay">延長 15 分鐘</string>
|
||||
<string name="timer_cancel">取消定時器</string>
|
||||
|
||||
<!-- Hint -->
|
||||
|
||||
@@ -5,5 +5,8 @@
|
||||
<color name="accent">#FFEB3B</color>
|
||||
<color name="indicator">@color/white_80</color>
|
||||
<color name="yellow_50">#4DFFEB3B</color>
|
||||
<color name="white_50">#80FFFFFF</color>
|
||||
<color name="black_50">#80000000</color>
|
||||
<color name="white_30">#4DFFFFFF</color>
|
||||
|
||||
</resources>
|
||||
@@ -43,7 +43,9 @@
|
||||
<string name="timer_15">15 minutes</string>
|
||||
<string name="timer_30">30 minutes</string>
|
||||
<string name="timer_60">1 hour</string>
|
||||
<string name="timer_delay">Add 5 minutes</string>
|
||||
<string name="timer_120">2 hours</string>
|
||||
<string name="timer_180">3 hours</string>
|
||||
<string name="timer_delay">Add 15 minutes</string>
|
||||
<string name="timer_cancel">Cancel timer</string>
|
||||
|
||||
<!-- Hint -->
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<item name="colorPrimary">@color/primary</item>
|
||||
<item name="colorPrimaryDark">@color/primaryDark</item>
|
||||
<item name="colorAccent">@color/accent</item>
|
||||
<item name="colorControlHighlight">@color/white_20</item>
|
||||
<item name="android:windowBackground">@null</item>
|
||||
<item name="android:windowDisablePreview">true</item>
|
||||
<item name="android:navigationBarColor">@color/transparent</item>
|
||||
@@ -30,12 +31,8 @@
|
||||
<style name="Control.Action">
|
||||
<item name="android:layout_width">wrap_content</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:background">?attr/selectableItemBackgroundBorderless</item>
|
||||
<item name="android:background">@drawable/shape_action_background</item>
|
||||
<item name="android:padding">8dp</item>
|
||||
<item name="android:shadowColor">@color/grey_200</item>
|
||||
<item name="android:shadowDx">1</item>
|
||||
<item name="android:shadowDy">1</item>
|
||||
<item name="android:shadowRadius">0.5</item>
|
||||
<item name="android:textColor">@color/white</item>
|
||||
<item name="android:textSize">14sp</item>
|
||||
</style>
|
||||
@@ -60,7 +57,8 @@
|
||||
</style>
|
||||
|
||||
<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 name="BottomNavigationView.TextAppearance" parent="TextAppearance.AppCompat">
|
||||
@@ -69,20 +67,20 @@
|
||||
</style>
|
||||
|
||||
<style name="BottomNavigation" parent="Widget.Material3.BottomNavigationView">
|
||||
<item name="itemPaddingTop">8dp</item>
|
||||
<item name="itemPaddingBottom">8dp</item>
|
||||
<item name="itemPaddingTop">4dp</item>
|
||||
<item name="itemPaddingBottom">4dp</item>
|
||||
<item name="itemTextAppearanceActive">@style/BottomNavigationView.TextAppearance</item>
|
||||
<item name="itemTextAppearanceInactive">@style/BottomNavigationView.TextAppearance</item>
|
||||
<item name="itemRippleColor">@android:color/transparent</item>
|
||||
<item name="android:background">@null</item>
|
||||
</style>
|
||||
|
||||
<!-- M3 Switch Style -->
|
||||
<style name="M3SwitchStyle" parent="Widget.AppCompat.CompoundButton.Switch">
|
||||
<item name="android:thumb">@drawable/m3_switch_thumb</item>
|
||||
<item name="track">@drawable/m3_switch_track</item>
|
||||
<item name="android:switchMinWidth">52dp</item>
|
||||
<item name="android:switchPadding">8dp</item>
|
||||
<item name="android:colorControlActivated">#FFEB3B</item>
|
||||
<item name="android:colorControlHighlight">@android:color/transparent</item>
|
||||
<!-- M3 Switch Style (Material Design) - 使用CustomSwitch完全自定义 -->
|
||||
<style name="M3SwitchStyle" parent="Widget.AppCompat.CompoundButton.CheckBox">
|
||||
<item name="android:button">@null</item>
|
||||
<item name="android:background">@drawable/custom_switch_bg</item>
|
||||
<item name="android:minHeight">30dp</item>
|
||||
<item name="android:minWidth">50dp</item>
|
||||
</style>
|
||||
|
||||
<!-- 自定义数据源按钮样式 -->
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
plugins {
|
||||
id 'com.android.application' version '8.8.2' apply false
|
||||
id 'com.android.library' version '8.8.2' apply false
|
||||
id 'com.chaquo.python' version '15.0.1' apply false
|
||||
id 'com.android.application' version '8.12.0' apply false
|
||||
id 'com.android.library' version '8.12.0' apply false
|
||||
id 'com.chaquo.python' version '16.1.0' apply false
|
||||
}
|
||||
|
||||
tasks.register('clean', Delete) {
|
||||
@@ -10,6 +10,6 @@ tasks.register('clean', Delete) {
|
||||
|
||||
project.ext {
|
||||
gsonVersion = '2.11.0'
|
||||
media3Version = '1.6.1'
|
||||
okhttpVersion = '5.0.0-alpha.14'
|
||||
media3Version = '1.4.1'
|
||||
okhttpVersion = '4.12.0'
|
||||
}
|
||||
|
||||
@@ -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 "========================================="
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
cd /Users/chen/Desktop/XMBOX
|
||||
./gradlew clean assembleMobileArm64_v8aDebug
|
||||
@@ -5,21 +5,23 @@ plugins {
|
||||
android {
|
||||
namespace 'com.github.catvod.crawler'
|
||||
|
||||
compileSdk 35
|
||||
compileSdk {
|
||||
version = release(36)
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk 21
|
||||
minSdk 24
|
||||
targetSdk 28
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api 'androidx.annotation:annotation:1.6.0'
|
||||
api 'androidx.annotation:annotation:1.9.1'
|
||||
api 'androidx.preference:preference:1.2.1'
|
||||
api 'com.google.code.gson:gson:' + gsonVersion
|
||||
api 'com.google.net.cronet:cronet-okhttp:0.1.0'
|
||||
|
||||
@@ -1,18 +1,195 @@
|
||||
package com.github.catvod.utils;
|
||||
|
||||
import android.os.SystemClock;
|
||||
|
||||
import com.github.catvod.net.OkHttp;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class Github {
|
||||
|
||||
public static final String URL = "https://github.com/Tosencen/XMBOX";
|
||||
public static final String URL = "https://raw.githubusercontent.com/Tosencen/XMBOX-Release/main";
|
||||
|
||||
// 国内镜像地址 - 使用Gitee作为镜像
|
||||
public static final String CN_URL = "https://gitee.com/ochenoktochen/XMBOX-Release/raw/main";
|
||||
|
||||
// 存储测速结果
|
||||
private static Boolean useCnMirror = null;
|
||||
private static long lastCheckTime = 0;
|
||||
private static final long CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24小时
|
||||
|
||||
private static String getUrl(String path, String name) {
|
||||
return URL + "/" + path + "/" + name;
|
||||
}
|
||||
|
||||
private static String getCnUrl(String path, String name) {
|
||||
return CN_URL + "/" + path + "/" + name;
|
||||
}
|
||||
|
||||
|
||||
public static String getJson(boolean dev, String name) {
|
||||
if (useCnMirror()) {
|
||||
return getCnUrl("apk/" + (dev ? "dev" : "release"), name + ".json");
|
||||
} else {
|
||||
return getUrl("apk/" + (dev ? "dev" : "release"), name + ".json");
|
||||
}
|
||||
}
|
||||
|
||||
public static String getApk(boolean dev, String name) {
|
||||
if (useCnMirror()) {
|
||||
return getCnUrl("apk/" + (dev ? "dev" : "release"), name + ".apk");
|
||||
} else {
|
||||
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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
lxml
|
||||
ujson
|
||||
pyquery
|
||||
requests
|
||||
jsonpath
|
||||
cachetools
|
||||
pycryptodome
|
||||
beautifulsoup4
|
||||
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
</manifest>
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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 上检查并发布"
|
||||
@@ -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. 🛡️ **片头片尾跳过** - 处理嵌入式广告
|
||||
|
||||
让您享受**纯净、流畅、安全**的视频观看体验!
|
||||
|
||||
@@ -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}"
|
||||
@@ -6,7 +6,7 @@
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 --add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 --add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
@@ -22,3 +22,7 @@ android.enableJetifier=true
|
||||
android.useFullClasspathForDexingTransform=true
|
||||
android.nonTransitiveRClass=false
|
||||
android.nonFinalResIds=false
|
||||
|
||||
# Library versions
|
||||
media3Version=1.3.0
|
||||
okhttpVersion=4.11.0
|
||||
@@ -1,6 +1,6 @@
|
||||
#Wed Mar 29 12:54:35 CST 2023
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -499,6 +499,6 @@ public class Hook extends PackageManager {
|
||||
|
||||
@Override
|
||||
public boolean canRequestPackageInstalls() {
|
||||
return false;
|
||||
return true; // 允许请求安装包权限
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 11 KiB |
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
@@ -5,26 +5,24 @@ plugins {
|
||||
android {
|
||||
namespace 'com.fongmi.android.tv.quickjs'
|
||||
|
||||
compileSdk 35
|
||||
compileSdk {
|
||||
version = release(36)
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk 21
|
||||
minSdk 24
|
||||
targetSdk 28
|
||||
}
|
||||
|
||||
lint {
|
||||
disable 'UnsafeOptInUsageError'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':catvod')
|
||||
implementation 'wang.harlon.quickjs:wrapper-java:3.2.0'
|
||||
implementation 'wang.harlon.quickjs:wrapper-android:3.2.0'
|
||||
implementation 'wang.harlon.quickjs:wrapper-java:3.2.3'
|
||||
implementation 'wang.harlon.quickjs:wrapper-android:3.2.3'
|
||||
implementation 'net.sourceforge.streamsupport:android-retrofuture:1.7.4'
|
||||
}
|
||||
@@ -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 "=== 完成! ==="
|
||||
|
||||
@@ -20,6 +20,6 @@ dependencyResolutionManagement {
|
||||
}
|
||||
include ':app'
|
||||
include ':catvod'
|
||||
include ':chaquo'
|
||||
// include ':chaquo' // 已移除Python支持
|
||||
include ':quickjs'
|
||||
rootProject.name = "TV"
|
||||
rootProject.name = "XMBOX"
|
||||
|
||||