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:19:13 +08:00
parent dde56eeedb
commit ca95128ee9
28 changed files with 528 additions and 41 deletions
+7
View File
@@ -10,6 +10,13 @@
<application>
<activity
android:name=".ui.activity.PrivacyAgreementActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode|orientation"
android:exported="false"
android:screenOrientation="fullUser"
android:windowSoftInputMode="adjustPan" />
<activity
android:name=".ui.activity.HomeActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|uiMode|orientation"
@@ -56,6 +56,7 @@ public class Updater implements Download.Callback {
public Updater() {
this.download = Download.create("", getFile(), this);
this.forceCheck = false; // 默认不是用户主动检查更新
}
public Updater force() {
@@ -127,14 +128,14 @@ public class Updater implements Download.Callback {
String response = OkHttp.string(jsonUrl);
Logger.d("Update check response: " + response);
// 检查响应是否包含错误信息
// 检查响应是否包含错误信息,只有在用户主动检查更新时才显示错误提示
if (response.contains("rate limit exceeded")) {
App.post(() -> Notify.show("检查更新失败:API请求过于频繁,请稍后重试"));
showErrorIfForceCheck("检查更新失败:API请求过于频繁,请稍后重试");
return;
}
if (response.contains("Not Found Project") || response.contains("Not Found")) {
App.post(() -> Notify.show("检查更新失败:更新服务暂时不可用"));
showErrorIfForceCheck("检查更新失败:更新服务暂时不可用");
return;
}
@@ -157,8 +158,10 @@ public class Updater implements Download.Callback {
download = Download.create(downloadUrl, getFile(), this);
App.post(() -> show(activity, tagName, body));
} else if (downloadUrl != null) {
// 找到APK但不需要更新,提示已是最新版
App.post(() -> Notify.show("已是最新版本 " + tagName));
// 找到APK但不需要更新,只在用户主动检查更新时提示已是最新版
if (forceCheck) {
App.post(() -> Notify.show("已是最新版本 " + tagName));
}
Logger.d("Already latest version: " + tagName);
} else {
// 未找到对应的APK文件
@@ -171,17 +174,19 @@ public class Updater implements Download.Callback {
} catch (Exception e) {
Logger.e("Update check failed", e);
e.printStackTrace();
// 添加用户友好的错误提示
// 添加用户友好的错误提示,只有在用户主动检查更新时才显示
App.post(() -> {
String errorMsg = "检查更新失败";
if (e.getMessage() != null && e.getMessage().contains("rate limit")) {
errorMsg = "检查更新失败:请求过于频繁,请稍后重试";
} else if (e.getMessage() != null && e.getMessage().contains("Not Found")) {
errorMsg = "检查更新失败:更新服务暂时不可用";
} else {
errorMsg = "检查更新失败,请稍后重试";
if (forceCheck) { // 只有在用户主动检查更新时才显示错误提示
String errorMsg = "检查更新失败";
if (e.getMessage() != null && e.getMessage().contains("rate limit")) {
errorMsg = "检查更新失败:API请求过于频繁,请稍后重试"; // 统一错误提示文本
} else if (e.getMessage() != null && e.getMessage().contains("Not Found")) {
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
public void progress(int 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.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();
}
@@ -17,6 +17,7 @@ import com.fongmi.android.tv.ui.adapter.HistoryAdapter;
import com.fongmi.android.tv.ui.base.BaseActivity;
import com.fongmi.android.tv.ui.dialog.SyncDialog;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.airbnb.lottie.LottieAnimationView;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
@@ -59,6 +60,25 @@ public class HistoryActivity extends BaseActivity implements HistoryAdapter.OnCl
private void getHistory() {
mAdapter.addAll(History.get());
mBinding.delete.setVisibility(mAdapter.getItemCount() > 0 ? View.VISIBLE : View.GONE);
updateEmptyState();
}
private void updateEmptyState() {
boolean isEmpty = mAdapter.getItemCount() == 0;
mBinding.emptyLayout.getRoot().setVisibility(isEmpty ? View.VISIBLE : View.GONE);
mBinding.recycler.setVisibility(isEmpty ? View.GONE : View.VISIBLE);
// 控制Lottie动画播放
if (isEmpty) {
try {
LottieAnimationView lottieView = mBinding.emptyLayout.getRoot().findViewById(R.id.lottieAnimation);
if (lottieView != null) {
lottieView.playAnimation();
}
} catch (Exception e) {
// 忽略错误
}
}
}
private void onSync(View view) {
@@ -67,7 +87,10 @@ public class HistoryActivity extends BaseActivity implements HistoryAdapter.OnCl
private void onDelete(View view) {
if (mAdapter.isDelete()) {
new MaterialAlertDialogBuilder(this).setTitle(R.string.dialog_delete_record).setMessage(R.string.dialog_delete_history).setNegativeButton(R.string.dialog_negative, null).setPositiveButton(R.string.dialog_positive, (dialog, which) -> mAdapter.clear()).show();
new MaterialAlertDialogBuilder(this).setTitle(R.string.dialog_delete_record).setMessage(R.string.dialog_delete_history).setNegativeButton(R.string.dialog_negative, null).setPositiveButton(R.string.dialog_positive, (dialog, which) -> {
mAdapter.clear();
updateEmptyState();
}).show();
} else if (mAdapter.getItemCount() > 0) {
mAdapter.setDelete(true);
} else {
@@ -91,6 +114,7 @@ public class HistoryActivity extends BaseActivity implements HistoryAdapter.OnCl
if (mAdapter.getItemCount() > 0) return;
mBinding.delete.setVisibility(View.GONE);
mAdapter.setDelete(false);
updateEmptyState();
}
@Override
@@ -16,6 +16,7 @@ import androidx.viewbinding.ViewBinding;
import com.fongmi.android.tv.App;
import com.fongmi.android.tv.R;
import com.fongmi.android.tv.Setting;
import com.fongmi.android.tv.Updater;
import com.fongmi.android.tv.api.config.LiveConfig;
import com.fongmi.android.tv.api.config.VodConfig;
@@ -62,6 +63,15 @@ 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;
}
orientation = getResources().getConfiguration().orientation;
Updater.create().release().start(this);
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.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
@@ -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.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;
@@ -265,6 +266,15 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
}
// 空源状态下隐藏所有悬浮按钮
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"
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>
@@ -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>
@@ -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>
+12 -9
View File
@@ -112,11 +112,13 @@
android:visibility="gone"
tools:visibility="visible">
<ImageView
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginBottom="16dp"
android:src="@drawable/ic_logo" />
<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"
@@ -124,16 +126,17 @@
android:gravity="center"
android:text="@string/source_hint"
android:textColor="@color/white"
android:textSize="16sp" />
android:textSize="14sp" />
<Button
android:id="@+id/add_source_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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:textColor="@color/black"
android:textSize="16sp" />
</LinearLayout>
<ImageView