diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml
index 2fd4aef6..c81d5bfe 100644
--- a/android-app/app/src/main/AndroidManifest.xml
+++ b/android-app/app/src/main/AndroidManifest.xml
@@ -27,6 +27,14 @@
android:name="com.example.livestreaming.TabPlaceholderActivity"
android:exported="false" />
+
+
+
+
diff --git a/android-app/app/src/main/java/com/example/livestreaming/BadgeItem.java b/android-app/app/src/main/java/com/example/livestreaming/BadgeItem.java
new file mode 100644
index 00000000..2372c8c1
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/BadgeItem.java
@@ -0,0 +1,44 @@
+package com.example.livestreaming;
+
+public class BadgeItem {
+
+ private final String id;
+ private final String name;
+ private final String desc;
+ private final int iconRes;
+ private final boolean achieved;
+ private final boolean locked;
+
+ public BadgeItem(String id, String name, String desc, int iconRes, boolean achieved, boolean locked) {
+ this.id = id;
+ this.name = name;
+ this.desc = desc;
+ this.iconRes = iconRes;
+ this.achieved = achieved;
+ this.locked = locked;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getDesc() {
+ return desc;
+ }
+
+ public int getIconRes() {
+ return iconRes;
+ }
+
+ public boolean isAchieved() {
+ return achieved;
+ }
+
+ public boolean isLocked() {
+ return locked;
+ }
+}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/BadgesAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/BadgesAdapter.java
new file mode 100644
index 00000000..43c1d3ee
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/BadgesAdapter.java
@@ -0,0 +1,101 @@
+package com.example.livestreaming;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.ListAdapter;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.example.livestreaming.databinding.ItemBadgeBinding;
+
+public class BadgesAdapter extends ListAdapter {
+
+ public interface OnBadgeClickListener {
+ void onClick(BadgeItem item);
+ }
+
+ private final OnBadgeClickListener onClick;
+
+ public BadgesAdapter(OnBadgeClickListener onClick) {
+ super(DIFF);
+ this.onClick = onClick;
+ }
+
+ @NonNull
+ @Override
+ public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ ItemBadgeBinding b = ItemBadgeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
+ return new VH(b, onClick);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull VH holder, int position) {
+ holder.bind(getItem(position));
+ }
+
+ static class VH extends RecyclerView.ViewHolder {
+ private final ItemBadgeBinding binding;
+ private final OnBadgeClickListener onClick;
+
+ VH(ItemBadgeBinding binding, OnBadgeClickListener onClick) {
+ super(binding.getRoot());
+ this.binding = binding;
+ this.onClick = onClick;
+ }
+
+ void bind(BadgeItem item) {
+ if (item == null) return;
+
+ binding.name.setText(item.getName() != null ? item.getName() : "");
+ binding.icon.setImageResource(item.getIconRes());
+
+ if (item.isLocked()) {
+ binding.lockTag.setVisibility(View.VISIBLE);
+ binding.status.setText("未解锁");
+ binding.status.setTextColor(0xFF888888);
+ binding.icon.setAlpha(0.35f);
+ } else if (item.isAchieved()) {
+ binding.lockTag.setVisibility(View.GONE);
+ binding.status.setText("已获得");
+ binding.status.setTextColor(0xFF5B2CFF);
+ binding.icon.setAlpha(1.0f);
+ } else {
+ binding.lockTag.setVisibility(View.GONE);
+ binding.status.setText("可获取");
+ binding.status.setTextColor(0xFF666666);
+ binding.icon.setAlpha(0.7f);
+ }
+
+ binding.getRoot().setOnClickListener(v -> {
+ if (onClick != null) onClick.onClick(item);
+ });
+ }
+ }
+
+ private static final DiffUtil.ItemCallback DIFF = new DiffUtil.ItemCallback() {
+ @Override
+ public boolean areItemsTheSame(@NonNull BadgeItem oldItem, @NonNull BadgeItem newItem) {
+ String o = oldItem.getId();
+ String n = newItem.getId();
+ return o != null && o.equals(n);
+ }
+
+ @Override
+ public boolean areContentsTheSame(@NonNull BadgeItem oldItem, @NonNull BadgeItem newItem) {
+ return oldItem.getId().equals(newItem.getId())
+ && safeEq(oldItem.getName(), newItem.getName())
+ && safeEq(oldItem.getDesc(), newItem.getDesc())
+ && oldItem.getIconRes() == newItem.getIconRes()
+ && oldItem.isAchieved() == newItem.isAchieved()
+ && oldItem.isLocked() == newItem.isLocked();
+ }
+
+ private boolean safeEq(String a, String b) {
+ if (a == null) return b == null;
+ return a.equals(b);
+ }
+ };
+}
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 2695f37d..cbfc00e1 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
@@ -20,6 +20,7 @@ import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+import com.bumptech.glide.Glide;
import com.example.livestreaming.databinding.ActivityMainBinding;
import com.example.livestreaming.databinding.DialogCreateRoomBinding;
import com.google.android.material.bottomnavigation.BottomNavigationView;
@@ -61,6 +62,7 @@ public class MainActivity extends AppCompatActivity {
// 立即显示缓存数据,提升启动速度
setupRecyclerView();
setupUI();
+ loadAvatarFromPrefs();
// 异步加载资源文件,避免阻塞主线程
loadCoverAssetsAsync();
@@ -88,6 +90,11 @@ public class MainActivity extends AppCompatActivity {
private void setupUI() {
binding.swipeRefresh.setOnRefreshListener(this::fetchRooms);
+ binding.avatarButton.setOnClickListener(v -> {
+ ProfileActivity.start(this);
+ finish();
+ });
+
binding.topTabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
@@ -190,12 +197,36 @@ public class MainActivity extends AppCompatActivity {
});
}
+ private void loadAvatarFromPrefs() {
+ try {
+ String avatarUri = getSharedPreferences("profile_prefs", MODE_PRIVATE)
+ .getString("profile_avatar_uri", null);
+ if (!TextUtils.isEmpty(avatarUri)) {
+ Glide.with(this).load(avatarUri).into(binding.avatarButton);
+ return;
+ }
+
+ int avatarRes = getSharedPreferences("profile_prefs", MODE_PRIVATE)
+ .getInt("profile_avatar_res", 0);
+ if (avatarRes != 0) {
+ binding.avatarButton.setImageResource(avatarRes);
+ } else {
+ binding.avatarButton.setImageResource(R.drawable.ic_account_circle_24);
+ }
+ } catch (Exception ignored) {
+ if (binding != null) {
+ binding.avatarButton.setImageResource(R.drawable.ic_account_circle_24);
+ }
+ }
+ }
+
@Override
protected void onResume() {
super.onResume();
if (binding != null) {
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
bottomNavigation.setSelectedItemId(R.id.nav_home);
+ loadAvatarFromPrefs();
}
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/MoreAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/MoreAdapter.java
new file mode 100644
index 00000000..f96ac948
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/MoreAdapter.java
@@ -0,0 +1,104 @@
+package com.example.livestreaming;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.example.livestreaming.databinding.ItemMoreRowBinding;
+import com.example.livestreaming.databinding.ItemMoreSectionBinding;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MoreAdapter extends RecyclerView.Adapter {
+
+ public interface OnRowClickListener {
+ void onClick(MoreItem item);
+ }
+
+ private static final int VT_SECTION = 1;
+ private static final int VT_ROW = 2;
+
+ private final List items = new ArrayList<>();
+ private final OnRowClickListener onRowClick;
+
+ public MoreAdapter(OnRowClickListener onRowClick) {
+ this.onRowClick = onRowClick;
+ }
+
+ public void submitList(List list) {
+ items.clear();
+ if (list != null) items.addAll(list);
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ MoreItem item = items.get(position);
+ if (item == null) return VT_ROW;
+ return item.getType() == MoreItem.Type.SECTION ? VT_SECTION : VT_ROW;
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ if (viewType == VT_SECTION) {
+ ItemMoreSectionBinding b = ItemMoreSectionBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
+ return new SectionVH(b);
+ }
+ ItemMoreRowBinding b = ItemMoreRowBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
+ return new RowVH(b, onRowClick);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ MoreItem item = items.get(position);
+ if (holder instanceof SectionVH) {
+ ((SectionVH) holder).bind(item);
+ } else if (holder instanceof RowVH) {
+ ((RowVH) holder).bind(item);
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return items.size();
+ }
+
+ static class SectionVH extends RecyclerView.ViewHolder {
+ private final ItemMoreSectionBinding binding;
+
+ SectionVH(ItemMoreSectionBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ void bind(MoreItem item) {
+ binding.title.setText(item != null && item.getTitle() != null ? item.getTitle() : "");
+ }
+ }
+
+ static class RowVH extends RecyclerView.ViewHolder {
+ private final ItemMoreRowBinding binding;
+ private final OnRowClickListener onRowClick;
+
+ RowVH(ItemMoreRowBinding binding, OnRowClickListener onRowClick) {
+ super(binding.getRoot());
+ this.binding = binding;
+ this.onRowClick = onRowClick;
+ }
+
+ void bind(MoreItem item) {
+ if (item == null) return;
+ binding.title.setText(item.getTitle() != null ? item.getTitle() : "");
+ binding.subtitle.setText(item.getSubtitle() != null ? item.getSubtitle() : "");
+ if (item.getIconRes() != 0) binding.icon.setImageResource(item.getIconRes());
+
+ binding.getRoot().setOnClickListener(v -> {
+ if (onRowClick != null) onRowClick.onClick(item);
+ });
+ }
+ }
+}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/MoreItem.java b/android-app/app/src/main/java/com/example/livestreaming/MoreItem.java
new file mode 100644
index 00000000..d61e16aa
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/MoreItem.java
@@ -0,0 +1,46 @@
+package com.example.livestreaming;
+
+public class MoreItem {
+
+ public enum Type {
+ SECTION,
+ ROW
+ }
+
+ private final Type type;
+
+ private final String title;
+ private final String subtitle;
+ private final int iconRes;
+
+ public static MoreItem section(String title) {
+ return new MoreItem(Type.SECTION, title, null, 0);
+ }
+
+ public static MoreItem row(String title, String subtitle, int iconRes) {
+ return new MoreItem(Type.ROW, title, subtitle, iconRes);
+ }
+
+ private MoreItem(Type type, String title, String subtitle, int iconRes) {
+ this.type = type;
+ this.title = title;
+ this.subtitle = subtitle;
+ this.iconRes = iconRes;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getSubtitle() {
+ return subtitle;
+ }
+
+ public int getIconRes() {
+ return iconRes;
+ }
+}
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 dbabf52b..5f5549af 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
@@ -2,8 +2,11 @@ package com.example.livestreaming;
import android.content.Context;
import android.content.Intent;
+import android.content.ClipData;
+import android.content.ClipboardManager;
import android.os.Bundle;
import android.text.TextUtils;
+import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
@@ -44,6 +47,7 @@ public class ProfileActivity extends AppCompatActivity {
loadProfileFromPrefs();
setupEditableAreas();
setupNavigationClicks();
+ setupProfileTabs();
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
bottomNavigation.setSelectedItemId(R.id.nav_profile);
@@ -117,10 +121,22 @@ public class ProfileActivity extends AppCompatActivity {
}
private void setupNavigationClicks() {
- binding.topActionSearch.setOnClickListener(v -> TabPlaceholderActivity.start(this, "搜索"));
- binding.topActionClock.setOnClickListener(v -> TabPlaceholderActivity.start(this, "定位/发现"));
+ binding.topActionSearch.setOnClickListener(v -> TabPlaceholderActivity.start(this, "定位/发现"));
+ binding.topActionClock.setOnClickListener(v -> WatchHistoryActivity.start(this));
binding.topActionMore.setOnClickListener(v -> TabPlaceholderActivity.start(this, "更多"));
+ binding.copyIdBtn.setOnClickListener(v -> {
+ String idText = binding.idLine.getText() != null ? binding.idLine.getText().toString() : "";
+ if (TextUtils.isEmpty(idText)) return;
+ String digits = idText.replaceAll("\\D+", "");
+ if (TextUtils.isEmpty(digits)) return;
+ ClipboardManager cm = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+ if (cm != null) {
+ cm.setPrimaryClip(ClipData.newPlainText("id", digits));
+ Toast.makeText(this, "已复制:" + digits, Toast.LENGTH_SHORT).show();
+ }
+ });
+
binding.following.setOnClickListener(v -> FollowingListActivity.start(this));
binding.followers.setOnClickListener(v -> FansListActivity.start(this));
binding.likes.setOnClickListener(v -> LikesListActivity.start(this));
@@ -136,6 +152,40 @@ public class ProfileActivity extends AppCompatActivity {
binding.addFriendBtn.setOnClickListener(v -> TabPlaceholderActivity.start(this, "加好友"));
}
+ private void setupProfileTabs() {
+ showTab(0);
+
+ binding.profileTabs.addOnTabSelectedListener(new com.google.android.material.tabs.TabLayout.OnTabSelectedListener() {
+ @Override
+ public void onTabSelected(com.google.android.material.tabs.TabLayout.Tab tab) {
+ if (tab == null) return;
+ showTab(tab.getPosition());
+ }
+
+ @Override
+ public void onTabUnselected(com.google.android.material.tabs.TabLayout.Tab tab) {
+ }
+
+ @Override
+ public void onTabReselected(com.google.android.material.tabs.TabLayout.Tab tab) {
+ if (tab == null) return;
+ showTab(tab.getPosition());
+ }
+ });
+
+ binding.worksPublishBtn.setOnClickListener(v -> Toast.makeText(this, "发布功能待接入", Toast.LENGTH_SHORT).show());
+ binding.likedGoBrowseBtn.setOnClickListener(v -> startActivity(new Intent(this, MainActivity.class)));
+ binding.favGoBrowseBtn.setOnClickListener(v -> startActivity(new Intent(this, MainActivity.class)));
+ binding.profileEditFromTab.setOnClickListener(v -> EditProfileActivity.start(this));
+ }
+
+ private void showTab(int index) {
+ binding.tabWorks.setVisibility(index == 0 ? View.VISIBLE : View.GONE);
+ binding.tabLiked.setVisibility(index == 1 ? View.VISIBLE : View.GONE);
+ binding.tabFavorites.setVisibility(index == 2 ? View.VISIBLE : View.GONE);
+ binding.tabProfile.setVisibility(index == 3 ? View.VISIBLE : View.GONE);
+ }
+
private interface OnTextSaved {
void onSaved(String text);
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java b/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java
new file mode 100644
index 00000000..68e13fcc
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java
@@ -0,0 +1,112 @@
+package com.example.livestreaming;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.inputmethod.EditorInfo;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.recyclerview.widget.StaggeredGridLayoutManager;
+
+import com.example.livestreaming.databinding.ActivitySearchBinding;
+import com.example.livestreaming.net.Room;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SearchActivity extends AppCompatActivity {
+
+ private ActivitySearchBinding binding;
+ private RoomsAdapter adapter;
+
+ private final List all = new ArrayList<>();
+
+ public static void start(Context context) {
+ Intent intent = new Intent(context, SearchActivity.class);
+ context.startActivity(intent);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ binding = ActivitySearchBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ setupList();
+ setupInput();
+
+ binding.backButton.setOnClickListener(v -> finish());
+ binding.cancelBtn.setOnClickListener(v -> finish());
+
+ binding.searchInput.requestFocus();
+ }
+
+ private void setupList() {
+ adapter = new RoomsAdapter(room -> {
+ if (room == null) return;
+ Intent intent = new Intent(SearchActivity.this, RoomDetailActivity.class);
+ intent.putExtra(RoomDetailActivity.EXTRA_ROOM_ID, room.getId());
+ startActivity(intent);
+ });
+
+ StaggeredGridLayoutManager glm = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
+ glm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
+ binding.resultsRecyclerView.setLayoutManager(glm);
+ binding.resultsRecyclerView.setAdapter(adapter);
+
+ all.clear();
+ all.addAll(buildDemoRooms(24));
+ adapter.submitList(new ArrayList<>(all));
+ }
+
+ private void setupInput() {
+ binding.searchInput.setImeOptions(EditorInfo.IME_ACTION_SEARCH);
+ binding.searchInput.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ applyFilter(s != null ? s.toString() : "");
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+ }
+
+ private void applyFilter(String q) {
+ String query = q != null ? q.trim() : "";
+ if (query.isEmpty()) {
+ adapter.submitList(new ArrayList<>(all));
+ return;
+ }
+
+ List filtered = new ArrayList<>();
+ for (Room r : all) {
+ if (r == null) continue;
+ String title = r.getTitle() != null ? r.getTitle() : "";
+ String streamer = r.getStreamerName() != null ? r.getStreamerName() : "";
+ if (title.contains(query) || streamer.contains(query)) {
+ filtered.add(r);
+ }
+ }
+ adapter.submitList(filtered);
+ }
+
+ private List buildDemoRooms(int count) {
+ List list = new ArrayList<>();
+ for (int i = 0; i < count; i++) {
+ String id = "search-" + i;
+ String title = "热门直播间 " + (i + 1);
+ String streamer = "主播" + (i + 1);
+ boolean live = i % 5 != 0;
+ list.add(new Room(id, title, streamer, live));
+ }
+ return list;
+ }
+}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/SearchSuggestionsAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/SearchSuggestionsAdapter.java
new file mode 100644
index 00000000..911068b1
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/SearchSuggestionsAdapter.java
@@ -0,0 +1,79 @@
+package com.example.livestreaming;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.ListAdapter;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.example.livestreaming.databinding.ItemSearchSuggestionBinding;
+import com.example.livestreaming.net.Room;
+
+public class SearchSuggestionsAdapter extends ListAdapter {
+
+ public interface OnSuggestionClickListener {
+ void onClick(Room room);
+ }
+
+ private final OnSuggestionClickListener onClick;
+
+ public SearchSuggestionsAdapter(OnSuggestionClickListener onClick) {
+ super(DIFF);
+ this.onClick = onClick;
+ }
+
+ @NonNull
+ @Override
+ public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ ItemSearchSuggestionBinding b = ItemSearchSuggestionBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
+ return new VH(b, onClick);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull VH holder, int position) {
+ holder.bind(getItem(position));
+ }
+
+ static class VH extends RecyclerView.ViewHolder {
+ private final ItemSearchSuggestionBinding binding;
+ private final OnSuggestionClickListener onClick;
+
+ VH(ItemSearchSuggestionBinding binding, OnSuggestionClickListener onClick) {
+ super(binding.getRoot());
+ this.binding = binding;
+ this.onClick = onClick;
+ }
+
+ void bind(Room room) {
+ String title = room != null && room.getTitle() != null ? room.getTitle() : "";
+ String sub = room != null && room.getStreamerName() != null ? room.getStreamerName() : "";
+ binding.title.setText(title);
+ binding.subtitle.setText(sub);
+
+ boolean live = room != null && room.isLive();
+ binding.badge.setVisibility(live ? View.VISIBLE : View.GONE);
+
+ binding.getRoot().setOnClickListener(v -> {
+ if (room == null) return;
+ if (onClick != null) onClick.onClick(room);
+ });
+ }
+ }
+
+ private static final DiffUtil.ItemCallback DIFF = new DiffUtil.ItemCallback() {
+ @Override
+ public boolean areItemsTheSame(@NonNull Room oldItem, @NonNull Room newItem) {
+ String o = oldItem.getId();
+ String n = newItem.getId();
+ return o != null && o.equals(n);
+ }
+
+ @Override
+ public boolean areContentsTheSame(@NonNull Room oldItem, @NonNull Room newItem) {
+ return oldItem.equals(newItem);
+ }
+ };
+}
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
new file mode 100644
index 00000000..5addcd72
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/SettingsPageActivity.java
@@ -0,0 +1,133 @@
+package com.example.livestreaming;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.recyclerview.widget.LinearLayoutManager;
+
+import com.example.livestreaming.databinding.ActivitySettingsPageBinding;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SettingsPageActivity extends AppCompatActivity {
+
+ public static final String EXTRA_PAGE = "extra_page";
+
+ public static final String PAGE_ACCOUNT_SECURITY = "account_security";
+ public static final String PAGE_PRIVACY = "privacy";
+ public static final String PAGE_NOTIFICATIONS = "notifications";
+ public static final String PAGE_CLEAR_CACHE = "clear_cache";
+ public static final String PAGE_HELP = "help";
+ public static final String PAGE_ABOUT = "about";
+
+ private ActivitySettingsPageBinding binding;
+
+ public static void start(Context context, String page) {
+ Intent intent = new Intent(context, SettingsPageActivity.class);
+ intent.putExtra(EXTRA_PAGE, page);
+ context.startActivity(intent);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ binding = ActivitySettingsPageBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ binding.backButton.setOnClickListener(v -> finish());
+
+ String page = getIntent() != null ? getIntent().getStringExtra(EXTRA_PAGE) : null;
+ if (page == null) page = "";
+
+ String title = resolveTitle(page);
+ binding.titleText.setText(title);
+
+ MoreAdapter 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();
+ });
+
+ binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
+ binding.recyclerView.setAdapter(adapter);
+
+ adapter.submitList(buildItems(page));
+ }
+
+ private String resolveTitle(String page) {
+ switch (page) {
+ case PAGE_ACCOUNT_SECURITY:
+ return "账号与安全";
+ case PAGE_PRIVACY:
+ return "隐私";
+ case PAGE_NOTIFICATIONS:
+ return "通知";
+ case PAGE_CLEAR_CACHE:
+ return "清理缓存";
+ case PAGE_HELP:
+ return "帮助与反馈";
+ case PAGE_ABOUT:
+ return "关于";
+ default:
+ return "设置";
+ }
+ }
+
+ private List buildItems(String page) {
+ List list = new ArrayList<>();
+
+ if (PAGE_ACCOUNT_SECURITY.equals(page)) {
+ list.add(MoreItem.section("登录与账号"));
+ list.add(MoreItem.row("修改密码", "设置登录密码", R.drawable.ic_person_24));
+ list.add(MoreItem.row("绑定手机号", "用于登录与找回", R.drawable.ic_people_24));
+ list.add(MoreItem.row("登录设备管理", "查看并管理已登录设备", R.drawable.ic_grid_24));
+ return list;
+ }
+
+ if (PAGE_PRIVACY.equals(page)) {
+ list.add(MoreItem.section("权限与安全"));
+ list.add(MoreItem.row("黑名单", "管理你屏蔽的用户", R.drawable.ic_people_24));
+ list.add(MoreItem.row("权限管理", "相机、麦克风、定位等", R.drawable.ic_mic_24));
+ list.add(MoreItem.row("隐私政策", "了解我们如何保护你的数据", R.drawable.ic_globe_24));
+ return list;
+ }
+
+ if (PAGE_NOTIFICATIONS.equals(page)) {
+ list.add(MoreItem.section("消息提醒"));
+ list.add(MoreItem.row("系统通知", "关注、评论、私信提醒", R.drawable.ic_notifications_24));
+ list.add(MoreItem.row("免打扰", "设置勿扰时段", R.drawable.ic_notifications_24));
+ return list;
+ }
+
+ 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;
+ }
+
+ 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));
+ 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;
+ }
+
+ list.add(MoreItem.row("返回", "", R.drawable.ic_arrow_back_24));
+ return list;
+ }
+}
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 69d971c9..4f0d4754 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
@@ -3,10 +3,17 @@ package com.example.livestreaming;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
import android.view.View;
+import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.app.AlertDialog;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
import com.example.livestreaming.databinding.ActivityTabPlaceholderBinding;
@@ -21,6 +28,16 @@ public class TabPlaceholderActivity extends AppCompatActivity {
private ActivityTabPlaceholderBinding binding;
+ private SearchSuggestionsAdapter suggestionsAdapter;
+ private final List discoverAllRooms = new ArrayList<>();
+
+ private BadgesAdapter badgesAdapter;
+
+ private MoreAdapter moreAdapter;
+
+ private NearbyUsersAdapter addFriendAdapter;
+ private final List addFriendAllUsers = new ArrayList<>();
+
public static void start(Context context, String title) {
Intent intent = new Intent(context, TabPlaceholderActivity.class);
intent.putExtra(EXTRA_TITLE, title);
@@ -51,12 +68,138 @@ public class TabPlaceholderActivity extends AppCompatActivity {
showNearbyUsers();
return;
}
+ if ("发现".equals(title)) {
+ showDiscover();
+ return;
+ }
+ if ("定位/发现".equals(title)) {
+ showLocationDiscover();
+ return;
+ }
+ if ("加好友".equals(title)) {
+ showAddFriends();
+ return;
+ }
+ if ("公园勋章".equals(title)) {
+ showParkBadges();
+ return;
+ }
+ if ("更多".equals(title)) {
+ showMore();
+ return;
+ }
binding.genericScroll.setVisibility(View.VISIBLE);
+ binding.discoverContainer.setVisibility(View.GONE);
+ binding.genericPlaceholderContainer.setVisibility(View.VISIBLE);
binding.followRecyclerView.setVisibility(View.GONE);
binding.nearbyRecyclerView.setVisibility(View.GONE);
}
+ private void showDiscover() {
+ binding.genericScroll.setVisibility(View.VISIBLE);
+ binding.discoverContainer.setVisibility(View.VISIBLE);
+ binding.parkBadgeContainer.setVisibility(View.GONE);
+ binding.moreContainer.setVisibility(View.GONE);
+ binding.locationDiscoverContainer.setVisibility(View.GONE);
+ binding.addFriendContainer.setVisibility(View.GONE);
+ binding.genericPlaceholderContainer.setVisibility(View.GONE);
+ binding.followRecyclerView.setVisibility(View.GONE);
+ binding.nearbyRecyclerView.setVisibility(View.GONE);
+
+ ensureDiscoverSuggestions();
+ binding.discoverSearchInput.requestFocus();
+ showKeyboard(binding.discoverSearchInput);
+
+ binding.discoverSearch.setOnClickListener(v -> {
+ binding.discoverSearchInput.requestFocus();
+ showKeyboard(binding.discoverSearchInput);
+ });
+
+ binding.discoverSearchCancel.setOnClickListener(v -> {
+ binding.discoverSearchInput.setText("");
+ binding.discoverSuggestionsRecyclerView.setVisibility(View.GONE);
+ hideKeyboard(binding.discoverSearchInput);
+ });
+
+ binding.discoverShortcutLive.setOnClickListener(v -> Toast.makeText(this, "进入直播模块", Toast.LENGTH_SHORT).show());
+ binding.discoverShortcutNearby.setOnClickListener(v -> TabPlaceholderActivity.start(this, "附近"));
+ binding.discoverShortcutRank.setOnClickListener(v -> Toast.makeText(this, "榜单功能待接入", Toast.LENGTH_SHORT).show());
+ binding.discoverShortcutTopic.setOnClickListener(v -> Toast.makeText(this, "话题功能待接入", Toast.LENGTH_SHORT).show());
+ }
+
+ private void ensureDiscoverSuggestions() {
+ if (suggestionsAdapter != null) return;
+
+ suggestionsAdapter = new SearchSuggestionsAdapter(room -> {
+ if (room == null) return;
+ Intent intent = new Intent(TabPlaceholderActivity.this, RoomDetailActivity.class);
+ intent.putExtra(RoomDetailActivity.EXTRA_ROOM_ID, room.getId());
+ startActivity(intent);
+ });
+
+ binding.discoverSuggestionsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
+ binding.discoverSuggestionsRecyclerView.setAdapter(suggestionsAdapter);
+
+ discoverAllRooms.clear();
+ discoverAllRooms.addAll(buildDiscoverDemoRooms(30));
+ suggestionsAdapter.submitList(new ArrayList<>());
+
+ binding.discoverSearchInput.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ applyDiscoverFilter(s != null ? s.toString() : "");
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+ }
+
+ private void applyDiscoverFilter(String q) {
+ String query = q != null ? q.trim() : "";
+ if (TextUtils.isEmpty(query)) {
+ binding.discoverSuggestionsRecyclerView.setVisibility(View.GONE);
+ suggestionsAdapter.submitList(new ArrayList<>());
+ return;
+ }
+
+ List filtered = new ArrayList<>();
+ for (Room r : discoverAllRooms) {
+ if (r == null) continue;
+ String title = r.getTitle() != null ? r.getTitle() : "";
+ String streamer = r.getStreamerName() != null ? r.getStreamerName() : "";
+ if (title.contains(query) || streamer.contains(query)) {
+ filtered.add(r);
+ if (filtered.size() >= 8) break;
+ }
+ }
+
+ binding.discoverSuggestionsRecyclerView.setVisibility(filtered.isEmpty() ? View.GONE : View.VISIBLE);
+ suggestionsAdapter.submitList(filtered);
+ }
+
+ private void showKeyboard(View target) {
+ try {
+ InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
+ if (imm != null) imm.showSoftInput(target, InputMethodManager.SHOW_IMPLICIT);
+ } catch (Exception ignored) {
+ }
+ }
+
+ private void hideKeyboard(View target) {
+ try {
+ InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
+ if (imm != null) imm.hideSoftInputFromWindow(target.getWindowToken(), 0);
+ } catch (Exception ignored) {
+ }
+ }
+
private void showFollowRooms() {
binding.genericScroll.setVisibility(View.GONE);
binding.followRecyclerView.setVisibility(View.VISIBLE);
@@ -124,4 +267,249 @@ public class TabPlaceholderActivity extends AppCompatActivity {
}
return list;
}
+
+ private List buildDiscoverDemoRooms(int count) {
+ List list = new ArrayList<>();
+ for (int i = 0; i < count; i++) {
+ String id = "discover-" + i;
+ String title = "推荐直播间 " + (i + 1);
+ String streamer = "推荐主播" + (i + 1);
+ boolean live = i % 4 != 0;
+ list.add(new Room(id, title, streamer, live));
+ }
+ return list;
+ }
+
+ private void showParkBadges() {
+ binding.genericScroll.setVisibility(View.VISIBLE);
+ binding.discoverContainer.setVisibility(View.GONE);
+ binding.parkBadgeContainer.setVisibility(View.VISIBLE);
+ binding.moreContainer.setVisibility(View.GONE);
+ binding.locationDiscoverContainer.setVisibility(View.GONE);
+ binding.addFriendContainer.setVisibility(View.GONE);
+ binding.genericPlaceholderContainer.setVisibility(View.GONE);
+ binding.followRecyclerView.setVisibility(View.GONE);
+ binding.nearbyRecyclerView.setVisibility(View.GONE);
+
+ ensureParkBadges();
+ }
+
+ private void ensureParkBadges() {
+ if (badgesAdapter != null) return;
+
+ badgesAdapter = new BadgesAdapter(item -> {
+ if (item == null) return;
+ String name = item.getName() != null ? item.getName() : "";
+ String desc = item.getDesc() != null ? item.getDesc() : "";
+ if (item.isLocked()) {
+ Toast.makeText(this, "未解锁:" + name, Toast.LENGTH_SHORT).show();
+ } else if (item.isAchieved()) {
+ Toast.makeText(this, "已获得:" + name + "\n" + desc, Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(this, "可获取:" + name + "\n" + desc, Toast.LENGTH_SHORT).show();
+ }
+ });
+
+ binding.parkBadgeRecyclerView.setLayoutManager(new GridLayoutManager(this, 3));
+ binding.parkBadgeRecyclerView.setAdapter(badgesAdapter);
+
+ badgesAdapter.submitList(buildDemoBadges());
+ }
+
+ private List buildDemoBadges() {
+ List list = new ArrayList<>();
+ list.add(new BadgeItem("b-1", "新人报道", "首次完善个人资料", R.drawable.ic_person_24, true, false));
+ list.add(new BadgeItem("b-2", "热度新星", "累计获得100次点赞", R.drawable.ic_heart_24, false, false));
+ list.add(new BadgeItem("b-3", "连续签到", "连续签到7天", R.drawable.ic_grid_24, false, true));
+ list.add(new BadgeItem("b-4", "分享达人", "分享主页3次", R.drawable.ic_copy_24, false, false));
+ list.add(new BadgeItem("b-5", "探索者", "进入发现页10次", R.drawable.ic_globe_24, true, false));
+ list.add(new BadgeItem("b-6", "公园守护", "完成公园任务5次", R.drawable.ic_tree_24, false, true));
+ list.add(new BadgeItem("b-7", "话题参与", "发布话题内容1次", R.drawable.ic_palette_24, false, false));
+ list.add(new BadgeItem("b-8", "社交达人", "添加好友5人", R.drawable.ic_people_24, false, true));
+ list.add(new BadgeItem("b-9", "开播尝鲜", "创建直播间1次", R.drawable.ic_mic_24, false, false));
+ return list;
+ }
+
+ private void showMore() {
+ binding.genericScroll.setVisibility(View.VISIBLE);
+ binding.discoverContainer.setVisibility(View.GONE);
+ binding.parkBadgeContainer.setVisibility(View.GONE);
+ binding.moreContainer.setVisibility(View.VISIBLE);
+ binding.locationDiscoverContainer.setVisibility(View.GONE);
+ binding.addFriendContainer.setVisibility(View.GONE);
+ binding.genericPlaceholderContainer.setVisibility(View.GONE);
+ binding.followRecyclerView.setVisibility(View.GONE);
+ binding.nearbyRecyclerView.setVisibility(View.GONE);
+
+ ensureMore();
+ }
+
+ private void ensureMore() {
+ if (moreAdapter != null) return;
+
+ moreAdapter = new MoreAdapter(item -> {
+ if (item == null) return;
+ if (item.getType() != MoreItem.Type.ROW) return;
+ String t = item.getTitle() != null ? item.getTitle() : "";
+
+ if ("账号与安全".equals(t)) {
+ SettingsPageActivity.start(this, SettingsPageActivity.PAGE_ACCOUNT_SECURITY);
+ return;
+ }
+ if ("隐私".equals(t)) {
+ SettingsPageActivity.start(this, SettingsPageActivity.PAGE_PRIVACY);
+ return;
+ }
+ if ("通知".equals(t)) {
+ SettingsPageActivity.start(this, SettingsPageActivity.PAGE_NOTIFICATIONS);
+ return;
+ }
+ if ("清理缓存".equals(t)) {
+ SettingsPageActivity.start(this, SettingsPageActivity.PAGE_CLEAR_CACHE);
+ return;
+ }
+ if ("帮助与反馈".equals(t)) {
+ SettingsPageActivity.start(this, SettingsPageActivity.PAGE_HELP);
+ return;
+ }
+ if ("关于".equals(t)) {
+ SettingsPageActivity.start(this, SettingsPageActivity.PAGE_ABOUT);
+ return;
+ }
+ if ("退出登录".equals(t)) {
+ showLogoutConfirm();
+ return;
+ }
+
+ Toast.makeText(this, "暂未接入:" + t, Toast.LENGTH_SHORT).show();
+ });
+
+ binding.moreRecyclerView.setLayoutManager(new LinearLayoutManager(this));
+ binding.moreRecyclerView.setAdapter(moreAdapter);
+
+ binding.moreEdit.setOnClickListener(v -> EditProfileActivity.start(this));
+
+ List items = new ArrayList<>();
+ items.add(MoreItem.section("账号"));
+ items.add(MoreItem.row("账号与安全", "密码、手机号、登录设备", R.drawable.ic_person_24));
+ items.add(MoreItem.row("隐私", "黑名单、权限管理", R.drawable.ic_globe_24));
+ items.add(MoreItem.row("通知", "消息提醒、勿扰", R.drawable.ic_notifications_24));
+
+ items.add(MoreItem.section("通用"));
+ items.add(MoreItem.row("清理缓存", "释放存储空间", R.drawable.ic_grid_24));
+ items.add(MoreItem.row("帮助与反馈", "常见问题、意见反馈", R.drawable.ic_chat_24));
+ items.add(MoreItem.row("关于", "版本信息、协议", R.drawable.ic_menu_24));
+
+ items.add(MoreItem.section("其他"));
+ items.add(MoreItem.row("退出登录", "切换账号", R.drawable.ic_arrow_back_24));
+
+ moreAdapter.submitList(items);
+ }
+
+ private void showLogoutConfirm() {
+ new AlertDialog.Builder(this)
+ .setTitle("退出登录")
+ .setMessage("确定要退出登录吗?")
+ .setNegativeButton("取消", null)
+ .setPositiveButton("退出", (d, w) -> {
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ finish();
+ })
+ .show();
+ }
+
+ private void showLocationDiscover() {
+ binding.genericScroll.setVisibility(View.VISIBLE);
+ binding.discoverContainer.setVisibility(View.GONE);
+ binding.parkBadgeContainer.setVisibility(View.GONE);
+ binding.moreContainer.setVisibility(View.GONE);
+ binding.locationDiscoverContainer.setVisibility(View.VISIBLE);
+ binding.addFriendContainer.setVisibility(View.GONE);
+ binding.genericPlaceholderContainer.setVisibility(View.GONE);
+ binding.followRecyclerView.setVisibility(View.GONE);
+ binding.nearbyRecyclerView.setVisibility(View.GONE);
+
+ binding.locationRefresh.setOnClickListener(v -> Toast.makeText(this, "刷新定位(待接入)", Toast.LENGTH_SHORT).show());
+ binding.nearbyEntryUsers.setOnClickListener(v -> TabPlaceholderActivity.start(this, "附近"));
+ binding.nearbyEntryLive.setOnClickListener(v -> Toast.makeText(this, "附近直播(待接入)", Toast.LENGTH_SHORT).show());
+ binding.nearbyEntryPlaces.setOnClickListener(v -> Toast.makeText(this, "热门地点(待接入)", Toast.LENGTH_SHORT).show());
+ }
+
+ private void showAddFriends() {
+ binding.genericScroll.setVisibility(View.VISIBLE);
+ binding.discoverContainer.setVisibility(View.GONE);
+ binding.parkBadgeContainer.setVisibility(View.GONE);
+ binding.moreContainer.setVisibility(View.GONE);
+ binding.locationDiscoverContainer.setVisibility(View.GONE);
+ binding.addFriendContainer.setVisibility(View.VISIBLE);
+ binding.genericPlaceholderContainer.setVisibility(View.GONE);
+ binding.followRecyclerView.setVisibility(View.GONE);
+ binding.nearbyRecyclerView.setVisibility(View.GONE);
+
+ ensureAddFriends();
+ }
+
+ private void ensureAddFriends() {
+ if (addFriendAdapter != null) return;
+
+ addFriendAdapter = new NearbyUsersAdapter(user -> {
+ if (user == null) return;
+ Toast.makeText(this, "已发送好友请求:" + user.getName(), Toast.LENGTH_SHORT).show();
+ });
+
+ StaggeredGridLayoutManager glm = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
+ glm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
+ binding.addFriendRecyclerView.setLayoutManager(glm);
+ binding.addFriendRecyclerView.setAdapter(addFriendAdapter);
+
+ addFriendAllUsers.clear();
+ addFriendAllUsers.addAll(buildNearbyDemoUsers(18));
+ addFriendAdapter.submitList(new ArrayList<>(addFriendAllUsers));
+ binding.addFriendEmpty.setVisibility(View.GONE);
+
+ binding.addFriendSearchClear.setOnClickListener(v -> {
+ binding.addFriendSearchInput.setText("");
+ addFriendAdapter.submitList(new ArrayList<>(addFriendAllUsers));
+ binding.addFriendEmpty.setVisibility(View.GONE);
+ });
+
+ binding.addFriendSearchInput.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ applyAddFriendFilter(s != null ? s.toString() : "");
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+ }
+
+ private void applyAddFriendFilter(String q) {
+ String query = q != null ? q.trim() : "";
+ if (TextUtils.isEmpty(query)) {
+ addFriendAdapter.submitList(new ArrayList<>(addFriendAllUsers));
+ binding.addFriendEmpty.setVisibility(View.GONE);
+ return;
+ }
+
+ List filtered = new ArrayList<>();
+ for (NearbyUser u : addFriendAllUsers) {
+ if (u == null) continue;
+ String name = u.getName() != null ? u.getName() : "";
+ String id = u.getId() != null ? u.getId() : "";
+ if (name.contains(query) || id.contains(query)) {
+ filtered.add(u);
+ }
+ }
+
+ addFriendAdapter.submitList(filtered);
+ binding.addFriendEmpty.setVisibility(filtered.isEmpty() ? View.VISIBLE : View.GONE);
+ }
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/WishTreeActivity.java b/android-app/app/src/main/java/com/example/livestreaming/WishTreeActivity.java
index 00f4c47c..c728f773 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/WishTreeActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/WishTreeActivity.java
@@ -3,27 +3,100 @@ package com.example.livestreaming;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
import androidx.appcompat.app.AppCompatActivity;
import com.example.livestreaming.databinding.ActivityWishTreeBinding;
import com.google.android.material.bottomnavigation.BottomNavigationView;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
public class WishTreeActivity extends AppCompatActivity {
private ActivityWishTreeBinding binding;
+ private final Handler handler = new Handler(Looper.getMainLooper());
+ private Runnable timerRunnable;
+
public static void start(Context context) {
Intent intent = new Intent(context, WishTreeActivity.class);
context.startActivity(intent);
}
+ @Override
+ protected void onStart() {
+ super.onStart();
+ startBannerCountdown();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ stopBannerCountdown();
+ }
+
+ private void startBannerCountdown() {
+ stopBannerCountdown();
+ timerRunnable = new Runnable() {
+ @Override
+ public void run() {
+ updateBannerTimer();
+ handler.postDelayed(this, 1000);
+ }
+ };
+ handler.post(timerRunnable);
+ }
+
+ private void stopBannerCountdown() {
+ if (timerRunnable != null) {
+ handler.removeCallbacks(timerRunnable);
+ timerRunnable = null;
+ }
+ }
+
+ private void updateBannerTimer() {
+ if (binding == null) return;
+ try {
+ long now = System.currentTimeMillis();
+ TimeZone tz = TimeZone.getDefault();
+ Calendar c = Calendar.getInstance(tz);
+ c.setTimeInMillis(now);
+ c.set(Calendar.HOUR_OF_DAY, 0);
+ c.set(Calendar.MINUTE, 0);
+ c.set(Calendar.SECOND, 0);
+ c.set(Calendar.MILLISECOND, 0);
+ c.add(Calendar.DAY_OF_MONTH, 1);
+ long nextMidnight = c.getTimeInMillis();
+ long diff = Math.max(0, nextMidnight - now);
+
+ long totalSeconds = diff / 1000;
+ long hours = totalSeconds / 3600;
+ long minutes = (totalSeconds % 3600) / 60;
+ long seconds = totalSeconds % 60;
+
+ SimpleDateFormat fmt = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
+ fmt.setTimeZone(tz);
+ String current = fmt.format(new Date(now));
+ String remain = String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds);
+ binding.bannerTimer.setText(current + " " + remain);
+ } catch (Exception ignored) {
+ }
+ }
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityWishTreeBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
+ startBannerCountdown();
+
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
bottomNavigation.setSelectedItemId(R.id.nav_wish_tree);
bottomNavigation.setOnItemSelectedListener(item -> {
diff --git a/android-app/app/src/main/res/drawable/bg_icon_button_12.xml b/android-app/app/src/main/res/drawable/bg_icon_button_12.xml
new file mode 100644
index 00000000..3b24737a
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_icon_button_12.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/ic_clock_24.xml b/android-app/app/src/main/res/drawable/ic_clock_24.xml
new file mode 100644
index 00000000..e57000bf
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/ic_clock_24.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/wish_tree_title.png b/android-app/app/src/main/res/drawable/wish_tree_title.png
new file mode 100644
index 00000000..5fdcf56c
Binary files /dev/null and b/android-app/app/src/main/res/drawable/wish_tree_title.png differ
diff --git a/android-app/app/src/main/res/drawable/yuan_chi_title.png b/android-app/app/src/main/res/drawable/yuan_chi_title.png
new file mode 100644
index 00000000..6fe65268
Binary files /dev/null and b/android-app/app/src/main/res/drawable/yuan_chi_title.png differ
diff --git a/android-app/app/src/main/res/layout/activity_fish_pond.xml b/android-app/app/src/main/res/layout/activity_fish_pond.xml
index 5613a4fd..bc50bf38 100644
--- a/android-app/app/src/main/res/layout/activity_fish_pond.xml
+++ b/android-app/app/src/main/res/layout/activity_fish_pond.xml
@@ -6,14 +6,14 @@
android:background="@drawable/bg_fishpond_gradient"
android:paddingTop="18dp">
-
diff --git a/android-app/app/src/main/res/layout/activity_profile.xml b/android-app/app/src/main/res/layout/activity_profile.xml
index a594e783..5e559cca 100644
--- a/android-app/app/src/main/res/layout/activity_profile.xml
+++ b/android-app/app/src/main/res/layout/activity_profile.xml
@@ -159,6 +159,7 @@
android:textSize="13sp" />
@@ -202,7 +203,7 @@
android:layout_marginEnd="10dp"
android:background="@drawable/bg_circle_white_60"
android:padding="8dp"
- android:src="@drawable/ic_search_24"
+ android:src="@drawable/ic_crosshair_24"
app:layout_constraintEnd_toStartOf="@id/topActionClock"
app:layout_constraintTop_toTopOf="@id/topActionMore" />
@@ -545,7 +546,7 @@
android:layout_height="44dp"
android:background="@drawable/bg_gray_12"
android:gravity="center"
- android:text="编辑资料 40%"
+ android:text="编辑资料"
android:textColor="#111111"
app:layout_constraintEnd_toStartOf="@id/shareHome"
app:layout_constraintStart_toStartOf="parent"
@@ -569,9 +570,10 @@
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_marginStart="12dp"
- android:background="@drawable/bg_gray_12"
- android:padding="10dp"
+ android:background="@drawable/bg_icon_button_12"
+ android:padding="11dp"
android:src="@drawable/ic_person_24"
+ android:tint="@color/purple_500"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/shareHome"
app:layout_constraintTop_toTopOf="parent" />
@@ -584,6 +586,9 @@
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:tabIndicatorColor="@color/purple_500"
+ app:tabIndicatorFullWidth="false"
+ app:tabIndicatorHeight="3dp"
+ app:tabRippleColor="@android:color/transparent"
app:tabSelectedTextColor="#111111"
app:tabTextColor="#666666"
app:layout_constraintEnd_toEndOf="parent"
@@ -612,52 +617,219 @@
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:orientation="vertical"
+ android:visibility="gone">
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/activity_search.xml b/android-app/app/src/main/res/layout/activity_search.xml
new file mode 100644
index 00000000..9ce5fd31
--- /dev/null
+++ b/android-app/app/src/main/res/layout/activity_search.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/activity_settings_page.xml b/android-app/app/src/main/res/layout/activity_settings_page.xml
new file mode 100644
index 00000000..c591b0e7
--- /dev/null
+++ b/android-app/app/src/main/res/layout/activity_settings_page.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 55cc6c39..d75b5118 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
@@ -68,6 +68,845 @@
android:paddingTop="12dp"
android:paddingBottom="24dp">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/activity_wish_tree.xml b/android-app/app/src/main/res/layout/activity_wish_tree.xml
index 26b095fa..cc4a16fc 100644
--- a/android-app/app/src/main/res/layout/activity_wish_tree.xml
+++ b/android-app/app/src/main/res/layout/activity_wish_tree.xml
@@ -23,14 +23,14 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
-
-
diff --git a/android-app/app/src/main/res/layout/item_badge.xml b/android-app/app/src/main/res/layout/item_badge.xml
new file mode 100644
index 00000000..a8204136
--- /dev/null
+++ b/android-app/app/src/main/res/layout/item_badge.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/item_more_row.xml b/android-app/app/src/main/res/layout/item_more_row.xml
new file mode 100644
index 00000000..321bffa3
--- /dev/null
+++ b/android-app/app/src/main/res/layout/item_more_row.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/item_more_section.xml b/android-app/app/src/main/res/layout/item_more_section.xml
new file mode 100644
index 00000000..69c4ffb6
--- /dev/null
+++ b/android-app/app/src/main/res/layout/item_more_section.xml
@@ -0,0 +1,11 @@
+
+
diff --git a/android-app/app/src/main/res/layout/item_search_suggestion.xml b/android-app/app/src/main/res/layout/item_search_suggestion.xml
new file mode 100644
index 00000000..a3b10dc3
--- /dev/null
+++ b/android-app/app/src/main/res/layout/item_search_suggestion.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+