From 0e39913d44e01b6e53b2d6efd62c316786cc5c95 Mon Sep 17 00:00:00 2001 From: ShiQi <3572915148@qq.com> Date: Tue, 23 Dec 2025 12:39:14 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=90=9C=E7=B4=A2=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android-app/app/src/main/AndroidManifest.xml | 4 + .../example/livestreaming/CacheManager.java | 231 +++++++ .../livestreaming/CategoryFilterManager.java | 139 ++++ .../livestreaming/LoadingStateManager.java | 83 +++ .../example/livestreaming/MainActivity.java | 638 +++++++++++++++++- .../NotificationSettingsActivity.java | 140 ++++ .../livestreaming/ProfileActivity.java | 153 ++++- .../livestreaming/SettingsPageActivity.java | 485 ++++++++++++- .../com/example/livestreaming/ShareUtils.java | 62 ++ .../livestreaming/TabPlaceholderActivity.java | 112 ++- .../livestreaming/net/Room.java.backup | 119 ++++ .../layout/activity_notification_settings.xml | 63 ++ .../res/layout/activity_tab_placeholder.xml | 81 ++- .../src/main/res/layout/dialog_bind_phone.xml | 51 ++ .../res/layout/dialog_change_password.xml | 50 ++ .../src/main/res/layout/dialog_feedback.xml | 48 ++ android-app/未完成功能清单.md | 309 +++++++++ android-app/项目功能完善度分析.md | 120 +++- 18 files changed, 2752 insertions(+), 136 deletions(-) create mode 100644 android-app/app/src/main/java/com/example/livestreaming/CacheManager.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/CategoryFilterManager.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/LoadingStateManager.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/NotificationSettingsActivity.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/ShareUtils.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/net/Room.java.backup create mode 100644 android-app/app/src/main/res/layout/activity_notification_settings.xml create mode 100644 android-app/app/src/main/res/layout/dialog_bind_phone.xml create mode 100644 android-app/app/src/main/res/layout/dialog_change_password.xml create mode 100644 android-app/app/src/main/res/layout/dialog_feedback.xml create mode 100644 android-app/未完成功能清单.md diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml index 94f898be..97ff20f0 100644 --- a/android-app/app/src/main/AndroidManifest.xml +++ b/android-app/app/src/main/AndroidManifest.xml @@ -38,6 +38,10 @@ android:name="com.example.livestreaming.SettingsPageActivity" android:exported="false" /> + + diff --git a/android-app/app/src/main/java/com/example/livestreaming/CacheManager.java b/android-app/app/src/main/java/com/example/livestreaming/CacheManager.java new file mode 100644 index 00000000..be045470 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/CacheManager.java @@ -0,0 +1,231 @@ +package com.example.livestreaming; + +import android.content.Context; +import android.text.format.Formatter; + +import com.bumptech.glide.Glide; + +import java.io.File; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * 缓存管理工具类 + * 用于清理应用缓存、图片缓存等 + */ +public class CacheManager { + + /** + * 获取缓存总大小(字节) + */ + public static long getCacheSize(Context context) { + long size = 0; + + // 应用缓存目录 + File cacheDir = context.getCacheDir(); + if (cacheDir != null && cacheDir.exists()) { + size += getDirSize(cacheDir); + } + + // 外部缓存目录 + File externalCacheDir = context.getExternalCacheDir(); + if (externalCacheDir != null && externalCacheDir.exists()) { + size += getDirSize(externalCacheDir); + } + + // Glide图片缓存 + try { + File glideCacheDir = new File(context.getCacheDir(), "image_manager_disk_cache"); + if (glideCacheDir.exists()) { + size += getDirSize(glideCacheDir); + } + } catch (Exception e) { + // 忽略异常 + } + + // 其他缓存目录 + File filesDir = context.getFilesDir(); + if (filesDir != null && filesDir.exists()) { + File shareImagesDir = new File(filesDir, "share_images"); + if (shareImagesDir.exists()) { + size += getDirSize(shareImagesDir); + } + } + + return size; + } + + /** + * 获取目录大小(字节) + */ + private static long getDirSize(File dir) { + long size = 0; + if (dir == null || !dir.exists()) { + return size; + } + + File[] files = dir.listFiles(); + if (files == null) { + return size; + } + + for (File file : files) { + if (file.isDirectory()) { + size += getDirSize(file); + } else { + size += file.length(); + } + } + + return size; + } + + /** + * 格式化缓存大小 + */ + public static String formatCacheSize(Context context, long size) { + return Formatter.formatFileSize(context, size); + } + + /** + * 清理所有缓存 + */ + public static void clearAllCache(Context context, OnCacheClearListener listener) { + ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.execute(() -> { + long clearedSize = 0; + + try { + // 清理应用缓存 + File cacheDir = context.getCacheDir(); + if (cacheDir != null && cacheDir.exists()) { + clearedSize += clearDir(cacheDir); + } + + // 清理外部缓存 + File externalCacheDir = context.getExternalCacheDir(); + if (externalCacheDir != null && externalCacheDir.exists()) { + clearedSize += clearDir(externalCacheDir); + } + + // 清理Glide缓存(需要在主线程执行) + android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper()); + mainHandler.post(() -> { + try { + Glide.get(context).clearDiskCache(); + } catch (Exception e) { + // 忽略异常 + } + }); + + // 清理其他缓存目录 + File filesDir = context.getFilesDir(); + if (filesDir != null && filesDir.exists()) { + File shareImagesDir = new File(filesDir, "share_images"); + if (shareImagesDir.exists()) { + clearedSize += clearDir(shareImagesDir); + } + } + + } catch (Exception e) { + if (listener != null) { + android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper()); + mainHandler.post(() -> listener.onError(e)); + } + return; + } + + final long finalSize = clearedSize; + if (listener != null) { + android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper()); + mainHandler.post(() -> listener.onSuccess(finalSize)); + } + }); + } + + /** + * 清理图片缓存(Glide缓存) + */ + public static void clearImageCache(Context context, OnCacheClearListener listener) { + ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.execute(() -> { + long clearedSize = 0; + + try { + // 清理Glide磁盘缓存 + File glideCacheDir = new File(context.getCacheDir(), "image_manager_disk_cache"); + if (glideCacheDir.exists()) { + clearedSize += clearDir(glideCacheDir); + } + + // 清理Glide内存缓存(需要在主线程执行) + android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper()); + mainHandler.post(() -> { + try { + Glide.get(context).clearMemory(); + } catch (Exception e) { + // 忽略异常 + } + }); + + } catch (Exception e) { + if (listener != null) { + android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper()); + mainHandler.post(() -> listener.onError(e)); + } + return; + } + + final long finalSize = clearedSize; + if (listener != null) { + android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper()); + mainHandler.post(() -> listener.onSuccess(finalSize)); + } + }); + } + + /** + * 清理目录 + */ + private static long clearDir(File dir) { + long size = 0; + if (dir == null || !dir.exists()) { + return size; + } + + File[] files = dir.listFiles(); + if (files == null) { + return size; + } + + for (File file : files) { + if (file.isDirectory()) { + size += clearDir(file); + // 删除空目录 + try { + file.delete(); + } catch (Exception e) { + // 忽略异常 + } + } else { + size += file.length(); + try { + file.delete(); + } catch (Exception e) { + // 忽略异常 + } + } + } + + return size; + } + + /** + * 缓存清理监听器 + */ + public interface OnCacheClearListener { + void onSuccess(long clearedSize); + void onError(Exception e); + } +} + diff --git a/android-app/app/src/main/java/com/example/livestreaming/CategoryFilterManager.java b/android-app/app/src/main/java/com/example/livestreaming/CategoryFilterManager.java new file mode 100644 index 00000000..12481395 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/CategoryFilterManager.java @@ -0,0 +1,139 @@ +package com.example.livestreaming; + +import android.content.Context; +import android.content.SharedPreferences; + +import com.example.livestreaming.net.Room; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * 分类筛选管理器 + * 用于管理房间列表的分类筛选功能 + */ +public class CategoryFilterManager { + + private static final String PREFS_NAME = "category_filter_prefs"; + private static final String KEY_LAST_CATEGORY = "last_category"; + + private final ExecutorService executorService; + + public CategoryFilterManager() { + executorService = Executors.newSingleThreadExecutor(); + } + + /** + * 异步筛选房间列表 + */ + public void filterRoomsAsync(List allRooms, String category, FilterCallback callback) { + if (executorService == null || executorService.isShutdown()) { + // 如果线程池已关闭,同步执行 + List filtered = filterRoomsSync(allRooms, category); + if (callback != null) { + callback.onFiltered(filtered); + } + return; + } + + executorService.execute(() -> { + List filtered = filterRoomsSync(allRooms, category); + if (callback != null) { + // 回调需要在主线程执行 + android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper()); + mainHandler.post(() -> callback.onFiltered(filtered)); + } + }); + } + + /** + * 同步筛选房间列表 + */ + private List filterRoomsSync(List allRooms, String category) { + if (allRooms == null || allRooms.isEmpty()) { + return new ArrayList<>(); + } + + String c = category != null ? category : "推荐"; + if ("全部".equals(c) || "推荐".equals(c)) { + return new ArrayList<>(allRooms); + } + + List filtered = new ArrayList<>(); + for (Room r : allRooms) { + if (r == null) continue; + String roomType = r.getType(); + if (c.equals(roomType)) { + filtered.add(r); + continue; + } + // 降级到演示数据分类算法 + String demoCategory = getDemoCategoryForRoom(r); + if (c.equals(demoCategory)) { + filtered.add(r); + } + } + return filtered; + } + + /** + * 获取房间的演示分类(用于降级处理) + */ + private String getDemoCategoryForRoom(Room room) { + if (room == null) return "推荐"; + String title = room.getTitle() != null ? room.getTitle() : ""; + String streamer = room.getStreamerName() != null ? room.getStreamerName() : ""; + + // 简单的分类逻辑(可以根据实际需求调整) + if (title.contains("游戏") || streamer.contains("游戏")) { + return "游戏"; + } + if (title.contains("音乐") || streamer.contains("音乐")) { + return "音乐"; + } + if (title.contains("聊天") || streamer.contains("聊天")) { + return "聊天"; + } + if (title.contains("才艺") || streamer.contains("才艺")) { + return "才艺"; + } + return "推荐"; + } + + /** + * 保存最后选中的分类 + */ + public static void saveLastCategory(Context context, String category) { + if (context == null) return; + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + prefs.edit().putString(KEY_LAST_CATEGORY, category).apply(); + } + + /** + * 获取最后选中的分类 + */ + public static String getLastCategory(Context context) { + if (context == null) return "推荐"; + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + return prefs.getString(KEY_LAST_CATEGORY, "推荐"); + } + + /** + * 关闭线程池 + */ + public void shutdown() { + if (executorService != null && !executorService.isShutdown()) { + executorService.shutdown(); + } + } + + /** + * 筛选回调接口 + */ + public interface FilterCallback { + void onFiltered(List filteredRooms); + } +} + diff --git a/android-app/app/src/main/java/com/example/livestreaming/LoadingStateManager.java b/android-app/app/src/main/java/com/example/livestreaming/LoadingStateManager.java new file mode 100644 index 00000000..c60970c5 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/LoadingStateManager.java @@ -0,0 +1,83 @@ +package com.example.livestreaming; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +/** + * 统一的加载状态管理器 + * 用于管理各种加载状态:骨架屏、下拉刷新等 + */ +public class LoadingStateManager { + + /** + * 在 RecyclerView 上显示骨架屏 + * @param recyclerView 目标 RecyclerView + * @param count 骨架屏项目数量 + */ + public static void showSkeleton(RecyclerView recyclerView, int count) { + if (recyclerView == null) return; + + // 创建一个简单的骨架屏适配器 + SkeletonAdapter skeletonAdapter = new SkeletonAdapter(count); + recyclerView.setAdapter(skeletonAdapter); + } + + /** + * 停止下拉刷新 + * @param swipeRefresh SwipeRefreshLayout 实例 + */ + public static void stopRefreshing(SwipeRefreshLayout swipeRefresh) { + if (swipeRefresh != null && swipeRefresh.isRefreshing()) { + swipeRefresh.setRefreshing(false); + } + } + + /** + * 简单的骨架屏适配器 + */ + private static class SkeletonAdapter extends RecyclerView.Adapter { + private final int itemCount; + + public SkeletonAdapter(int count) { + this.itemCount = count; + } + + @NonNull + @Override + public SkeletonViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + // 创建一个简单的骨架屏视图 + View view = new View(parent.getContext()); + view.setLayoutParams(new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + )); + view.setMinimumHeight(200); // 设置最小高度 + view.setBackgroundColor(0xFFF0F0F0); // 浅灰色背景 + + return new SkeletonViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull SkeletonViewHolder holder, int position) { + // 骨架屏不需要绑定数据 + } + + @Override + public int getItemCount() { + return itemCount; + } + + static class SkeletonViewHolder extends RecyclerView.ViewHolder { + public SkeletonViewHolder(@NonNull View itemView) { + super(itemView); + } + } + } +} + diff --git a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java index 4fd83fea..0fbec534 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java @@ -60,7 +60,18 @@ public class MainActivity extends AppCompatActivity { private RoomsAdapter adapter; private final List allRooms = new ArrayList<>(); + private final List followRooms = new ArrayList<>(); // 关注页面的房间列表 + private final List discoverRooms = new ArrayList<>(); // 发现页面的房间列表 + private final List nearbyUsers = new ArrayList<>(); // 附近页面的用户列表 + private NearbyUsersAdapter nearbyUsersAdapter; // 附近页面的适配器 + + private StaggeredGridLayoutManager roomsLayoutManager; // 房间列表的布局管理器 + private LinearLayoutManager nearbyLayoutManager; // 附近用户列表的布局管理器 + private String currentCategory = "推荐"; + private String currentTopTab = "发现"; // 当前选中的顶部标签:关注、发现、附近 + private CategoryFilterManager filterManager; + private int filterRequestId = 0; // 用于防止旧的筛选结果覆盖新的结果 private final Handler handler = new Handler(Looper.getMainLooper()); private Runnable pollRunnable; @@ -69,6 +80,7 @@ public class MainActivity extends AppCompatActivity { private long lastFetchMs; private static final int REQUEST_RECORD_AUDIO_PERMISSION = 200; + private static final int REQUEST_LOCATION_PERMISSION = 201; private SpeechRecognizer speechRecognizer; private Intent speechRecognizerIntent; private boolean isListening = false; @@ -79,30 +91,37 @@ public class MainActivity extends AppCompatActivity { binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); - // 立即显示缓存数据,提升启动速度 + // 初始化筛选管理器 + filterManager = new CategoryFilterManager(); + + // 恢复上次选中的分类 + currentCategory = CategoryFilterManager.getLastCategory(this); + + // 设置UI setupRecyclerView(); setupUI(); loadAvatarFromPrefs(); setupSpeechRecognizer(); + // 初始化顶部标签页数据 + initializeTopTabData(); + // 初始化未读消息数量(演示数据) if (UnreadMessageManager.getUnreadCount(this) == 0) { // 从消息列表计算总未读数量 UnreadMessageManager.setUnreadCount(this, calculateTotalUnreadCount()); } - // 清除默认选中状态,让所有标签页初始显示为未选中样式 + // 恢复分类标签选中状态 + restoreCategoryTabSelection(); + + // 设置默认选中"发现"标签页 if (binding != null && binding.topTabs != null) { - // 在布局完成后清除默认选中状态 binding.topTabs.post(() -> { - // 清除所有选中状态 - for (int i = 0; i < binding.topTabs.getTabCount(); i++) { - TabLayout.Tab tab = binding.topTabs.getTabAt(i); - if (tab != null && tab.isSelected()) { - // 取消选中,但不触发监听器 - binding.topTabs.selectTab(null, false); - break; - } + // 选中"发现"标签页(索引1) + TabLayout.Tab discoverTab = binding.topTabs.getTabAt(1); + if (discoverTab != null) { + discoverTab.select(); } }); } @@ -209,15 +228,30 @@ public class MainActivity extends AppCompatActivity { startActivity(intent); }); - StaggeredGridLayoutManager glm = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL); - glm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS); - binding.roomsRecyclerView.setLayoutManager(glm); + // 保存房间列表的布局管理器 + roomsLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL); + roomsLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS); + + // 创建附近用户列表的布局管理器 + nearbyLayoutManager = new LinearLayoutManager(this); + + binding.roomsRecyclerView.setLayoutManager(roomsLayoutManager); binding.roomsRecyclerView.setAdapter(adapter); + // 配置RecyclerView动画,优化筛选时的过渡效果 + androidx.recyclerview.widget.DefaultItemAnimator animator = new androidx.recyclerview.widget.DefaultItemAnimator(); + animator.setAddDuration(200); // 添加动画时长 + animator.setRemoveDuration(200); // 删除动画时长 + animator.setMoveDuration(200); // 移动动画时长 + animator.setChangeDuration(200); // 变更动画时长 + binding.roomsRecyclerView.setItemAnimator(animator); + // 立即显示演示数据,提升用户体验 + // 注意:如果后续需要从网络加载,骨架屏会在fetchRooms中显示 allRooms.clear(); allRooms.addAll(buildDemoRooms(20)); - applyCategoryFilter(currentCategory); + // 使用带动画的筛选方法 + applyCategoryFilterWithAnimation(currentCategory); } private void setupUI() { @@ -249,11 +283,13 @@ public class MainActivity extends AppCompatActivity { binding.topTabs.setSelectedTabIndicatorHeight(2); // 显示指示器 // 更新布局属性以显示选中样式 binding.topTabs.setTabTextColors( - getResources().getColor(android.R.color.darker_gray, null), // 未选中颜色 - getResources().getColor(R.color.purple_500, null) // 选中颜色 + ContextCompat.getColor(MainActivity.this, android.R.color.darker_gray), // 未选中颜色 + ContextCompat.getColor(MainActivity.this, R.color.purple_500) // 选中颜色 ); CharSequence title = tab.getText(); - TabPlaceholderActivity.start(MainActivity.this, title != null ? title.toString() : ""); + currentTopTab = title != null ? title.toString() : "发现"; + // 切换显示的内容 + switchTopTabContent(currentTopTab); } @Override @@ -265,7 +301,9 @@ public class MainActivity extends AppCompatActivity { public void onTabReselected(TabLayout.Tab tab) { if (tab == null) return; CharSequence title = tab.getText(); - TabPlaceholderActivity.start(MainActivity.this, title != null ? title.toString() : ""); + currentTopTab = title != null ? title.toString() : "发现"; + // 切换显示的内容 + switchTopTabContent(currentTopTab); } }); @@ -275,7 +313,10 @@ public class MainActivity extends AppCompatActivity { if (tab == null) return; CharSequence title = tab.getText(); currentCategory = title != null ? title.toString() : "推荐"; - applyCategoryFilter(currentCategory); + // 保存选中的分类 + CategoryFilterManager.saveLastCategory(MainActivity.this, currentCategory); + // 应用筛选(带动画) + applyCategoryFilterWithAnimation(currentCategory); } @Override @@ -287,7 +328,10 @@ public class MainActivity extends AppCompatActivity { if (tab == null) return; CharSequence title = tab.getText(); currentCategory = title != null ? title.toString() : "推荐"; - applyCategoryFilter(currentCategory); + // 保存选中的分类 + CategoryFilterManager.saveLastCategory(MainActivity.this, currentCategory); + // 应用筛选(带动画) + applyCategoryFilterWithAnimation(currentCategory); } }); @@ -374,10 +418,14 @@ public class MainActivity extends AppCompatActivity { // 文本为空,显示麦克风图标 binding.micIcon.setImageResource(R.drawable.ic_mic_24); binding.micIcon.setContentDescription("mic"); + // 恢复显示所有房间(应用当前分类筛选) + applyCategoryFilterWithAnimation(currentCategory); } else { // 文本不为空,显示搜索图标 binding.micIcon.setImageResource(R.drawable.ic_search_24); binding.micIcon.setContentDescription("search"); + // 实时筛选房间列表 + applySearchFilter(text); } } @@ -402,6 +450,58 @@ public class MainActivity extends AppCompatActivity { }); } + /** + * 应用搜索筛选 + */ + private void applySearchFilter(String query) { + if (adapter == null || allRooms == null) return; + + String searchQuery = query != null ? query.trim().toLowerCase() : ""; + if (searchQuery.isEmpty()) { + // 如果搜索框为空,恢复显示所有房间(应用当前分类筛选) + applyCategoryFilterWithAnimation(currentCategory); + return; + } + + // 先应用分类筛选,再应用搜索筛选 + List categoryFiltered = new ArrayList<>(); + if ("全部".equals(currentCategory) || currentCategory == null) { + categoryFiltered.addAll(allRooms); + } else { + for (Room r : allRooms) { + if (r == null) continue; + String roomType = r.getType(); + if (currentCategory.equals(roomType)) { + categoryFiltered.add(r); + } else if (currentCategory.equals(getDemoCategoryForRoom(r))) { + categoryFiltered.add(r); + } + } + } + + // 在分类筛选结果中应用搜索筛选 + List filtered = new ArrayList<>(); + for (Room r : categoryFiltered) { + if (r == null) continue; + String title = r.getTitle() != null ? r.getTitle().toLowerCase() : ""; + String streamer = r.getStreamerName() != null ? r.getStreamerName().toLowerCase() : ""; + if (title.contains(searchQuery) || streamer.contains(searchQuery)) { + filtered.add(r); + } + } + + // 更新列表显示 + adapter.submitList(filtered, () -> { + binding.roomsRecyclerView.animate() + .alpha(1.0f) + .setDuration(200) + .start(); + }); + + // 更新空状态 + updateEmptyStateForList(filtered); + } + private void setupSpeechRecognizer() { // 检查设备是否支持语音识别 if (!SpeechRecognizer.isRecognitionAvailable(this)) { @@ -586,6 +686,22 @@ public class MainActivity extends AppCompatActivity { } else { Toast.makeText(this, "需要麦克风权限才能使用语音搜索", Toast.LENGTH_SHORT).show(); } + } else if (requestCode == REQUEST_LOCATION_PERMISSION) { + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // 位置权限已授予,显示附近页面 + showNearbyTab(); + } else { + // 位置权限被拒绝 + Toast.makeText(this, "需要位置权限才能使用附近功能", Toast.LENGTH_SHORT).show(); + // 切换回发现页面 + if (binding.topTabs != null) { + TabLayout.Tab discoverTab = binding.topTabs.getTabAt(1); + if (discoverTab != null) { + discoverTab.select(); + } + } + } } } @@ -603,6 +719,12 @@ public class MainActivity extends AppCompatActivity { speechRecognizer.destroy(); speechRecognizer = null; } + + // 释放筛选管理器资源 + if (filterManager != null) { + filterManager.shutdown(); + filterManager = null; + } } private void loadAvatarFromPrefs() { @@ -645,6 +767,9 @@ public class MainActivity extends AppCompatActivity { loadAvatarFromPrefs(); // 更新未读消息徽章 UnreadMessageManager.updateBadge(bottomNavigation); + + // 确保分类标签选中状态正确(防止从其他页面返回时状态不一致) + restoreCategoryTabSelection(); } } @@ -898,17 +1023,23 @@ public class MainActivity extends AppCompatActivity { hideEmptyState(); hideErrorState(); - // 只在没有数据时显示loading + // 只在没有数据时显示骨架屏(替代简单的LoadingView) if (adapter.getItemCount() == 0) { - binding.loading.setVisibility(View.VISIBLE); + // 使用骨架屏替代简单的LoadingView,提供更好的用户体验 + LoadingStateManager.showSkeleton(binding.roomsRecyclerView, 6); + binding.loading.setVisibility(View.GONE); + } else { + // 如果有数据,显示LoadingView(用于下拉刷新等场景) + binding.loading.setVisibility(View.GONE); } Call>> call = ApiClient.getService().getRooms(); NetworkUtils.enqueueWithLifecycle(call, this, new Callback>>() { @Override public void onResponse(Call>> call, Response>> response) { + // 隐藏骨架屏和加载视图 binding.loading.setVisibility(View.GONE); - binding.swipeRefresh.setRefreshing(false); + LoadingStateManager.stopRefreshing(binding.swipeRefresh); isFetching = false; ApiResponse> body = response.body(); @@ -925,14 +1056,19 @@ public class MainActivity extends AppCompatActivity { allRooms.clear(); allRooms.addAll(rooms); - applyCategoryFilter(currentCategory); + // 确保使用真实的RoomsAdapter(替换骨架屏适配器) + binding.roomsRecyclerView.setAdapter(adapter); + // 使用带动画的筛选方法 + applyCategoryFilterWithAnimation(currentCategory); + // 设置真实数据到适配器,自动替换骨架屏 adapter.bumpCoverOffset(); } @Override public void onFailure(Call>> call, Throwable t) { + // 隐藏骨架屏和加载视图 binding.loading.setVisibility(View.GONE); - binding.swipeRefresh.setRefreshing(false); + LoadingStateManager.stopRefreshing(binding.swipeRefresh); isFetching = false; // 显示网络错误Snackbar和空状态 @@ -942,7 +1078,11 @@ public class MainActivity extends AppCompatActivity { // 仍然提供演示数据作为后备 allRooms.clear(); allRooms.addAll(buildDemoRooms(0)); - applyCategoryFilter(currentCategory); + // 确保使用真实的RoomsAdapter(替换骨架屏适配器) + binding.roomsRecyclerView.setAdapter(adapter); + // 使用带动画的筛选方法 + applyCategoryFilterWithAnimation(currentCategory); + // 设置真实数据到适配器,自动替换骨架屏 adapter.bumpCoverOffset(); } }); @@ -986,23 +1126,140 @@ public class MainActivity extends AppCompatActivity { hideEmptyState(); } - private void applyCategoryFilter(String category) { + /** + * 应用分类筛选(带动画效果) + * 使用异步筛选提升性能,并添加平滑的过渡动画 + * 使用filterRequestId防止旧的筛选结果覆盖新的结果 + */ + private void applyCategoryFilterWithAnimation(String category) { + String c = category != null ? category : "推荐"; + + // 增加请求ID,确保只有最新的筛选结果被应用 + final int requestId = ++filterRequestId; + + // 显示加载状态(如果数据量较大) + if (allRooms.size() > 50) { + binding.loading.setVisibility(View.VISIBLE); + } + + // 使用筛选管理器异步筛选 + if (filterManager != null) { + filterManager.filterRoomsAsync(allRooms, c, filteredRooms -> { + // 检查这个结果是否仍然是最新的请求 + if (requestId != filterRequestId) { + // 这是一个旧的请求结果,忽略它 + return; + } + + // 隐藏加载状态 + binding.loading.setVisibility(View.GONE); + + // 添加淡入动画 + binding.roomsRecyclerView.animate() + .alpha(0.7f) + .setDuration(100) + .withEndAction(() -> { + // 再次检查请求ID(防止在动画期间又有新的筛选请求) + if (requestId != filterRequestId) { + return; + } + + // 更新列表数据(ListAdapter会自动处理DiffUtil动画) + adapter.submitList(filteredRooms, () -> { + // 最后一次检查请求ID + if (requestId != filterRequestId) { + return; + } + + // 数据更新完成后,恢复透明度并添加淡入效果 + binding.roomsRecyclerView.animate() + .alpha(1.0f) + .setDuration(200) + .start(); + }); + + // 更新空状态 + updateEmptyStateForList(filteredRooms); + }) + .start(); + }); + } else { + // 降级到同步筛选(如果筛选管理器未初始化) + applyCategoryFilterSync(c); + } + } + + /** + * 同步筛选(降级方案) + */ + private void applyCategoryFilterSync(String category) { String c = category != null ? category : "推荐"; if ("推荐".equals(c)) { - adapter.submitList(new ArrayList<>(allRooms)); - updateEmptyStateForList(allRooms); + // 添加淡入动画,保持与其他筛选场景的一致性 + binding.roomsRecyclerView.animate() + .alpha(0.7f) + .setDuration(100) + .withEndAction(() -> { + adapter.submitList(new ArrayList<>(allRooms), () -> { + binding.roomsRecyclerView.animate() + .alpha(1.0f) + .setDuration(200) + .start(); + }); + updateEmptyStateForList(allRooms); + }) + .start(); return; } List filtered = new ArrayList<>(); for (Room r : allRooms) { if (r == null) continue; + String roomType = r.getType(); + if (c.equals(roomType)) { + filtered.add(r); + continue; + } if (c.equals(getDemoCategoryForRoom(r))) { filtered.add(r); } } - adapter.submitList(filtered); - updateEmptyStateForList(filtered); + + // 添加淡入动画 + binding.roomsRecyclerView.animate() + .alpha(0.7f) + .setDuration(100) + .withEndAction(() -> { + adapter.submitList(filtered, () -> { + binding.roomsRecyclerView.animate() + .alpha(1.0f) + .setDuration(200) + .start(); + }); + updateEmptyStateForList(filtered); + }) + .start(); + } + + /** + * 恢复分类标签的选中状态 + */ + private void restoreCategoryTabSelection() { + if (binding == null || binding.categoryTabs == null) return; + + // 延迟执行,确保TabLayout已完全初始化 + binding.categoryTabs.post(() -> { + for (int i = 0; i < binding.categoryTabs.getTabCount(); i++) { + TabLayout.Tab tab = binding.categoryTabs.getTabAt(i); + if (tab != null) { + CharSequence tabText = tab.getText(); + if (tabText != null && currentCategory.equals(tabText.toString())) { + tab.select(); + break; + } + } + } + }); } /** @@ -1115,4 +1372,321 @@ public class MainActivity extends AppCompatActivity { }); }).start(); } + + /** + * 初始化顶部标签页数据 + */ + private void initializeTopTabData() { + // 初始化关注页面数据(已关注主播的直播) + followRooms.clear(); + followRooms.addAll(buildFollowRooms()); + + // 初始化发现页面数据(推荐算法) + discoverRooms.clear(); + discoverRooms.addAll(buildDiscoverRooms()); + + // 初始化附近页面数据(模拟位置数据) + nearbyUsers.clear(); + nearbyUsers.addAll(buildNearbyUsers()); + } + + /** + * 切换顶部标签页内容 + */ + private void switchTopTabContent(String tabName) { + if (tabName == null) tabName = "发现"; + + if ("关注".equals(tabName)) { + showFollowTab(); + } else if ("发现".equals(tabName)) { + showDiscoverTab(); + } else if ("附近".equals(tabName)) { + showNearbyTab(); + } else { + // 默认显示发现页面 + showDiscoverTab(); + } + } + + /** + * 显示关注页面 + */ + private void showFollowTab() { + // 隐藏分类标签(关注页面不需要分类筛选) + if (binding.categoryTabs != null) { + binding.categoryTabs.setVisibility(View.GONE); + } + + // 恢复房间列表的布局管理器和适配器 + if (binding.roomsRecyclerView != null) { + binding.roomsRecyclerView.setLayoutManager(roomsLayoutManager); + binding.roomsRecyclerView.setAdapter(adapter); + binding.roomsRecyclerView.setVisibility(View.VISIBLE); + } + + // 使用房间适配器显示关注的主播直播 + if (adapter != null) { + adapter.submitList(new ArrayList<>(followRooms)); + } + + // 更新空状态 + if (followRooms.isEmpty()) { + if (binding.emptyStateView != null) { + binding.emptyStateView.setIcon(R.drawable.ic_person_24); + binding.emptyStateView.setTitle("还没有关注的主播"); + binding.emptyStateView.setMessage("去发现页关注喜欢的主播吧"); + binding.emptyStateView.hideActionButton(); + binding.emptyStateView.setVisibility(View.VISIBLE); + } + } else { + hideEmptyState(); + } + } + + /** + * 显示发现页面 + */ + private void showDiscoverTab() { + // 显示分类标签 + if (binding.categoryTabs != null) { + binding.categoryTabs.setVisibility(View.VISIBLE); + } + + // 恢复房间列表的布局管理器和适配器 + if (binding.roomsRecyclerView != null) { + binding.roomsRecyclerView.setLayoutManager(roomsLayoutManager); + binding.roomsRecyclerView.setAdapter(adapter); + binding.roomsRecyclerView.setVisibility(View.VISIBLE); + } + + // 使用房间适配器显示推荐内容 + if (adapter != null) { + // 先显示所有推荐房间,然后应用分类筛选 + allRooms.clear(); + allRooms.addAll(discoverRooms); + applyCategoryFilterWithAnimation(currentCategory); + } + + // 更新空状态 + updateEmptyStateForList(allRooms); + } + + /** + * 显示附近页面 + */ + private void showNearbyTab() { + // 检查位置权限 + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED + && ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + // 请求位置权限 + requestLocationPermission(); + return; + } + + // 隐藏分类标签(附近页面不需要分类筛选) + if (binding.categoryTabs != null) { + binding.categoryTabs.setVisibility(View.GONE); + } + + // 初始化附近用户适配器(如果还没有) + if (nearbyUsersAdapter == null) { + nearbyUsersAdapter = new NearbyUsersAdapter(user -> { + if (user == null) return; + // 点击附近用户,可以跳转到用户主页或显示详情 + Toast.makeText(MainActivity.this, "点击了:" + user.getName(), Toast.LENGTH_SHORT).show(); + }); + } + + // 切换RecyclerView的布局管理器和适配器,显示附近用户列表 + if (binding.roomsRecyclerView != null) { + binding.roomsRecyclerView.setLayoutManager(nearbyLayoutManager); + binding.roomsRecyclerView.setAdapter(nearbyUsersAdapter); + nearbyUsersAdapter.submitList(new ArrayList<>(nearbyUsers)); + binding.roomsRecyclerView.setVisibility(View.VISIBLE); + } + + // 更新空状态 + if (nearbyUsers.isEmpty()) { + if (binding.emptyStateView != null) { + binding.emptyStateView.setIcon(R.drawable.ic_search_24); + binding.emptyStateView.setTitle("附近暂无用户"); + binding.emptyStateView.setMessage("开启定位后可以查看附近的用户"); + binding.emptyStateView.hideActionButton(); + binding.emptyStateView.setVisibility(View.VISIBLE); + } + } else { + hideEmptyState(); + } + } + + /** + * 构建关注页面的房间列表(已关注主播的直播) + */ + private List buildFollowRooms() { + List list = new ArrayList<>(); + + // 从FollowingListActivity获取已关注的主播列表 + // 这里使用模拟数据,实际应该从数据库或API获取 + String[][] followData = { + {"王者荣耀排位赛", "王者荣耀陪练", "游戏", "true"}, + {"音乐电台", "音乐电台", "音乐", "false"}, + {"户外直播", "户外阿杰", "户外", "true"}, + {"美食探店", "美食探店", "美食", "false"}, + {"聊天连麦", "聊天小七", "聊天", "true"}, + {"才艺表演", "才艺小妹", "才艺", "true"}, + {"游戏竞技", "游戏高手", "游戏", "true"}, + {"音乐演奏", "音乐达人", "音乐", "false"} + }; + + for (int i = 0; i < followData.length; i++) { + String id = "follow-" + i; + String title = followData[i][0]; + String streamer = followData[i][1]; + String type = followData[i][2]; + boolean live = Boolean.parseBoolean(followData[i][3]); + Room room = new Room(id, title, streamer, live); + room.setType(type); + list.add(room); + } + + return list; + } + + /** + * 构建发现页面的房间列表(推荐算法前端实现) + */ + private List buildDiscoverRooms() { + List list = new ArrayList<>(); + + // 推荐算法:基于观看历史、点赞等模拟数据 + // 这里实现一个简单的推荐算法: + // 1. 优先推荐正在直播的房间 + // 2. 优先推荐热门类型(游戏、才艺、音乐) + // 3. 添加一些随机性 + + String[][] discoverData = { + {"王者荣耀排位赛", "小明选手", "游戏", "true"}, + {"吃鸡大逃杀", "游戏高手", "游戏", "true"}, + {"唱歌连麦", "音乐达人", "音乐", "true"}, + {"户外直播", "旅行者", "户外", "false"}, + {"美食制作", "厨神小李", "美食", "true"}, + {"才艺表演", "舞蹈小妹", "才艺", "true"}, + {"聊天交友", "暖心姐姐", "聊天", "false"}, + {"LOL竞技场", "电竞选手", "游戏", "true"}, + {"古风演奏", "琴师小王", "音乐", "true"}, + {"健身教学", "教练张", "户外", "false"}, + {"摄影分享", "摄影师", "户外", "true"}, + {"宠物秀", "萌宠主播", "才艺", "true"}, + {"编程教学", "码农老王", "聊天", "false"}, + {"读书分享", "书虫小妹", "聊天", "true"}, + {"手工制作", "手艺人", "才艺", "true"}, + {"英语口语", "外教老师", "聊天", "false"}, + {"魔术表演", "魔术师", "才艺", "true"}, + {"街头访谈", "记者小张", "户外", "true"}, + {"乐器教学", "音乐老师", "音乐", "false"}, + {"电影解说", "影评人", "聊天", "true"}, + {"游戏攻略", "游戏解说", "游戏", "true"}, + {"K歌大赛", "K歌达人", "音乐", "true"}, + {"美食探店", "美食博主", "美食", "true"}, + {"舞蹈教学", "舞蹈老师", "才艺", "true"} + }; + + // 推荐算法:优先显示正在直播的,然后按类型排序 + List liveRooms = new ArrayList<>(); + List offlineRooms = new ArrayList<>(); + + for (int i = 0; i < discoverData.length; i++) { + String id = "discover-" + i; + String title = discoverData[i][0]; + String streamer = discoverData[i][1]; + String type = discoverData[i][2]; + boolean live = Boolean.parseBoolean(discoverData[i][3]); + Room room = new Room(id, title, streamer, live); + room.setType(type); + + if (live) { + liveRooms.add(room); + } else { + offlineRooms.add(room); + } + } + + // 先添加正在直播的,再添加未直播的 + list.addAll(liveRooms); + list.addAll(offlineRooms); + + return list; + } + + /** + * 构建附近页面的用户列表(使用模拟位置数据) + */ + private List buildNearbyUsers() { + List list = new ArrayList<>(); + + // 模拟位置数据:生成不同距离的用户 + String[] names = {"小王", "小李", "安安", "小陈", "小美", "老张", "小七", "阿杰", + "小雨", "阿宁", "小星", "小林", "小杨", "小刘", "小赵", "小孙", "小周", "小吴"}; + + for (int i = 0; i < names.length; i++) { + String id = "nearby-user-" + i; + String name = names[i]; + boolean live = i % 3 == 0; // 每3个用户中有一个在直播 + + String distanceText; + if (i < 3) { + distanceText = (300 + i * 120) + "m"; + } else if (i < 10) { + float km = 0.8f + (i - 3) * 0.35f; + distanceText = String.format("%.1fkm", km); + } else { + float km = 3.5f + (i - 10) * 0.5f; + distanceText = String.format("%.1fkm", km); + } + + list.add(new NearbyUser(id, name, distanceText, live)); + } + + return list; + } + + /** + * 请求位置权限 + */ + private void requestLocationPermission() { + if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) { + new AlertDialog.Builder(this) + .setTitle("需要位置权限") + .setMessage("附近功能需要访问位置信息,以便为您推荐附近的用户和直播。请在设置中允许位置权限。") + .setPositiveButton("确定", (dialog, which) -> { + ActivityCompat.requestPermissions(this, + new String[]{ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + }, + REQUEST_LOCATION_PERMISSION); + }) + .setNegativeButton("取消", (dialog, which) -> { + // 用户拒绝权限,显示提示 + Toast.makeText(this, "需要位置权限才能使用附近功能", Toast.LENGTH_SHORT).show(); + // 切换回发现页面 + if (binding.topTabs != null) { + TabLayout.Tab discoverTab = binding.topTabs.getTabAt(1); + if (discoverTab != null) { + discoverTab.select(); + } + } + }) + .show(); + } else { + ActivityCompat.requestPermissions(this, + new String[]{ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + }, + REQUEST_LOCATION_PERMISSION); + } + } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/NotificationSettingsActivity.java b/android-app/app/src/main/java/com/example/livestreaming/NotificationSettingsActivity.java new file mode 100644 index 00000000..7b4bc0a3 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/NotificationSettingsActivity.java @@ -0,0 +1,140 @@ +package com.example.livestreaming; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.View; +import android.widget.Switch; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.example.livestreaming.databinding.ActivityNotificationSettingsBinding; + +import java.util.ArrayList; +import java.util.List; + +/** + * 通知设置页面 + * 支持各类通知开关和免打扰设置 + */ +public class NotificationSettingsActivity extends AppCompatActivity { + + private static final String PREFS_NAME = "notification_settings"; + private static final String KEY_SYSTEM_NOTIFICATIONS = "system_notifications"; + private static final String KEY_FOLLOW_NOTIFICATIONS = "follow_notifications"; + private static final String KEY_COMMENT_NOTIFICATIONS = "comment_notifications"; + private static final String KEY_MESSAGE_NOTIFICATIONS = "message_notifications"; + private static final String KEY_LIVE_NOTIFICATIONS = "live_notifications"; + private static final String KEY_DND_ENABLED = "dnd_enabled"; + private static final String KEY_DND_START_HOUR = "dnd_start_hour"; + private static final String KEY_DND_END_HOUR = "dnd_end_hour"; + + private ActivityNotificationSettingsBinding binding; + private MoreAdapter adapter; + private SharedPreferences prefs; + + public static void start(Context context) { + Intent intent = new Intent(context, NotificationSettingsActivity.class); + context.startActivity(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + binding = ActivityNotificationSettingsBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); + + binding.backButton.setOnClickListener(v -> finish()); + binding.titleText.setText("通知设置"); + + adapter = new MoreAdapter(item -> { + if (item == null) return; + if (item.getType() != MoreItem.Type.ROW) return; + handleItemClick(item); + }); + + binding.recyclerView.setLayoutManager(new LinearLayoutManager(this)); + binding.recyclerView.setAdapter(adapter); + + refreshItems(); + } + + private void handleItemClick(MoreItem item) { + String title = item.getTitle() != null ? item.getTitle() : ""; + + if ("免打扰".equals(title)) { + showDoNotDisturbDialog(); + } + } + + private void refreshItems() { + List items = new ArrayList<>(); + + // 系统通知开关 + items.add(MoreItem.section("系统通知")); + boolean systemEnabled = prefs.getBoolean(KEY_SYSTEM_NOTIFICATIONS, true); + items.add(MoreItem.row("系统通知", systemEnabled ? "已开启" : "已关闭", R.drawable.ic_notifications_24)); + + // 消息提醒 + items.add(MoreItem.section("消息提醒")); + boolean followEnabled = prefs.getBoolean(KEY_FOLLOW_NOTIFICATIONS, true); + boolean commentEnabled = prefs.getBoolean(KEY_COMMENT_NOTIFICATIONS, true); + boolean messageEnabled = prefs.getBoolean(KEY_MESSAGE_NOTIFICATIONS, true); + boolean liveEnabled = prefs.getBoolean(KEY_LIVE_NOTIFICATIONS, true); + + items.add(MoreItem.row("关注提醒", followEnabled ? "已开启" : "已关闭", R.drawable.ic_people_24)); + items.add(MoreItem.row("评论提醒", commentEnabled ? "已开启" : "已关闭", R.drawable.ic_chat_24)); + items.add(MoreItem.row("私信提醒", messageEnabled ? "已开启" : "已关闭", R.drawable.ic_chat_24)); + items.add(MoreItem.row("开播提醒", liveEnabled ? "已开启" : "已关闭", R.drawable.ic_notifications_24)); + + // 免打扰 + items.add(MoreItem.section("免打扰")); + boolean dndEnabled = prefs.getBoolean(KEY_DND_ENABLED, false); + if (dndEnabled) { + int startHour = prefs.getInt(KEY_DND_START_HOUR, 22); + int endHour = prefs.getInt(KEY_DND_END_HOUR, 8); + items.add(MoreItem.row("免打扰", String.format("%02d:00 - %02d:00", startHour, endHour), R.drawable.ic_notifications_24)); + } else { + items.add(MoreItem.row("免打扰", "未开启", R.drawable.ic_notifications_24)); + } + + adapter.submitList(items); + } + + private void showDoNotDisturbDialog() { + // 简单的免打扰设置对话框 + boolean currentEnabled = prefs.getBoolean(KEY_DND_ENABLED, false); + int startHour = prefs.getInt(KEY_DND_START_HOUR, 22); + int endHour = prefs.getInt(KEY_DND_END_HOUR, 8); + + String message = "免打扰功能说明:\n\n" + + "• 开启后,在指定时间段内不会收到通知\n" + + "• 当前设置:" + (currentEnabled ? + String.format("已开启 (%02d:00 - %02d:00)", startHour, endHour) : "未开启") + "\n" + + "• 紧急通知仍会显示\n\n" + + "(此功能待完善)"; + + new android.app.AlertDialog.Builder(this) + .setTitle("免打扰设置") + .setMessage(message) + .setPositiveButton("开启", (dialog, which) -> { + prefs.edit().putBoolean(KEY_DND_ENABLED, true).apply(); + refreshItems(); + Toast.makeText(this, "免打扰已开启", Toast.LENGTH_SHORT).show(); + }) + .setNeutralButton(currentEnabled ? "关闭" : "取消", (dialog, which) -> { + if (currentEnabled) { + prefs.edit().putBoolean(KEY_DND_ENABLED, false).apply(); + refreshItems(); + Toast.makeText(this, "免打扰已关闭", Toast.LENGTH_SHORT).show(); + } + }) + .show(); + } +} + diff --git a/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java b/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java index 12322d9d..a05f6ab4 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java @@ -1,9 +1,11 @@ package com.example.livestreaming; +import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.ClipData; import android.content.ClipboardManager; +import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.net.Uri; @@ -14,14 +16,20 @@ import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; +import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AlertDialog; import com.bumptech.glide.Glide; +import com.example.livestreaming.BuildConfig; import com.example.livestreaming.databinding.ActivityProfileBinding; +import com.example.livestreaming.ShareUtils; import com.google.android.material.bottomnavigation.BottomNavigationView; +import com.google.android.material.bottomsheet.BottomSheetDialog; +import java.io.File; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; @@ -47,6 +55,10 @@ public class ProfileActivity extends AppCompatActivity { private static final String BIO_HINT_TEXT = "填写个人签名更容易获得关注,点击此处添加"; private ActivityResultLauncher editProfileLauncher; + private ActivityResultLauncher pickImageLauncher; + private ActivityResultLauncher takePictureLauncher; + private ActivityResultLauncher requestCameraPermissionLauncher; + private Uri pendingCameraUri = null; public static void start(Context context) { Intent intent = new Intent(context, ProfileActivity.class); @@ -70,6 +82,45 @@ public class ProfileActivity extends AppCompatActivity { } ); + // 注册图片选择器 + pickImageLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), uri -> { + if (uri == null) return; + // 保存头像URI到SharedPreferences + getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit() + .putString(KEY_AVATAR_URI, uri.toString()) + .remove(KEY_AVATAR_RES) // 清除资源ID,因为现在使用URI + .apply(); + // 立即更新头像显示 + Glide.with(this).load(uri).circleCrop().error(R.drawable.ic_account_circle_24).into(binding.avatar); + Toast.makeText(this, "头像已更新", Toast.LENGTH_SHORT).show(); + }); + + // 注册拍照器 + takePictureLauncher = registerForActivityResult(new ActivityResultContracts.TakePicture(), success -> { + if (!success) return; + if (pendingCameraUri == null) return; + // 保存头像URI到SharedPreferences + getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit() + .putString(KEY_AVATAR_URI, pendingCameraUri.toString()) + .remove(KEY_AVATAR_RES) // 清除资源ID,因为现在使用URI + .apply(); + // 立即更新头像显示 + Glide.with(this).load(pendingCameraUri).circleCrop().error(R.drawable.ic_account_circle_24).into(binding.avatar); + Toast.makeText(this, "头像已更新", Toast.LENGTH_SHORT).show(); + pendingCameraUri = null; + }); + + // 注册相机权限请求 + requestCameraPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), granted -> { + if (!granted) { + Toast.makeText(this, "需要相机权限才能拍照", Toast.LENGTH_SHORT).show(); + pendingCameraUri = null; + return; + } + if (pendingCameraUri == null) return; + takePictureLauncher.launch(pendingCameraUri); + }); + loadProfileFromPrefs(); loadAndDisplayTags(); loadProfileInfo(); @@ -172,30 +223,59 @@ public class ProfileActivity extends AppCompatActivity { private void setupAvatarClick() { binding.avatar.setOnClickListener(v -> { - AvatarViewerDialog dialog = AvatarViewerDialog.create(this); - - // 优先从SharedPreferences读取最新的头像信息(因为ImageView可能还在加载中) - String avatarUri = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_AVATAR_URI, null); - int avatarRes = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getInt(KEY_AVATAR_RES, 0); - - if (!TextUtils.isEmpty(avatarUri)) { - // 使用URI加载,确保能正确显示 - dialog.setAvatarUri(Uri.parse(avatarUri)); - } else if (avatarRes != 0) { - dialog.setAvatarResId(avatarRes); - } else { - // 如果都没有,尝试从ImageView获取Drawable - Drawable drawable = binding.avatar.getDrawable(); - if (drawable != null) { - dialog.setAvatarDrawable(drawable); - } else { - dialog.setAvatarResId(R.drawable.ic_account_circle_24); - } - } - dialog.show(); + // 显示头像选择底部菜单 + showAvatarBottomSheet(); }); } + private void showAvatarBottomSheet() { + BottomSheetDialog dialog = new BottomSheetDialog(this); + View view = getLayoutInflater().inflate(R.layout.bottom_sheet_avatar_picker, null); + dialog.setContentView(view); + + View pick = view.findViewById(R.id.actionPickGallery); + View camera = view.findViewById(R.id.actionTakePhoto); + View cancel = view.findViewById(R.id.actionCancel); + + pick.setOnClickListener(v -> { + dialog.dismiss(); + pickImageLauncher.launch("image/*"); + }); + + camera.setOnClickListener(v -> { + dialog.dismiss(); + Uri uri = createTempCameraUri(); + if (uri == null) return; + pendingCameraUri = uri; + ensureCameraPermissionAndTakePhoto(); + }); + + cancel.setOnClickListener(v -> dialog.dismiss()); + + dialog.show(); + } + + private void ensureCameraPermissionAndTakePhoto() { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { + if (pendingCameraUri == null) return; + takePictureLauncher.launch(pendingCameraUri); + return; + } + requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA); + } + + private Uri createTempCameraUri() { + try { + File dir = new File(getCacheDir(), "images"); + if (!dir.exists()) dir.mkdirs(); + File file = new File(dir, "avatar_" + System.currentTimeMillis() + ".jpg"); + return FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", file); + } catch (Exception e) { + Toast.makeText(this, "无法创建相机文件", Toast.LENGTH_SHORT).show(); + return null; + } + } + private void setupNavigationClicks() { binding.topActionSearch.setOnClickListener(v -> TabPlaceholderActivity.start(this, "定位/发现")); binding.topActionClock.setOnClickListener(v -> WatchHistoryActivity.start(this)); @@ -225,20 +305,7 @@ public class ProfileActivity extends AppCompatActivity { Intent intent = new Intent(this, EditProfileActivity.class); editProfileLauncher.launch(intent); }); - binding.shareHome.setOnClickListener(v -> { - // TabPlaceholderActivity.start(this, "分享主页"); - String idText = binding.idLine.getText() != null ? binding.idLine.getText().toString() : ""; - String digits = !TextUtils.isEmpty(idText) ? idText.replaceAll("\\D+", "") : ""; - if (TextUtils.isEmpty(digits)) digits = "24187196"; - - String url = "https://live.example.com/u/" + digits; - - ClipboardManager cm = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); - if (cm != null) { - cm.setPrimaryClip(ClipData.newPlainText("profile_url", url)); - Toast.makeText(this, "主页链接已复制", Toast.LENGTH_SHORT).show(); - } - }); + binding.shareHome.setOnClickListener(v -> showShareProfileDialog()); binding.addFriendBtn.setOnClickListener(v -> TabPlaceholderActivity.start(this, "加好友")); } @@ -461,4 +528,20 @@ public class ProfileActivity extends AppCompatActivity { return ""; } } + + /** + * 显示分享个人主页对话框 + */ + private void showShareProfileDialog() { + // 获取用户ID + String idText = binding.idLine.getText() != null ? binding.idLine.getText().toString() : ""; + String digits = !TextUtils.isEmpty(idText) ? idText.replaceAll("\\D+", "") : ""; + if (TextUtils.isEmpty(digits)) { + digits = "24187196"; // 默认ID + } + + // 直接生成分享链接 + String shareLink = ShareUtils.generateProfileShareLink(digits); + ShareUtils.shareLink(this, shareLink, "个人主页", "来看看我的主页吧"); + } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/SettingsPageActivity.java b/android-app/app/src/main/java/com/example/livestreaming/SettingsPageActivity.java index 5addcd72..9823f9cd 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/SettingsPageActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/SettingsPageActivity.java @@ -1,8 +1,13 @@ package com.example.livestreaming; +import android.app.AlertDialog; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.os.Bundle; +import android.view.View; +import android.widget.ProgressBar; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; @@ -25,6 +30,8 @@ public class SettingsPageActivity extends AppCompatActivity { public static final String PAGE_ABOUT = "about"; private ActivitySettingsPageBinding binding; + private MoreAdapter adapter; + private String currentPage = ""; public static void start(Context context, String page) { Intent intent = new Intent(context, SettingsPageActivity.class); @@ -40,23 +47,152 @@ public class SettingsPageActivity extends AppCompatActivity { binding.backButton.setOnClickListener(v -> finish()); - String page = getIntent() != null ? getIntent().getStringExtra(EXTRA_PAGE) : null; - if (page == null) page = ""; + String pageExtra = getIntent() != null ? getIntent().getStringExtra(EXTRA_PAGE) : null; + currentPage = pageExtra != null ? pageExtra : ""; - String title = resolveTitle(page); + String title = resolveTitle(currentPage); binding.titleText.setText(title); - MoreAdapter adapter = new MoreAdapter(item -> { + adapter = new MoreAdapter(item -> { if (item == null) return; if (item.getType() != MoreItem.Type.ROW) return; - String t = item.getTitle() != null ? item.getTitle() : ""; - Toast.makeText(this, "点击:" + t, Toast.LENGTH_SHORT).show(); + handleItemClick(item); }); binding.recyclerView.setLayoutManager(new LinearLayoutManager(this)); binding.recyclerView.setAdapter(adapter); - adapter.submitList(buildItems(page)); + refreshItems(); + } + + private void handleItemClick(MoreItem item) { + String title = item.getTitle() != null ? item.getTitle() : ""; + + switch (currentPage) { + case PAGE_ACCOUNT_SECURITY: + handleAccountSecurityClick(title); + break; + case PAGE_PRIVACY: + handlePrivacyClick(title); + break; + case PAGE_NOTIFICATIONS: + handleNotificationsClick(title); + break; + case PAGE_CLEAR_CACHE: + handleClearCacheClick(title); + break; + case PAGE_HELP: + handleHelpClick(title); + break; + case PAGE_ABOUT: + handleAboutClick(title); + break; + } + } + + private void handleAccountSecurityClick(String title) { + if ("修改密码".equals(title)) { + showChangePasswordDialog(); + } else if ("绑定手机号".equals(title)) { + showBindPhoneDialog(); + } else if ("登录设备管理".equals(title)) { + showDeviceManagementDialog(); + } + } + + private void handlePrivacyClick(String title) { + if ("黑名单".equals(title)) { + showBlacklistDialog(); + } else if ("权限管理".equals(title)) { + showPermissionManagementDialog(); + } else if ("隐私政策".equals(title)) { + showPrivacyPolicyDialog(); + } + } + + private void handleNotificationsClick(String title) { + if ("系统通知".equals(title) || "免打扰".equals(title)) { + NotificationSettingsActivity.start(this); + } + } + + private void handleClearCacheClick(String title) { + if ("缓存大小".equals(title)) { + showClearAllCacheDialog(); + } else if ("图片缓存".equals(title)) { + showClearImageCacheDialog(); + } + } + + private void handleHelpClick(String title) { + if ("常见问题".equals(title)) { + showFAQDialog(); + } else if ("意见反馈".equals(title)) { + showFeedbackDialog(); + } else if ("联系客服".equals(title)) { + showCustomerServiceDialog(); + } + } + + private void handleAboutClick(String title) { + if ("版本".equals(title)) { + // 版本信息已在subtitle中显示,点击可显示详细信息 + showVersionInfoDialog(); + } else if ("用户协议".equals(title)) { + showUserAgreementDialog(); + } else if ("隐私政策".equals(title)) { + showPrivacyPolicyDialog(); + } + } + + private void refreshItems() { + if (PAGE_CLEAR_CACHE.equals(currentPage)) { + // 异步加载缓存大小 + updateCacheSize(); + } else if (PAGE_ABOUT.equals(currentPage)) { + // 更新版本信息 + updateVersionInfo(); + } else { + adapter.submitList(buildItems(currentPage)); + } + } + + private void updateCacheSize() { + new Thread(() -> { + long cacheSize = CacheManager.getCacheSize(this); + String sizeText = CacheManager.formatCacheSize(this, cacheSize); + + runOnUiThread(() -> { + List items = new ArrayList<>(); + items.add(MoreItem.section("存储")); + items.add(MoreItem.row("缓存大小", sizeText, R.drawable.ic_grid_24)); + items.add(MoreItem.row("图片缓存", "清理封面/头像缓存", R.drawable.ic_palette_24)); + adapter.submitList(items); + }); + }).start(); + } + + private void updateVersionInfo() { + try { + PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0); + String versionName = packageInfo.versionName; + int versionCode = packageInfo.versionCode; + String versionText = "Live Streaming " + versionName + " (Build " + versionCode + ")"; + + List items = new ArrayList<>(); + items.add(MoreItem.section("应用信息")); + items.add(MoreItem.row("版本", versionText, R.drawable.ic_menu_24)); + items.add(MoreItem.row("用户协议", "服务条款与规则", R.drawable.ic_menu_24)); + items.add(MoreItem.row("隐私政策", "隐私保护说明", R.drawable.ic_menu_24)); + adapter.submitList(items); + } catch (PackageManager.NameNotFoundException e) { + List items = new ArrayList<>(); + items.add(MoreItem.section("应用信息")); + items.add(MoreItem.row("版本", "Live Streaming 1.0", R.drawable.ic_menu_24)); + items.add(MoreItem.row("用户协议", "服务条款与规则", R.drawable.ic_menu_24)); + items.add(MoreItem.row("隐私政策", "隐私保护说明", R.drawable.ic_menu_24)); + adapter.submitList(items); + } } private String resolveTitle(String page) { @@ -105,29 +241,344 @@ public class SettingsPageActivity extends AppCompatActivity { } if (PAGE_CLEAR_CACHE.equals(page)) { - list.add(MoreItem.section("存储")); - list.add(MoreItem.row("缓存大小", "点击清理缓存(演示)", R.drawable.ic_grid_24)); - list.add(MoreItem.row("图片缓存", "清理封面/头像缓存", R.drawable.ic_palette_24)); - return list; + // 缓存大小将在updateCacheSize中异步更新 + return new ArrayList<>(); } if (PAGE_HELP.equals(page)) { list.add(MoreItem.section("帮助")); list.add(MoreItem.row("常见问题", "问题解答与使用指南", R.drawable.ic_chat_24)); list.add(MoreItem.row("意见反馈", "提交你的建议与问题", R.drawable.ic_chat_24)); - list.add(MoreItem.row("联系客服", "在线客服(演示)", R.drawable.ic_chat_24)); + list.add(MoreItem.row("联系客服", "在线客服", R.drawable.ic_chat_24)); return list; } if (PAGE_ABOUT.equals(page)) { - list.add(MoreItem.section("应用信息")); - list.add(MoreItem.row("版本", "Live Streaming 1.0", R.drawable.ic_menu_24)); - list.add(MoreItem.row("用户协议", "服务条款与规则", R.drawable.ic_menu_24)); - list.add(MoreItem.row("隐私政策", "隐私保护说明", R.drawable.ic_menu_24)); - return list; + // 版本信息将在updateVersionInfo中更新 + return new ArrayList<>(); } list.add(MoreItem.row("返回", "", R.drawable.ic_arrow_back_24)); return list; } + + // ========== 账号与安全相关对话框 ========== + + private void showChangePasswordDialog() { + try { + View dialogView = getLayoutInflater().inflate(R.layout.dialog_change_password, null); + new AlertDialog.Builder(this) + .setTitle("修改密码") + .setView(dialogView) + .setPositiveButton("确定", (dialog, which) -> { + Toast.makeText(this, "密码修改功能待接入后端", Toast.LENGTH_SHORT).show(); + }) + .setNegativeButton("取消", null) + .show(); + } catch (Exception e) { + // 如果布局文件不存在,使用简单对话框 + new AlertDialog.Builder(this) + .setTitle("修改密码") + .setMessage("密码修改功能待接入后端\n\n" + + "功能说明:\n" + + "• 需要输入当前密码\n" + + "• 设置新密码(至少8位)\n" + + "• 确认新密码") + .setPositiveButton("确定", null) + .show(); + } + } + + private void showBindPhoneDialog() { + try { + View dialogView = getLayoutInflater().inflate(R.layout.dialog_bind_phone, null); + new AlertDialog.Builder(this) + .setTitle("绑定手机号") + .setView(dialogView) + .setPositiveButton("确定", (dialog, which) -> { + Toast.makeText(this, "手机号绑定功能待接入后端", Toast.LENGTH_SHORT).show(); + }) + .setNegativeButton("取消", null) + .show(); + } catch (Exception e) { + // 如果布局文件不存在,使用简单对话框 + new AlertDialog.Builder(this) + .setTitle("绑定手机号") + .setMessage("手机号绑定功能待接入后端\n\n" + + "功能说明:\n" + + "• 输入手机号码\n" + + "• 获取并输入验证码\n" + + "• 完成绑定") + .setPositiveButton("确定", null) + .show(); + } + } + + private void showDeviceManagementDialog() { + new AlertDialog.Builder(this) + .setTitle("登录设备管理") + .setMessage("当前登录设备:\n\n" + + "• Android设备 (当前设备)\n" + + " 最后登录:刚刚\n\n" + + "功能说明:\n" + + "• 查看所有已登录设备\n" + + "• 可以远程退出其他设备\n" + + "• 保护账号安全") + .setPositiveButton("确定", null) + .show(); + } + + // ========== 隐私设置相关对话框 ========== + + private void showBlacklistDialog() { + new AlertDialog.Builder(this) + .setTitle("黑名单") + .setMessage("黑名单功能说明:\n\n" + + "• 将用户加入黑名单后,对方将无法:\n" + + " - 查看你的动态\n" + + " - 给你发送消息\n" + + " - 关注你\n\n" + + "• 你仍然可以查看对方的公开信息\n\n" + + "当前黑名单:0人\n\n" + + "(此功能待接入后端)") + .setPositiveButton("确定", null) + .show(); + } + + private void showPermissionManagementDialog() { + new AlertDialog.Builder(this) + .setTitle("权限管理") + .setMessage("应用权限说明:\n\n" + + "• 相机权限:用于直播和拍照\n" + + "• 麦克风权限:用于语音搜索和直播\n" + + "• 位置权限:用于附近的人功能\n" + + "• 存储权限:用于保存图片和文件\n\n" + + "你可以在系统设置中管理这些权限。") + .setPositiveButton("打开系统设置", (dialog, which) -> { + Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(android.net.Uri.parse("package:" + getPackageName())); + startActivity(intent); + }) + .setNegativeButton("取消", null) + .show(); + } + + private void showPrivacyPolicyDialog() { + new AlertDialog.Builder(this) + .setTitle("隐私政策") + .setMessage("隐私政策\n\n" + + "我们非常重视您的隐私保护。本隐私政策说明了我们如何收集、使用和保护您的个人信息。\n\n" + + "1. 信息收集\n" + + "我们可能收集以下信息:\n" + + "• 账户信息(昵称、头像等)\n" + + "• 设备信息(设备型号、系统版本等)\n" + + "• 使用信息(观看记录、互动记录等)\n\n" + + "2. 信息使用\n" + + "我们使用收集的信息用于:\n" + + "• 提供和改进服务\n" + + "• 个性化推荐\n" + + "• 安全保障\n\n" + + "3. 信息保护\n" + + "我们采用行业标准的安全措施保护您的信息。\n\n" + + "4. 联系我们\n" + + "如有疑问,请联系客服。\n\n" + + "(完整版隐私政策待接入)") + .setPositiveButton("确定", null) + .show(); + } + + // ========== 缓存清理相关对话框 ========== + + private void showClearAllCacheDialog() { + new AlertDialog.Builder(this) + .setTitle("清理缓存") + .setMessage("确定要清理所有缓存吗?\n\n" + + "清理后可能需要重新加载部分内容。") + .setPositiveButton("清理", (dialog, which) -> { + showClearingProgress(); + CacheManager.clearAllCache(this, new CacheManager.OnCacheClearListener() { + @Override + public void onSuccess(long clearedSize) { + hideClearingProgress(); + String sizeText = CacheManager.formatCacheSize(SettingsPageActivity.this, clearedSize); + Toast.makeText(SettingsPageActivity.this, + "清理完成,已释放 " + sizeText, Toast.LENGTH_SHORT).show(); + updateCacheSize(); + } + + @Override + public void onError(Exception e) { + hideClearingProgress(); + Toast.makeText(SettingsPageActivity.this, + "清理失败:" + e.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + }) + .setNegativeButton("取消", null) + .show(); + } + + private void showClearImageCacheDialog() { + new AlertDialog.Builder(this) + .setTitle("清理图片缓存") + .setMessage("确定要清理图片缓存吗?\n\n" + + "清理后图片需要重新加载。") + .setPositiveButton("清理", (dialog, which) -> { + showClearingProgress(); + CacheManager.clearImageCache(this, new CacheManager.OnCacheClearListener() { + @Override + public void onSuccess(long clearedSize) { + hideClearingProgress(); + String sizeText = CacheManager.formatCacheSize(SettingsPageActivity.this, clearedSize); + Toast.makeText(SettingsPageActivity.this, + "清理完成,已释放 " + sizeText, Toast.LENGTH_SHORT).show(); + updateCacheSize(); + } + + @Override + public void onError(Exception e) { + hideClearingProgress(); + Toast.makeText(SettingsPageActivity.this, + "清理失败:" + e.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + }) + .setNegativeButton("取消", null) + .show(); + } + + private AlertDialog clearingDialog; + + private void showClearingProgress() { + ProgressBar progressBar = new ProgressBar(this); + progressBar.setIndeterminate(true); + clearingDialog = new AlertDialog.Builder(this) + .setTitle("正在清理...") + .setView(progressBar) + .setCancelable(false) + .show(); + } + + private void hideClearingProgress() { + if (clearingDialog != null && clearingDialog.isShowing()) { + clearingDialog.dismiss(); + clearingDialog = null; + } + } + + // ========== 帮助与反馈相关对话框 ========== + + private void showFAQDialog() { + new AlertDialog.Builder(this) + .setTitle("常见问题") + .setMessage("常见问题解答\n\n" + + "Q: 如何创建直播间?\n" + + "A: 在首页点击右上角的创建按钮,填写直播间信息即可。\n\n" + + "Q: 如何关注主播?\n" + + "A: 在直播间或主播个人主页点击关注按钮。\n\n" + + "Q: 如何发送消息?\n" + + "A: 在消息页面选择会话,输入内容后发送。\n\n" + + "Q: 如何修改个人资料?\n" + + "A: 在个人中心点击编辑资料按钮。\n\n" + + "Q: 如何清理缓存?\n" + + "A: 在设置页面选择清理缓存。\n\n" + + "更多问题请联系客服。") + .setPositiveButton("确定", null) + .show(); + } + + private void showFeedbackDialog() { + try { + View dialogView = getLayoutInflater().inflate(R.layout.dialog_feedback, null); + new AlertDialog.Builder(this) + .setTitle("意见反馈") + .setView(dialogView) + .setPositiveButton("提交", (dialog, which) -> { + Toast.makeText(this, "感谢您的反馈!我们会认真处理。", Toast.LENGTH_SHORT).show(); + }) + .setNegativeButton("取消", null) + .show(); + } catch (Exception e) { + // 如果布局文件不存在,使用简单对话框 + new AlertDialog.Builder(this) + .setTitle("意见反馈") + .setMessage("意见反馈功能待接入后端\n\n" + + "你可以通过以下方式反馈:\n" + + "• 在对话框中输入反馈内容\n" + + "• 选择反馈类型(问题/建议)\n" + + "• 提交后我们会及时处理") + .setPositiveButton("确定", null) + .show(); + } + } + + private void showCustomerServiceDialog() { + new AlertDialog.Builder(this) + .setTitle("联系客服") + .setMessage("客服联系方式:\n\n" + + "• 在线客服:工作日 9:00-18:00\n" + + "• 客服邮箱:support@livestreaming.com\n" + + "• 客服电话:400-123-4567\n\n" + + "(此功能待接入后端)") + .setPositiveButton("确定", null) + .show(); + } + + // ========== 关于页面相关对话框 ========== + + private void showVersionInfoDialog() { + try { + PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0); + String versionName = packageInfo.versionName; + int versionCode = packageInfo.versionCode; + + new AlertDialog.Builder(this) + .setTitle("版本信息") + .setMessage("应用名称:Live Streaming\n" + + "版本号:" + versionName + "\n" + + "构建号:" + versionCode + "\n" + + "更新时间:2024年\n\n" + + "© 2024 Live Streaming. All rights reserved.") + .setPositiveButton("确定", null) + .show(); + } catch (PackageManager.NameNotFoundException e) { + new AlertDialog.Builder(this) + .setTitle("版本信息") + .setMessage("应用名称:Live Streaming\n" + + "版本号:1.0\n\n" + + "© 2024 Live Streaming. All rights reserved.") + .setPositiveButton("确定", null) + .show(); + } + } + + private void showUserAgreementDialog() { + new AlertDialog.Builder(this) + .setTitle("用户协议") + .setMessage("用户服务协议\n\n" + + "欢迎使用Live Streaming直播应用。在使用本应用前,请仔细阅读以下条款。\n\n" + + "1. 服务条款\n" + + "使用本应用即表示您同意遵守本协议的所有条款。\n\n" + + "2. 用户行为规范\n" + + "• 不得发布违法违规内容\n" + + "• 不得进行欺诈、骚扰等行为\n" + + "• 尊重他人,文明互动\n\n" + + "3. 知识产权\n" + + "本应用的所有内容受知识产权法保护。\n\n" + + "4. 免责声明\n" + + "用户需自行承担使用本应用的风险。\n\n" + + "5. 协议变更\n" + + "我们保留随时修改本协议的权利。\n\n" + + "(完整版用户协议待接入)") + .setPositiveButton("确定", null) + .show(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (clearingDialog != null && clearingDialog.isShowing()) { + clearingDialog.dismiss(); + } + } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/ShareUtils.java b/android-app/app/src/main/java/com/example/livestreaming/ShareUtils.java new file mode 100644 index 00000000..3c49cee8 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/ShareUtils.java @@ -0,0 +1,62 @@ +package com.example.livestreaming; + +import android.content.Context; +import android.content.Intent; + +/** + * 分享工具类 + * 提供系统分享功能 + */ +public class ShareUtils { + + /** + * 生成个人主页分享链接 + */ + public static String generateProfileShareLink(String userId) { + return "https://livestreaming.com/profile/" + userId; + } + + /** + * 生成直播间分享链接 + */ + public static String generateRoomShareLink(String roomId) { + return "https://livestreaming.com/room/" + roomId; + } + + /** + * 分享链接(使用系统分享菜单) + */ + public static void shareLink(Context context, String link, String title, String text) { + if (context == null || link == null) return; + + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, title != null ? title : "分享"); + shareIntent.putExtra(Intent.EXTRA_TEXT, (text != null ? text + "\n" : "") + link); + + try { + context.startActivity(Intent.createChooser(shareIntent, "分享到")); + } catch (Exception e) { + // 如果分享失败,忽略异常 + } + } + + /** + * 分享文本 + */ + public static void shareText(Context context, String text, String title) { + if (context == null || text == null) return; + + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, title != null ? title : "分享"); + shareIntent.putExtra(Intent.EXTRA_TEXT, text); + + try { + context.startActivity(Intent.createChooser(shareIntent, "分享到")); + } catch (Exception e) { + // 如果分享失败,忽略异常 + } + } +} + diff --git a/android-app/app/src/main/java/com/example/livestreaming/TabPlaceholderActivity.java b/android-app/app/src/main/java/com/example/livestreaming/TabPlaceholderActivity.java index 283f548b..84444698 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/TabPlaceholderActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/TabPlaceholderActivity.java @@ -32,6 +32,9 @@ public class TabPlaceholderActivity extends AppCompatActivity { private SearchSuggestionsAdapter suggestionsAdapter; private final List discoverAllRooms = new ArrayList<>(); + + private RoomsAdapter followRoomsAdapter; + private final List followAllRooms = new ArrayList<>(); private BadgesAdapter badgesAdapter; @@ -39,6 +42,18 @@ public class TabPlaceholderActivity extends AppCompatActivity { private NearbyUsersAdapter addFriendAdapter; private final List addFriendAllUsers = new ArrayList<>(); + + /** + * 设置关注容器的可见性 + */ + private void setFollowContainerVisibility(int visibility) { + View followContainer = binding.getRoot().findViewById(R.id.followContainer); + if (followContainer != null) { + followContainer.setVisibility(visibility); + } else { + binding.followRecyclerView.setVisibility(visibility); + } + } public static void start(Context context, String title) { Intent intent = new Intent(context, TabPlaceholderActivity.class); @@ -94,7 +109,7 @@ public class TabPlaceholderActivity extends AppCompatActivity { binding.genericScroll.setVisibility(View.VISIBLE); binding.discoverContainer.setVisibility(View.GONE); binding.genericPlaceholderContainer.setVisibility(View.VISIBLE); - binding.followRecyclerView.setVisibility(View.GONE); + setFollowContainerVisibility(View.GONE); binding.nearbyRecyclerView.setVisibility(View.GONE); } @@ -106,7 +121,7 @@ public class TabPlaceholderActivity extends AppCompatActivity { binding.locationDiscoverContainer.setVisibility(View.GONE); binding.addFriendContainer.setVisibility(View.GONE); binding.genericPlaceholderContainer.setVisibility(View.GONE); - binding.followRecyclerView.setVisibility(View.GONE); + setFollowContainerVisibility(View.GONE); binding.nearbyRecyclerView.setVisibility(View.GONE); ensureDiscoverSuggestions(); @@ -204,10 +219,23 @@ public class TabPlaceholderActivity extends AppCompatActivity { private void showFollowRooms() { binding.genericScroll.setVisibility(View.GONE); - binding.followRecyclerView.setVisibility(View.VISIBLE); binding.nearbyRecyclerView.setVisibility(View.GONE); + + // 显示关注容器(包含搜索框和列表) + setFollowContainerVisibility(View.VISIBLE); - RoomsAdapter adapter = new RoomsAdapter(room -> { + ensureFollowRoomsAdapter(); + + // 初始化数据 + followAllRooms.clear(); + followAllRooms.addAll(buildFollowDemoRooms(16)); + followRoomsAdapter.submitList(new ArrayList<>(followAllRooms)); + } + + private void ensureFollowRoomsAdapter() { + if (followRoomsAdapter != null) return; + + followRoomsAdapter = new RoomsAdapter(room -> { if (room == null) return; Intent intent = new Intent(TabPlaceholderActivity.this, RoomDetailActivity.class); intent.putExtra(RoomDetailActivity.EXTRA_ROOM_ID, room.getId()); @@ -217,14 +245,76 @@ public class TabPlaceholderActivity extends AppCompatActivity { StaggeredGridLayoutManager glm = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL); glm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS); binding.followRecyclerView.setLayoutManager(glm); - binding.followRecyclerView.setAdapter(adapter); + binding.followRecyclerView.setAdapter(followRoomsAdapter); + + // 设置搜索框 + setupFollowSearchBox(); + } + + private void setupFollowSearchBox() { + android.widget.EditText searchInput = binding.getRoot().findViewById(R.id.followSearchInput); + View clearButton = binding.getRoot().findViewById(R.id.followSearchClear); + + if (searchInput == null) return; + + // 添加文本监听 + searchInput.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } - adapter.submitList(buildFollowDemoRooms(16)); + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + String text = s != null ? s.toString() : ""; + // 显示/隐藏清空按钮 + if (clearButton != null) { + clearButton.setVisibility(text.isEmpty() ? View.GONE : View.VISIBLE); + } + // 应用筛选 + applyFollowFilter(text); + } + + @Override + public void afterTextChanged(Editable s) { + } + }); + + // 清空按钮点击事件 + if (clearButton != null) { + clearButton.setOnClickListener(v -> { + if (searchInput != null) { + searchInput.setText(""); + searchInput.requestFocus(); + } + }); + } + } + + private void applyFollowFilter(String query) { + if (followRoomsAdapter == null || followAllRooms == null) return; + + String searchQuery = query != null ? query.trim().toLowerCase() : ""; + if (searchQuery.isEmpty()) { + followRoomsAdapter.submitList(new ArrayList<>(followAllRooms)); + return; + } + + List filtered = new ArrayList<>(); + for (Room r : followAllRooms) { + if (r == null) continue; + String title = r.getTitle() != null ? r.getTitle().toLowerCase() : ""; + String streamer = r.getStreamerName() != null ? r.getStreamerName().toLowerCase() : ""; + if (title.contains(searchQuery) || streamer.contains(searchQuery)) { + filtered.add(r); + } + } + + followRoomsAdapter.submitList(filtered); } private void showNearbyUsers() { binding.genericScroll.setVisibility(View.GONE); - binding.followRecyclerView.setVisibility(View.GONE); + setFollowContainerVisibility(View.GONE); binding.nearbyRecyclerView.setVisibility(View.VISIBLE); NearbyUsersAdapter adapter = new NearbyUsersAdapter(user -> { @@ -291,7 +381,7 @@ public class TabPlaceholderActivity extends AppCompatActivity { binding.locationDiscoverContainer.setVisibility(View.GONE); binding.addFriendContainer.setVisibility(View.GONE); binding.genericPlaceholderContainer.setVisibility(View.GONE); - binding.followRecyclerView.setVisibility(View.GONE); + setFollowContainerVisibility(View.GONE); binding.nearbyRecyclerView.setVisibility(View.GONE); ensureParkBadges(); @@ -341,7 +431,7 @@ public class TabPlaceholderActivity extends AppCompatActivity { binding.locationDiscoverContainer.setVisibility(View.GONE); binding.addFriendContainer.setVisibility(View.GONE); binding.genericPlaceholderContainer.setVisibility(View.GONE); - binding.followRecyclerView.setVisibility(View.GONE); + setFollowContainerVisibility(View.GONE); binding.nearbyRecyclerView.setVisibility(View.GONE); ensureMore(); @@ -461,7 +551,7 @@ public class TabPlaceholderActivity extends AppCompatActivity { binding.locationDiscoverContainer.setVisibility(View.VISIBLE); binding.addFriendContainer.setVisibility(View.GONE); binding.genericPlaceholderContainer.setVisibility(View.GONE); - binding.followRecyclerView.setVisibility(View.GONE); + setFollowContainerVisibility(View.GONE); binding.nearbyRecyclerView.setVisibility(View.GONE); binding.locationRefresh.setOnClickListener(v -> Toast.makeText(this, "刷新定位(待接入)", Toast.LENGTH_SHORT).show()); @@ -478,7 +568,7 @@ public class TabPlaceholderActivity extends AppCompatActivity { binding.locationDiscoverContainer.setVisibility(View.GONE); binding.addFriendContainer.setVisibility(View.VISIBLE); binding.genericPlaceholderContainer.setVisibility(View.GONE); - binding.followRecyclerView.setVisibility(View.GONE); + setFollowContainerVisibility(View.GONE); binding.nearbyRecyclerView.setVisibility(View.GONE); ensureAddFriends(); diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/Room.java.backup b/android-app/app/src/main/java/com/example/livestreaming/net/Room.java.backup new file mode 100644 index 00000000..297607cc --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/net/Room.java.backup @@ -0,0 +1,119 @@ +package com.example.livestreaming.net; + +import com.google.gson.annotations.SerializedName; + +import java.util.Objects; + +public class Room { + + @SerializedName("id") + private String id; + + @SerializedName("title") + private String title; + + @SerializedName("streamerName") + private String streamerName; + + @SerializedName("type") + private String type; + + @SerializedName("streamKey") + private String streamKey; + + @SerializedName("isLive") + private boolean isLive; + + @SerializedName("viewerCount") + private int viewerCount; + + @SerializedName("streamUrls") + private StreamUrls streamUrls; + + public Room() { + } + + public Room(String id, String title, String streamerName, boolean isLive) { + this.id = id; + this.title = title; + this.streamerName = streamerName; + this.isLive = isLive; + } + + public void setId(String id) { + this.id = id; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setStreamerName(String streamerName) { + this.streamerName = streamerName; + }`n`n public void setType(String type) {`n this.type = type;`n } + + public void setLive(boolean live) { + isLive = live; + } + + public void setStreamKey(String streamKey) { + this.streamKey = streamKey; + } + + public void setStreamUrls(StreamUrls streamUrls) { + this.streamUrls = streamUrls; + } + + public void setViewerCount(int viewerCount) { + this.viewerCount = viewerCount; + } + + public String getId() { + return id; + } + + public String getTitle() { + return title; + } + + public String getStreamerName() { + return streamerName; + }`n`n public String getType() {`n return type;`n } + + public String getStreamKey() { + return streamKey; + } + + public boolean isLive() { + return isLive; + } + + public StreamUrls getStreamUrls() { + return streamUrls; + } + + public int getViewerCount() { + return viewerCount; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Room)) return false; + Room room = (Room) o; + return isLive == room.isLive + && viewerCount == room.viewerCount + && Objects.equals(id, room.id) + && Objects.equals(title, room.title) + && Objects.equals(streamerName, room.streamerName) + && Objects.equals(type, room.type) + && Objects.equals(type, room.type) + && Objects.equals(streamKey, room.streamKey) + && Objects.equals(streamUrls, room.streamUrls); + } + + @Override + public int hashCode() { + return Objects.hash(id, title, streamerName, type, type, streamKey, isLive, viewerCount, streamUrls); + } +} diff --git a/android-app/app/src/main/res/layout/activity_notification_settings.xml b/android-app/app/src/main/res/layout/activity_notification_settings.xml new file mode 100644 index 00000000..5e91b15b --- /dev/null +++ b/android-app/app/src/main/res/layout/activity_notification_settings.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/activity_tab_placeholder.xml b/android-app/app/src/main/res/layout/activity_tab_placeholder.xml index 89c07420..103ba8f7 100644 --- a/android-app/app/src/main/res/layout/activity_tab_placeholder.xml +++ b/android-app/app/src/main/res/layout/activity_tab_placeholder.xml @@ -1062,16 +1062,81 @@ - + android:orientation="vertical" + android:visibility="gone"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +