feat: 全面优化应用稳定性和用户体验

🐛 核心修复:
- 修复 VodConfig/LiveConfig 空指针崩溃问题
- 添加构造函数初始化列表,防止 clear() 方法空指针异常
- 增强 Setting 类隐私协议状态管理

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

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

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

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

版本:v3.0.7 - 包含所有修复和优化的稳定版本
This commit is contained in:
您的名字
2025-09-26 13:17:49 +08:00
parent dde56eeedb
commit ca95128ee9
28 changed files with 528 additions and 41 deletions
+3 -2
View File
@@ -22,8 +22,8 @@ android {
minSdk 24 minSdk 24
//noinspection ExpiredTargetSdkVersion //noinspection ExpiredTargetSdkVersion
targetSdk 28 targetSdk 28
versionCode 305 versionCode 307
versionName "3.0.6" versionName "3.0.7"
javaCompileOptions { javaCompileOptions {
annotationProcessorOptions { annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString(), "eventBusIndex": "com.fongmi.android.tv.event.EventIndex"] arguments = ["room.schemaLocation": "$projectDir/schemas".toString(), "eventBusIndex": "com.fongmi.android.tv.event.EventIndex"]
@@ -162,4 +162,5 @@ dependencies {
annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.3.1' annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.3.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.5' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.5'
implementation 'io.noties.markwon:core:4.6.2' implementation 'io.noties.markwon:core:4.6.2'
implementation 'com.airbnb.android:lottie:5.2.0'
} }
@@ -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>
@@ -12,6 +12,6 @@
android:nextFocusUp="@id/flag" android:nextFocusUp="@id/flag"
android:nextFocusDown="@id/array" android:nextFocusDown="@id/array"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/text" android:textColor="@color/episode_text"
android:textSize="16sp" android:textSize="16sp"
tools:text="20" /> tools:text="20" />
File diff suppressed because one or more lines are too long
@@ -316,4 +316,12 @@ public class Setting {
public static boolean hasCaption() { public static boolean hasCaption() {
return new Intent(Settings.ACTION_CAPTIONING_SETTINGS).resolveActivity(App.get().getPackageManager()) != null; return new Intent(Settings.ACTION_CAPTIONING_SETTINGS).resolveActivity(App.get().getPackageManager()) != null;
} }
public static boolean isPrivacyAgreed() {
return Prefers.getBoolean("privacy_agreed_v1", false);
}
public static void setPrivacyAgreed(boolean agreed) {
Prefers.put("privacy_agreed_v1", agreed);
}
} }
@@ -40,6 +40,13 @@ public class LiveConfig {
private boolean sync; private boolean sync;
private Live home; private Live home;
private LiveConfig() {
// 在构造函数中初始化列表,防止空指针异常
this.ads = new ArrayList<>();
this.rules = new ArrayList<>();
this.lives = new ArrayList<>();
}
private static class Loader { private static class Loader {
static volatile LiveConfig INSTANCE = new LiveConfig(); static volatile LiveConfig INSTANCE = new LiveConfig();
} }
@@ -97,9 +104,9 @@ public class LiveConfig {
public LiveConfig clear() { public LiveConfig clear() {
this.home = null; this.home = null;
this.ads.clear(); if (this.ads != null) this.ads.clear();
this.rules.clear(); if (this.rules != null) this.rules.clear();
this.lives.clear(); if (this.lives != null) this.lives.clear();
return this; return this;
} }
@@ -39,6 +39,16 @@ public class VodConfig {
private Site home; private Site home;
private volatile boolean isLoading = false; // 添加加载状态标记 private volatile boolean isLoading = false; // 添加加载状态标记
private VodConfig() {
// 在构造函数中初始化列表,防止空指针异常
this.ads = new ArrayList<>();
this.doh = new ArrayList<>();
this.rules = new ArrayList<>();
this.sites = new ArrayList<>();
this.flags = new ArrayList<>();
this.parses = new ArrayList<>();
}
private static class Loader { private static class Loader {
static volatile VodConfig INSTANCE = new VodConfig(); static volatile VodConfig INSTANCE = new VodConfig();
} }
@@ -123,12 +133,12 @@ public class VodConfig {
this.wall = null; this.wall = null;
this.home = null; this.home = null;
this.parse = null; this.parse = null;
this.ads.clear(); if (this.ads != null) this.ads.clear();
this.doh.clear(); if (this.doh != null) this.doh.clear();
this.rules.clear(); if (this.rules != null) this.rules.clear();
this.sites.clear(); if (this.sites != null) this.sites.clear();
this.flags.clear(); if (this.flags != null) this.flags.clear();
this.parses.clear(); if (this.parses != null) this.parses.clear();
this.loadLive = true; this.loadLive = true;
BaseLoader.get().clear(); BaseLoader.get().clear();
return this; return this;
@@ -9,6 +9,7 @@ import android.widget.RelativeLayout;
import com.fongmi.android.tv.databinding.ViewEmptyBinding; import com.fongmi.android.tv.databinding.ViewEmptyBinding;
import com.fongmi.android.tv.databinding.ViewProgressBinding; import com.fongmi.android.tv.databinding.ViewProgressBinding;
import com.airbnb.lottie.LottieAnimationView;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -47,7 +48,8 @@ public class ProgressLayout extends RelativeLayout {
} }
private void initView() { private void initView() {
mEmptyView = ViewEmptyBinding.inflate(LayoutInflater.from(getContext())).getRoot(); // 使用新的Lottie动画空状态布局
mEmptyView = LayoutInflater.from(getContext()).inflate(com.fongmi.android.tv.R.layout.view_empty_lottie, null);
mEmptyView.setTag(TAG_PROGRESS); mEmptyView.setTag(TAG_PROGRESS);
mEmptyView.setVisibility(GONE); mEmptyView.setVisibility(GONE);
mProgressView = ViewProgressBinding.inflate(LayoutInflater.from(getContext())).getRoot(); mProgressView = ViewProgressBinding.inflate(LayoutInflater.from(getContext())).getRoot();
@@ -103,21 +105,46 @@ public class ProgressLayout extends RelativeLayout {
case CONTENT: case CONTENT:
mEmptyView.setVisibility(GONE); mEmptyView.setVisibility(GONE);
mProgressView.setVisibility(GONE); mProgressView.setVisibility(GONE);
pauseLottieAnimation();
setContentVisibility(true); setContentVisibility(true);
break; break;
case PROGRESS: case PROGRESS:
mEmptyView.setVisibility(GONE); mEmptyView.setVisibility(GONE);
mProgressView.setVisibility(VISIBLE); mProgressView.setVisibility(VISIBLE);
pauseLottieAnimation();
setContentVisibility(false); setContentVisibility(false);
break; break;
case EMPTY: case EMPTY:
mEmptyView.setVisibility(VISIBLE); mEmptyView.setVisibility(VISIBLE);
mProgressView.setVisibility(GONE); mProgressView.setVisibility(GONE);
playLottieAnimation();
setContentVisibility(false); setContentVisibility(false);
break; break;
} }
} }
private void playLottieAnimation() {
try {
LottieAnimationView lottieView = mEmptyView.findViewById(com.fongmi.android.tv.R.id.lottieAnimation);
if (lottieView != null) {
lottieView.playAnimation();
}
} catch (Exception e) {
// 忽略错误,保持兼容性
}
}
private void pauseLottieAnimation() {
try {
LottieAnimationView lottieView = mEmptyView.findViewById(com.fongmi.android.tv.R.id.lottieAnimation);
if (lottieView != null) {
lottieView.pauseAnimation();
}
} catch (Exception e) {
// 忽略错误,保持兼容性
}
}
private void setContentVisibility(boolean visible) { private void setContentVisibility(boolean visible) {
for (View view : mContentViews) { for (View view : mContentViews) {
if (visible) showView(view); if (visible) showView(view);
@@ -0,0 +1,103 @@
<?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">
<!-- 应用图标 -->
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@mipmap/ic_launcher"
android:contentDescription="@string/app_name" />
<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:padding="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>
+1 -1
View File
@@ -19,6 +19,6 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="@string/error_empty" android:text="@string/error_empty"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="16sp" /> android:textSize="14sp" />
</LinearLayout> </LinearLayout>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottieAnimation"
android:layout_width="180dp"
android:layout_height="180dp"
app:lottie_fileName="lottie_empty_1.json"
app:lottie_loop="true"
app:lottie_autoPlay="true" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/error_keep_empty"
android:textColor="@color/white"
android:textSize="14sp" />
</LinearLayout>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottieAnimation"
android:layout_width="180dp"
android:layout_height="180dp"
app:lottie_fileName="lottie_empty_1.json"
app:lottie_loop="true"
app:lottie_autoPlay="true" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/error_empty"
android:textColor="@color/white"
android:textSize="14sp"
android:alpha="0.8" />
</LinearLayout>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottieAnimation"
android:layout_width="180dp"
android:layout_height="180dp"
app:lottie_fileName="lottie_empty_1.json"
app:lottie_loop="true"
app:lottie_autoPlay="true" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/error_search_empty"
android:textColor="@color/white"
android:textSize="14sp" />
</LinearLayout>
+1 -1
View File
@@ -141,7 +141,7 @@
<string name="error_play_flag">暫無線路資料</string> <string name="error_play_flag">暫無線路資料</string>
<string name="error_play_timeout">連線逾時</string> <string name="error_play_timeout">連線逾時</string>
<string name="error_detail">暫無播放資料</string> <string name="error_detail">暫無播放資料</string>
<string name="error_empty">找不到資料</string> <string name="error_empty">這裡撒子內容都沒得~</string>
<string name="error_cast_file">不支持的檔案格式</string> <string name="error_cast_file">不支持的檔案格式</string>
<string name="error_device_limit">設備授權數已達上限</string> <string name="error_device_limit">設備授權數已達上限</string>
<string name="error_live_empty">該訂閱無直播內容</string> <string name="error_live_empty">該訂閱無直播內容</string>
+10 -1
View File
@@ -151,7 +151,9 @@
<string name="error_device_limit">Device authorization limit reached</string> <string name="error_device_limit">Device authorization limit reached</string>
<string name="error_live_empty">This subscription has no live content</string> <string name="error_live_empty">This subscription has no live content</string>
<string name="error_no_live">Current source has no live content</string> <string name="error_no_live">Current source has no live content</string>
<string name="error_empty">Not found</string> <string name="error_empty">这里撒子内容都没得~</string>
<string name="error_keep_empty">老表~没得收藏哈</string>
<string name="error_search_empty">搜索无结果,换个关键词试试</string>
<string name="error_detail">No play data</string> <string name="error_detail">No play data</string>
<string name="error_play_flag">No flag data</string> <string name="error_play_flag">No flag data</string>
<string name="error_play_code">Error code: <xliff:g name="name">%s</xliff:g></string> <string name="error_play_code">Error code: <xliff:g name="name">%s</xliff:g></string>
@@ -237,6 +239,13 @@
<string name="source_hint">No video sources added yet\nClick the button below to add</string> <string name="source_hint">No video sources added yet\nClick the button below to add</string>
<string name="add_source">Add Source</string> <string name="add_source">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_setting">Add Source</string>
<string name="source_hint_live">Add Live Source</string> <string name="source_hint_live">Add Live Source</string>
<string name="source_hint_wall">Add Wallpaper Source</string> <string name="source_hint_wall">Add Wallpaper Source</string>
+7
View File
@@ -10,6 +10,13 @@
<application> <application>
<activity
android:name=".ui.activity.PrivacyAgreementActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode|orientation"
android:exported="false"
android:screenOrientation="fullUser"
android:windowSoftInputMode="adjustPan" />
<activity <activity
android:name=".ui.activity.HomeActivity" android:name=".ui.activity.HomeActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode|orientation" android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode|orientation"
@@ -56,6 +56,7 @@ public class Updater implements Download.Callback {
public Updater() { public Updater() {
this.download = Download.create("", getFile(), this); this.download = Download.create("", getFile(), this);
this.forceCheck = false; // 默认不是用户主动检查更新
} }
public Updater force() { public Updater force() {
@@ -127,14 +128,14 @@ public class Updater implements Download.Callback {
String response = OkHttp.string(jsonUrl); String response = OkHttp.string(jsonUrl);
Logger.d("Update check response: " + response); Logger.d("Update check response: " + response);
// 检查响应是否包含错误信息 // 检查响应是否包含错误信息,只有在用户主动检查更新时才显示错误提示
if (response.contains("rate limit exceeded")) { if (response.contains("rate limit exceeded")) {
App.post(() -> Notify.show("检查更新失败:API请求过于频繁,请稍后重试")); showErrorIfForceCheck("检查更新失败:API请求过于频繁,请稍后重试");
return; return;
} }
if (response.contains("Not Found Project") || response.contains("Not Found")) { if (response.contains("Not Found Project") || response.contains("Not Found")) {
App.post(() -> Notify.show("检查更新失败:更新服务暂时不可用")); showErrorIfForceCheck("检查更新失败:更新服务暂时不可用");
return; return;
} }
@@ -157,8 +158,10 @@ public class Updater implements Download.Callback {
download = Download.create(downloadUrl, getFile(), this); download = Download.create(downloadUrl, getFile(), this);
App.post(() -> show(activity, tagName, body)); App.post(() -> show(activity, tagName, body));
} else if (downloadUrl != null) { } else if (downloadUrl != null) {
// 找到APK但不需要更新,提示已是最新版 // 找到APK但不需要更新,只在用户主动检查更新时提示已是最新版
App.post(() -> Notify.show("已是最新版本 " + tagName)); if (forceCheck) {
App.post(() -> Notify.show("已是最新版本 " + tagName));
}
Logger.d("Already latest version: " + tagName); Logger.d("Already latest version: " + tagName);
} else { } else {
// 未找到对应的APK文件 // 未找到对应的APK文件
@@ -171,17 +174,19 @@ public class Updater implements Download.Callback {
} catch (Exception e) { } catch (Exception e) {
Logger.e("Update check failed", e); Logger.e("Update check failed", e);
e.printStackTrace(); e.printStackTrace();
// 添加用户友好的错误提示 // 添加用户友好的错误提示,只有在用户主动检查更新时才显示
App.post(() -> { App.post(() -> {
String errorMsg = "检查更新失败"; if (forceCheck) { // 只有在用户主动检查更新时才显示错误提示
if (e.getMessage() != null && e.getMessage().contains("rate limit")) { String errorMsg = "检查更新失败";
errorMsg = "检查更新失败:请求过于频繁,请稍后重试"; if (e.getMessage() != null && e.getMessage().contains("rate limit")) {
} else if (e.getMessage() != null && e.getMessage().contains("Not Found")) { errorMsg = "检查更新失败:API请求过于频繁,请稍后重试"; // 统一错误提示文本
errorMsg = "检查更新失败:更新服务暂时不可用"; } else if (e.getMessage() != null && e.getMessage().contains("Not Found")) {
} else { errorMsg = "检查更新失败:更新服务暂时不可用";
errorMsg = "检查更新失败,请稍后重试"; } else {
errorMsg = "检查更新失败,请稍后重试";
}
Notify.show(errorMsg);
} }
Notify.show(errorMsg);
}); });
} }
} }
@@ -216,6 +221,16 @@ public class Updater implements Download.Callback {
} }
} }
/**
* 只有在用户主动检查更新时才显示错误提示
* @param message 错误提示消息
*/
private void showErrorIfForceCheck(String message) {
if (forceCheck) {
App.post(() -> Notify.show(message));
}
}
@Override @Override
public void progress(int progress) { public void progress(int progress) {
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setText(String.format(Locale.getDefault(), "%1$d%%", progress)); dialog.getButton(DialogInterface.BUTTON_POSITIVE).setText(String.format(Locale.getDefault(), "%1$d%%", progress));
@@ -44,6 +44,7 @@ import com.fongmi.android.tv.utils.Util;
import com.github.catvod.net.OkHttp; import com.github.catvod.net.OkHttp;
import com.google.android.flexbox.FlexDirection; import com.google.android.flexbox.FlexDirection;
import com.google.android.flexbox.FlexboxLayoutManager; import com.google.android.flexbox.FlexboxLayoutManager;
import com.airbnb.lottie.LottieAnimationView;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder; import java.net.URLEncoder;
@@ -151,12 +152,14 @@ public class CollectActivity extends BaseActivity implements CustomScroller.Call
if (mCollectAdapter.getPosition() == 0) mSearchAdapter.addAll(result.getList()); if (mCollectAdapter.getPosition() == 0) mSearchAdapter.addAll(result.getList());
mCollectAdapter.add(Collect.create(result.getList())); mCollectAdapter.add(Collect.create(result.getList()));
mCollectAdapter.add(result.getList()); mCollectAdapter.add(result.getList());
updateEmptyState();
}); });
mViewModel.result.observe(this, result -> { mViewModel.result.observe(this, result -> {
boolean same = !result.getList().isEmpty() && mCollectAdapter.getActivated().getSite().equals(result.getList().get(0).getSite()); boolean same = !result.getList().isEmpty() && mCollectAdapter.getActivated().getSite().equals(result.getList().get(0).getSite());
if (same) mCollectAdapter.getActivated().getList().addAll(result.getList()); if (same) mCollectAdapter.getActivated().getList().addAll(result.getList());
if (same) mSearchAdapter.addAll(result.getList()); if (same) mSearchAdapter.addAll(result.getList());
mScroller.endLoading(result); mScroller.endLoading(result);
updateEmptyState();
}); });
} }
@@ -187,6 +190,7 @@ public class CollectActivity extends BaseActivity implements CustomScroller.Call
mBinding.agent.setVisibility(View.GONE); mBinding.agent.setVisibility(View.GONE);
mBinding.view.setVisibility(View.VISIBLE); mBinding.view.setVisibility(View.VISIBLE);
mBinding.result.setVisibility(View.VISIBLE); mBinding.result.setVisibility(View.VISIBLE);
updateEmptyState(); // 搜索开始时显示空状态
if (mExecutor != null) mExecutor.shutdownNow(); if (mExecutor != null) mExecutor.shutdownNow();
mExecutor = new PauseExecutor(20); mExecutor = new PauseExecutor(20);
String keyword = mBinding.keyword.getText().toString().trim(); String keyword = mBinding.keyword.getText().toString().trim();
@@ -194,6 +198,27 @@ public class CollectActivity extends BaseActivity implements CustomScroller.Call
App.post(() -> mRecordAdapter.add(keyword), 250); App.post(() -> mRecordAdapter.add(keyword), 250);
} }
private void updateEmptyState() {
// 只有在结果页面可见且搜索结果为空时才显示空状态动画
boolean isResultVisible = isVisible(mBinding.result);
boolean isEmpty = mSearchAdapter.getItemCount() == 0;
boolean shouldShowEmpty = isResultVisible && isEmpty;
mBinding.emptyLayout.getRoot().setVisibility(shouldShowEmpty ? View.VISIBLE : View.GONE);
// 控制Lottie动画播放
if (shouldShowEmpty) {
try {
LottieAnimationView lottieView = mBinding.emptyLayout.getRoot().findViewById(R.id.lottieAnimation);
if (lottieView != null) {
lottieView.playAnimation();
}
} catch (Exception e) {
// 忽略错误
}
}
}
private void search(Site site, String keyword) { private void search(Site site, String keyword) {
try { try {
mViewModel.searchContent(site, keyword, false); mViewModel.searchContent(site, keyword, false);
@@ -235,6 +260,7 @@ public class CollectActivity extends BaseActivity implements CustomScroller.Call
mBinding.result.setVisibility(View.GONE); mBinding.result.setVisibility(View.GONE);
mBinding.site.setVisibility(View.VISIBLE); mBinding.site.setVisibility(View.VISIBLE);
mBinding.agent.setVisibility(View.VISIBLE); mBinding.agent.setVisibility(View.VISIBLE);
mBinding.emptyLayout.getRoot().setVisibility(View.GONE); // 隐藏空状态动画
if (mExecutor != null) mExecutor.shutdownNow(); if (mExecutor != null) mExecutor.shutdownNow();
} }
@@ -17,6 +17,7 @@ import com.fongmi.android.tv.ui.adapter.HistoryAdapter;
import com.fongmi.android.tv.ui.base.BaseActivity; import com.fongmi.android.tv.ui.base.BaseActivity;
import com.fongmi.android.tv.ui.dialog.SyncDialog; import com.fongmi.android.tv.ui.dialog.SyncDialog;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.airbnb.lottie.LottieAnimationView;
import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;
@@ -59,6 +60,25 @@ public class HistoryActivity extends BaseActivity implements HistoryAdapter.OnCl
private void getHistory() { private void getHistory() {
mAdapter.addAll(History.get()); mAdapter.addAll(History.get());
mBinding.delete.setVisibility(mAdapter.getItemCount() > 0 ? View.VISIBLE : View.GONE); mBinding.delete.setVisibility(mAdapter.getItemCount() > 0 ? View.VISIBLE : View.GONE);
updateEmptyState();
}
private void updateEmptyState() {
boolean isEmpty = mAdapter.getItemCount() == 0;
mBinding.emptyLayout.getRoot().setVisibility(isEmpty ? View.VISIBLE : View.GONE);
mBinding.recycler.setVisibility(isEmpty ? View.GONE : View.VISIBLE);
// 控制Lottie动画播放
if (isEmpty) {
try {
LottieAnimationView lottieView = mBinding.emptyLayout.getRoot().findViewById(R.id.lottieAnimation);
if (lottieView != null) {
lottieView.playAnimation();
}
} catch (Exception e) {
// 忽略错误
}
}
} }
private void onSync(View view) { private void onSync(View view) {
@@ -67,7 +87,10 @@ public class HistoryActivity extends BaseActivity implements HistoryAdapter.OnCl
private void onDelete(View view) { private void onDelete(View view) {
if (mAdapter.isDelete()) { if (mAdapter.isDelete()) {
new MaterialAlertDialogBuilder(this).setTitle(R.string.dialog_delete_record).setMessage(R.string.dialog_delete_history).setNegativeButton(R.string.dialog_negative, null).setPositiveButton(R.string.dialog_positive, (dialog, which) -> mAdapter.clear()).show(); new MaterialAlertDialogBuilder(this).setTitle(R.string.dialog_delete_record).setMessage(R.string.dialog_delete_history).setNegativeButton(R.string.dialog_negative, null).setPositiveButton(R.string.dialog_positive, (dialog, which) -> {
mAdapter.clear();
updateEmptyState();
}).show();
} else if (mAdapter.getItemCount() > 0) { } else if (mAdapter.getItemCount() > 0) {
mAdapter.setDelete(true); mAdapter.setDelete(true);
} else { } else {
@@ -91,6 +114,7 @@ public class HistoryActivity extends BaseActivity implements HistoryAdapter.OnCl
if (mAdapter.getItemCount() > 0) return; if (mAdapter.getItemCount() > 0) return;
mBinding.delete.setVisibility(View.GONE); mBinding.delete.setVisibility(View.GONE);
mAdapter.setDelete(false); mAdapter.setDelete(false);
updateEmptyState();
} }
@Override @Override
@@ -16,6 +16,7 @@ import androidx.viewbinding.ViewBinding;
import com.fongmi.android.tv.App; import com.fongmi.android.tv.App;
import com.fongmi.android.tv.R; import com.fongmi.android.tv.R;
import com.fongmi.android.tv.Setting;
import com.fongmi.android.tv.Updater; import com.fongmi.android.tv.Updater;
import com.fongmi.android.tv.api.config.LiveConfig; import com.fongmi.android.tv.api.config.LiveConfig;
import com.fongmi.android.tv.api.config.VodConfig; import com.fongmi.android.tv.api.config.VodConfig;
@@ -62,6 +63,15 @@ public class HomeActivity extends BaseActivity implements NavigationBarView.OnIt
@Override @Override
protected void initView(Bundle savedInstanceState) { protected void initView(Bundle savedInstanceState) {
// 检查隐私协议
if (!Setting.isPrivacyAgreed()) {
Intent intent = new Intent(this, PrivacyAgreementActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
finish();
return;
}
orientation = getResources().getConfiguration().orientation; orientation = getResources().getConfiguration().orientation;
Updater.create().release().start(this); Updater.create().release().start(this);
initFragment(savedInstanceState); initFragment(savedInstanceState);
@@ -21,6 +21,7 @@ import com.fongmi.android.tv.ui.base.BaseActivity;
import com.fongmi.android.tv.ui.dialog.SyncDialog; import com.fongmi.android.tv.ui.dialog.SyncDialog;
import com.fongmi.android.tv.utils.Notify; import com.fongmi.android.tv.utils.Notify;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.airbnb.lottie.LottieAnimationView;
import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;
@@ -63,6 +64,25 @@ public class KeepActivity extends BaseActivity implements KeepAdapter.OnClickLis
private void getKeep() { private void getKeep() {
mAdapter.addAll(Keep.getVod()); mAdapter.addAll(Keep.getVod());
mBinding.delete.setVisibility(mAdapter.getItemCount() > 0 ? View.VISIBLE : View.GONE); mBinding.delete.setVisibility(mAdapter.getItemCount() > 0 ? View.VISIBLE : View.GONE);
updateEmptyState();
}
private void updateEmptyState() {
boolean isEmpty = mAdapter.getItemCount() == 0;
mBinding.emptyLayout.getRoot().setVisibility(isEmpty ? View.VISIBLE : View.GONE);
mBinding.recycler.setVisibility(isEmpty ? View.GONE : View.VISIBLE);
// 控制Lottie动画播放
if (isEmpty) {
try {
LottieAnimationView lottieView = mBinding.emptyLayout.getRoot().findViewById(R.id.lottieAnimation);
if (lottieView != null) {
lottieView.playAnimation();
}
} catch (Exception e) {
// 忽略错误
}
}
} }
private void onSync(View view) { private void onSync(View view) {
@@ -71,7 +91,10 @@ public class KeepActivity extends BaseActivity implements KeepAdapter.OnClickLis
private void onDelete(View view) { private void onDelete(View view) {
if (mAdapter.isDelete()) { if (mAdapter.isDelete()) {
new MaterialAlertDialogBuilder(this).setTitle(R.string.dialog_delete_record).setMessage(R.string.dialog_delete_keep).setNegativeButton(R.string.dialog_negative, null).setPositiveButton(R.string.dialog_positive, (dialog, which) -> mAdapter.clear()).show(); new MaterialAlertDialogBuilder(this).setTitle(R.string.dialog_delete_record).setMessage(R.string.dialog_delete_keep).setNegativeButton(R.string.dialog_negative, null).setPositiveButton(R.string.dialog_positive, (dialog, which) -> {
mAdapter.clear();
updateEmptyState();
}).show();
} else if (mAdapter.getItemCount() > 0) { } else if (mAdapter.getItemCount() > 0) {
mAdapter.setDelete(true); mAdapter.setDelete(true);
} else { } else {
@@ -114,6 +137,7 @@ public class KeepActivity extends BaseActivity implements KeepAdapter.OnClickLis
if (mAdapter.getItemCount() > 0) return; if (mAdapter.getItemCount() > 0) return;
mBinding.delete.setVisibility(View.GONE); mBinding.delete.setVisibility(View.GONE);
mAdapter.setDelete(false); mAdapter.setDelete(false);
updateEmptyState();
} }
@Override @Override
@@ -0,0 +1,89 @@
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);
// 跳转到主界面,清除任务栈避免用户通过任务管理器回到协议页面
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();
}
}
@@ -45,6 +45,7 @@ import com.fongmi.android.tv.ui.activity.CollectActivity;
import com.fongmi.android.tv.ui.activity.HistoryActivity; import com.fongmi.android.tv.ui.activity.HistoryActivity;
import com.fongmi.android.tv.ui.activity.KeepActivity; import com.fongmi.android.tv.ui.activity.KeepActivity;
import com.fongmi.android.tv.ui.activity.VideoActivity; import com.fongmi.android.tv.ui.activity.VideoActivity;
import com.airbnb.lottie.LottieAnimationView;
import com.fongmi.android.tv.ui.adapter.TypeAdapter; import com.fongmi.android.tv.ui.adapter.TypeAdapter;
import com.fongmi.android.tv.ui.base.BaseFragment; import com.fongmi.android.tv.ui.base.BaseFragment;
import com.fongmi.android.tv.ui.dialog.ConfigDialog; import com.fongmi.android.tv.ui.dialog.ConfigDialog;
@@ -265,6 +266,15 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
} }
// 空源状态下隐藏所有悬浮按钮 // 空源状态下隐藏所有悬浮按钮
hideFabButtons(); hideFabButtons();
// 启动Lottie动画
try {
LottieAnimationView lottieView = mBinding.emptySourceHint.findViewById(R.id.lottieAnimation);
if (lottieView != null) {
lottieView.playAnimation();
}
} catch (Exception e) {
// 忽略错误
}
} }
} }
} }
@@ -154,5 +154,15 @@
app:spanCount="2" app:spanCount="2"
tools:listitem="@layout/adapter_vod_rect" /> tools:listitem="@layout/adapter_vod_rect" />
<!-- 搜索结果空状态Lottie动画 -->
<include
android:id="@+id/emptyLayout"
layout="@layout/view_empty_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginTop="-40dp"
android:visibility="gone" />
</RelativeLayout> </RelativeLayout>
</LinearLayout> </LinearLayout>
@@ -65,5 +65,15 @@
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:paddingBottom="8dp" /> android:paddingBottom="8dp" />
<!-- 空状态Lottie动画 -->
<include
android:id="@+id/emptyLayout"
layout="@layout/view_empty_lottie"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="-40dp"
android:visibility="gone" />
</FrameLayout> </FrameLayout>
</LinearLayout> </LinearLayout>
@@ -65,5 +65,14 @@
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:paddingBottom="8dp" /> android:paddingBottom="8dp" />
<include
android:id="@+id/emptyLayout"
layout="@layout/view_empty_keep"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="-40dp"
android:visibility="gone" />
</FrameLayout> </FrameLayout>
</LinearLayout> </LinearLayout>
+12 -9
View File
@@ -112,11 +112,13 @@
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> tools:visibility="visible">
<ImageView <com.airbnb.lottie.LottieAnimationView
android:layout_width="60dp" android:id="@+id/lottieAnimation"
android:layout_height="60dp" android:layout_width="180dp"
android:layout_marginBottom="16dp" android:layout_height="180dp"
android:src="@drawable/ic_logo" /> app:lottie_fileName="lottie_empty_1.json"
app:lottie_loop="true"
app:lottie_autoPlay="true" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -124,16 +126,17 @@
android:gravity="center" android:gravity="center"
android:text="@string/source_hint" android:text="@string/source_hint"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="16sp" /> android:textSize="14sp" />
<Button <Button
android:id="@+id/add_source_btn" android:id="@+id/add_source_btn"
android:layout_width="wrap_content" android:layout_width="200dp"
android:layout_height="wrap_content" android:layout_height="48dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:backgroundTint="@color/primary" android:backgroundTint="@color/primary"
android:text="@string/add_source" android:text="@string/add_source"
android:textColor="@color/black" /> android:textColor="@color/black"
android:textSize="16sp" />
</LinearLayout> </LinearLayout>
<ImageView <ImageView
Binary file not shown.