在对应的地方添加了todo,表示需要哪些接口
This commit is contained in:
parent
c5314d6309
commit
484c17a4d3
|
|
@ -60,12 +60,6 @@ android {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 编译优化
|
||||
dexOptions {
|
||||
javaMaxHeapSize = "2g"
|
||||
preDexLibraries = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@
|
|||
android:name="com.example.livestreaming.NotificationSettingsActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name="com.example.livestreaming.NotificationsActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name="com.example.livestreaming.WatchHistoryActivity"
|
||||
android:exported="false" />
|
||||
|
|
|
|||
|
|
@ -1,23 +1,56 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class ChatMessage {
|
||||
public enum MessageStatus {
|
||||
SENDING, // 发送中
|
||||
SENT, // 已发送
|
||||
READ // 已读
|
||||
}
|
||||
|
||||
private String messageId;
|
||||
private String username;
|
||||
private String message;
|
||||
private long timestamp;
|
||||
private boolean isSystemMessage;
|
||||
private MessageStatus status;
|
||||
private String avatarUrl; // 发送者头像 URL,后续从后端获取
|
||||
|
||||
public ChatMessage(String username, String message) {
|
||||
this.messageId = UUID.randomUUID().toString();
|
||||
this.username = username;
|
||||
this.message = message;
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
this.isSystemMessage = false;
|
||||
// 如果是自己发送的消息,初始状态为发送中
|
||||
this.status = "我".equals(username) ? MessageStatus.SENDING : MessageStatus.SENT;
|
||||
}
|
||||
|
||||
public ChatMessage(String message, boolean isSystemMessage) {
|
||||
this.messageId = UUID.randomUUID().toString();
|
||||
this.message = message;
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
this.isSystemMessage = isSystemMessage;
|
||||
this.username = isSystemMessage ? "系统" : "匿名用户";
|
||||
this.status = MessageStatus.SENT; // 系统消息默认已发送
|
||||
}
|
||||
|
||||
public ChatMessage(String messageId, String username, String message, long timestamp, boolean isSystemMessage, MessageStatus status) {
|
||||
this.messageId = messageId;
|
||||
this.username = username;
|
||||
this.message = message;
|
||||
this.timestamp = timestamp;
|
||||
this.isSystemMessage = isSystemMessage;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getMessageId() {
|
||||
return messageId;
|
||||
}
|
||||
|
||||
public void setMessageId(String messageId) {
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
|
|
@ -36,6 +69,14 @@ public class ChatMessage {
|
|||
return isSystemMessage;
|
||||
}
|
||||
|
||||
public MessageStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(MessageStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
|
@ -51,4 +92,12 @@ public class ChatMessage {
|
|||
public void setSystemMessage(boolean systemMessage) {
|
||||
isSystemMessage = systemMessage;
|
||||
}
|
||||
|
||||
public String getAvatarUrl() {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
public void setAvatarUrl(String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,25 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.PopupMenu;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.example.livestreaming.databinding.ActivityConversationBinding;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
|
@ -25,6 +33,8 @@ public class ConversationActivity extends AppCompatActivity {
|
|||
|
||||
private ConversationMessagesAdapter adapter;
|
||||
private final List<ChatMessage> messages = new ArrayList<>();
|
||||
private Handler handler;
|
||||
private Runnable statusUpdateRunnable;
|
||||
|
||||
public static void start(Context context, String conversationId, String title) {
|
||||
Intent intent = new Intent(context, ConversationActivity.class);
|
||||
|
|
@ -43,6 +53,8 @@ public class ConversationActivity extends AppCompatActivity {
|
|||
binding = ActivityConversationBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
handler = new Handler(Looper.getMainLooper());
|
||||
|
||||
String title = getIntent() != null ? getIntent().getStringExtra(EXTRA_CONVERSATION_TITLE) : null;
|
||||
binding.titleText.setText(title != null ? title : "会话");
|
||||
|
||||
|
|
@ -54,7 +66,13 @@ public class ConversationActivity extends AppCompatActivity {
|
|||
setupMessages();
|
||||
setupInput();
|
||||
|
||||
// 用户进入会话时,标记该会话的消息为已读,减少未读数量
|
||||
// TODO: 接入后端接口 - 标记会话消息为已读
|
||||
// 接口路径: POST /api/conversations/{conversationId}/read
|
||||
// 请求参数:
|
||||
// - conversationId: 会话ID(路径参数)
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// 返回数据格式: ApiResponse<{success: boolean}>
|
||||
// 用户进入会话时,标记该会话的所有消息为已读,减少未读数量
|
||||
if (initialUnreadCount > 0) {
|
||||
UnreadMessageManager.decrementUnreadCount(this, initialUnreadCount);
|
||||
}
|
||||
|
|
@ -68,19 +86,90 @@ public class ConversationActivity extends AppCompatActivity {
|
|||
|
||||
private void setupMessages() {
|
||||
adapter = new ConversationMessagesAdapter();
|
||||
|
||||
// 设置长按监听
|
||||
adapter.setOnMessageLongClickListener((message, position, view) -> {
|
||||
showMessageMenu(message, position, view);
|
||||
});
|
||||
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
layoutManager.setStackFromEnd(false);
|
||||
binding.messagesRecyclerView.setLayoutManager(layoutManager);
|
||||
binding.messagesRecyclerView.setAdapter(adapter);
|
||||
|
||||
// TODO: 接入后端接口 - 获取会话消息列表
|
||||
// 接口路径: GET /api/conversations/{conversationId}/messages
|
||||
// 请求参数:
|
||||
// - conversationId: 会话ID(路径参数)
|
||||
// - page (可选): 页码,用于分页加载历史消息
|
||||
// - pageSize (可选): 每页数量,默认20
|
||||
// - beforeMessageId (可选): 获取指定消息ID之前的消息,用于上拉加载更多
|
||||
// 返回数据格式: ApiResponse<List<ChatMessage>>
|
||||
// ChatMessage对象应包含: messageId, userId, username, avatarUrl, message, timestamp, status等字段
|
||||
// 消息列表应按时间正序排列(最早的在前面)
|
||||
messages.clear();
|
||||
String title = binding.titleText.getText() != null ? binding.titleText.getText().toString() : "";
|
||||
messages.add(new ChatMessage(title, "你好~"));
|
||||
messages.add(new ChatMessage("我", "在的,有什么需要帮忙?"));
|
||||
ChatMessage incomingMsg = new ChatMessage(title, "你好~");
|
||||
incomingMsg.setStatus(ChatMessage.MessageStatus.SENT);
|
||||
messages.add(incomingMsg);
|
||||
|
||||
ChatMessage outgoingMsg = new ChatMessage("我", "在的,有什么需要帮忙?");
|
||||
outgoingMsg.setStatus(ChatMessage.MessageStatus.READ);
|
||||
messages.add(outgoingMsg);
|
||||
|
||||
adapter.submitList(new ArrayList<>(messages));
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
private void showMessageMenu(ChatMessage message, int position, View anchorView) {
|
||||
PopupMenu popupMenu = new PopupMenu(this, anchorView);
|
||||
popupMenu.getMenu().add(0, 0, 0, "复制");
|
||||
popupMenu.getMenu().add(0, 1, 0, "删除");
|
||||
|
||||
popupMenu.setOnMenuItemClickListener(item -> {
|
||||
if (item.getItemId() == 0) {
|
||||
// 复制消息
|
||||
copyMessage(message);
|
||||
return true;
|
||||
} else if (item.getItemId() == 1) {
|
||||
// 删除消息
|
||||
deleteMessage(position);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
popupMenu.show();
|
||||
}
|
||||
|
||||
private void copyMessage(ChatMessage message) {
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText("消息", message.getMessage());
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Snackbar.make(binding.getRoot(), "已复制到剪贴板", Snackbar.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private void deleteMessage(int position) {
|
||||
// TODO: 接入后端接口 - 删除消息
|
||||
// 接口路径: DELETE /api/messages/{messageId}
|
||||
// 请求参数:
|
||||
// - messageId: 消息ID(路径参数)
|
||||
// - userId: 当前用户ID(从token中获取,验证是否为消息发送者)
|
||||
// 返回数据格式: ApiResponse<{success: boolean}>
|
||||
// 删除成功后,从本地消息列表移除
|
||||
if (position < 0 || position >= messages.size()) return;
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("删除消息")
|
||||
.setMessage("确定要删除这条消息吗?")
|
||||
.setPositiveButton("删除", (dialog, which) -> {
|
||||
messages.remove(position);
|
||||
adapter.submitList(new ArrayList<>(messages));
|
||||
Snackbar.make(binding.getRoot(), "消息已删除", Snackbar.LENGTH_SHORT).show();
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void setupInput() {
|
||||
binding.sendButton.setOnClickListener(v -> sendMessage());
|
||||
|
|
@ -99,17 +188,70 @@ public class ConversationActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void sendMessage() {
|
||||
// TODO: 接入后端接口 - 发送私信消息
|
||||
// 接口路径: POST /api/conversations/{conversationId}/messages
|
||||
// 请求参数:
|
||||
// - conversationId: 会话ID(路径参数)
|
||||
// - message: 消息内容
|
||||
// - userId: 发送者用户ID(从token中获取)
|
||||
// 返回数据格式: ApiResponse<ChatMessage>
|
||||
// ChatMessage对象应包含: messageId, userId, username, avatarUrl, message, timestamp, status等字段
|
||||
// 发送成功后,更新消息状态为SENT,并添加到消息列表
|
||||
// TODO: 接入后端接口 - 接收对方已读状态(WebSocket或轮询)
|
||||
// 方案1: WebSocket实时推送
|
||||
// - 连接: ws://api.example.com/conversations/{conversationId}
|
||||
// - 接收消息格式: {type: "read", messageId: "xxx"} 或 {type: "new_message", data: ChatMessage}
|
||||
// 方案2: 轮询检查消息状态
|
||||
// - 接口路径: GET /api/conversations/{conversationId}/messages/{messageId}/status
|
||||
// - 返回数据格式: ApiResponse<{status: "sent"|"read"}>
|
||||
String text = binding.messageInput.getText() != null ? binding.messageInput.getText().toString().trim() : "";
|
||||
if (TextUtils.isEmpty(text)) return;
|
||||
|
||||
messages.add(new ChatMessage("我", text));
|
||||
ChatMessage newMessage = new ChatMessage("我", text);
|
||||
newMessage.setStatus(ChatMessage.MessageStatus.SENDING);
|
||||
messages.add(newMessage);
|
||||
adapter.submitList(new ArrayList<>(messages));
|
||||
binding.messageInput.setText("");
|
||||
scrollToBottom();
|
||||
|
||||
// 模拟消息发送过程:发送中 -> 已发送 -> 已读
|
||||
if (statusUpdateRunnable != null) {
|
||||
handler.removeCallbacks(statusUpdateRunnable);
|
||||
}
|
||||
|
||||
statusUpdateRunnable = () -> {
|
||||
// 1秒后更新为已发送
|
||||
newMessage.setStatus(ChatMessage.MessageStatus.SENT);
|
||||
adapter.notifyItemChanged(messages.size() - 1);
|
||||
|
||||
// 再2秒后更新为已读
|
||||
handler.postDelayed(() -> {
|
||||
newMessage.setStatus(ChatMessage.MessageStatus.READ);
|
||||
adapter.notifyItemChanged(messages.size() - 1);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
handler.postDelayed(statusUpdateRunnable, 1000);
|
||||
}
|
||||
|
||||
private void scrollToBottom() {
|
||||
if (messages.isEmpty()) return;
|
||||
binding.messagesRecyclerView.post(() -> binding.messagesRecyclerView.scrollToPosition(messages.size() - 1));
|
||||
binding.messagesRecyclerView.post(() -> {
|
||||
int lastPosition = messages.size() - 1;
|
||||
if (lastPosition >= 0) {
|
||||
binding.messagesRecyclerView.smoothScrollToPosition(lastPosition);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
// 清理Handler中的延迟任务,防止内存泄漏
|
||||
if (handler != null && statusUpdateRunnable != null) {
|
||||
handler.removeCallbacks(statusUpdateRunnable);
|
||||
}
|
||||
handler = null;
|
||||
statusUpdateRunnable = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import android.widget.ImageView;
|
|||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
|
@ -23,6 +24,12 @@ import java.util.Locale;
|
|||
|
||||
public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, RecyclerView.ViewHolder> {
|
||||
|
||||
public interface OnMessageLongClickListener {
|
||||
void onMessageLongClick(ChatMessage message, int position, View view);
|
||||
}
|
||||
|
||||
private OnMessageLongClickListener longClickListener;
|
||||
|
||||
private static final int TYPE_INCOMING = 1;
|
||||
private static final int TYPE_OUTGOING = 2;
|
||||
|
||||
|
|
@ -32,6 +39,10 @@ public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, Recycl
|
|||
super(DIFF);
|
||||
}
|
||||
|
||||
public void setOnMessageLongClickListener(OnMessageLongClickListener listener) {
|
||||
this.longClickListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
ChatMessage msg = getItem(position);
|
||||
|
|
@ -56,9 +67,9 @@ public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, Recycl
|
|||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
ChatMessage msg = getItem(position);
|
||||
if (holder instanceof IncomingVH) {
|
||||
((IncomingVH) holder).bind(msg);
|
||||
((IncomingVH) holder).bind(msg, longClickListener);
|
||||
} else if (holder instanceof OutgoingVH) {
|
||||
((OutgoingVH) holder).bind(msg);
|
||||
((OutgoingVH) holder).bind(msg, longClickListener);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -94,49 +105,107 @@ public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, Recycl
|
|||
});
|
||||
}
|
||||
|
||||
void bind(ChatMessage message) {
|
||||
void bind(ChatMessage message, OnMessageLongClickListener listener) {
|
||||
if (message == null) return;
|
||||
nameText.setText(message.getUsername() != null ? message.getUsername() : "");
|
||||
msgText.setText(message.getMessage() != null ? message.getMessage() : "");
|
||||
timeText.setText(TIME_FORMAT.format(new Date(message.getTimestamp())));
|
||||
|
||||
// 清除之前的监听器,避免重复绑定
|
||||
itemView.setOnClickListener(null);
|
||||
itemView.setOnLongClickListener(null);
|
||||
|
||||
// 设置长按监听
|
||||
itemView.setOnLongClickListener(v -> {
|
||||
if (listener != null) {
|
||||
int position = getBindingAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onMessageLongClick(message, position, v);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// 确保头像圆形裁剪设置正确
|
||||
setupAvatarOutline();
|
||||
// 加载对方头像(可以根据用户名或其他信息加载)
|
||||
loadAvatar(message.getUsername());
|
||||
// 加载对方头像(优先使用消息中的头像 URL,如果没有则使用默认头像)
|
||||
loadAvatar(message);
|
||||
}
|
||||
|
||||
private void loadAvatar(String username) {
|
||||
private void loadAvatar(ChatMessage message) {
|
||||
// TODO: 接入后端接口 - 加载消息发送者头像
|
||||
// 接口路径: GET /api/users/{userId}/avatar 或直接从ChatMessage的avatarUrl字段获取
|
||||
// ChatMessage对象应包含avatarUrl字段,如果为空,则根据userId调用接口获取
|
||||
// 建议后端在返回消息列表时,每条消息都包含发送者的avatarUrl,避免额外请求
|
||||
if (avatarView == null) return;
|
||||
|
||||
// 这里可以根据用户名加载对应的头像
|
||||
// 暂时使用默认头像,后续可以根据实际需求从服务器或本地加载
|
||||
try {
|
||||
// 可以根据username从SharedPreferences或其他地方加载头像
|
||||
// 这里先使用默认头像,使用Glide确保圆形裁剪
|
||||
// 优先使用消息中的头像 URL(从后端获取)
|
||||
String avatarUrl = message != null ? message.getAvatarUrl() : null;
|
||||
if (!TextUtils.isEmpty(avatarUrl)) {
|
||||
Glide.with(avatarView)
|
||||
.load(avatarUrl)
|
||||
.circleCrop()
|
||||
.error(R.drawable.ic_account_circle_24)
|
||||
.placeholder(R.drawable.ic_account_circle_24)
|
||||
.into(avatarView);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果没有头像 URL,暂时根据用户名生成不同的默认头像
|
||||
// 这样不同发送者至少能通过不同的默认头像区分开
|
||||
String username = message != null ? message.getUsername() : null;
|
||||
int defaultAvatarRes = getDefaultAvatarForUsername(username);
|
||||
Glide.with(avatarView)
|
||||
.load(R.drawable.ic_account_circle_24)
|
||||
.load(defaultAvatarRes)
|
||||
.circleCrop()
|
||||
.into(avatarView);
|
||||
} catch (Exception e) {
|
||||
// 如果加载失败,使用默认头像
|
||||
Glide.with(avatarView)
|
||||
.load(R.drawable.ic_account_circle_24)
|
||||
.circleCrop()
|
||||
.into(avatarView);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名生成一个稳定的默认头像资源
|
||||
* 这样同一个用户名的消息会显示相同的默认头像
|
||||
* TODO: 后续接入后端接口后,这个方法可以改为从后端获取头像 URL
|
||||
*/
|
||||
private int getDefaultAvatarForUsername(String username) {
|
||||
if (TextUtils.isEmpty(username)) {
|
||||
return R.drawable.ic_account_circle_24;
|
||||
}
|
||||
|
||||
// 使用用户名的哈希值来选择不同的默认头像
|
||||
// 这样不同的发送者至少能通过不同的默认头像区分开
|
||||
int hash = Math.abs(username.hashCode());
|
||||
int[] defaultAvatars = {
|
||||
R.drawable.ic_account_circle_24,
|
||||
// 如果有其他默认头像资源,可以在这里添加
|
||||
// R.drawable.default_avatar_1,
|
||||
// R.drawable.default_avatar_2,
|
||||
};
|
||||
|
||||
return defaultAvatars[hash % defaultAvatars.length];
|
||||
}
|
||||
}
|
||||
|
||||
static class OutgoingVH extends RecyclerView.ViewHolder {
|
||||
private final ImageView avatarView;
|
||||
private final TextView msgText;
|
||||
private final TextView timeText;
|
||||
private final ImageView statusIcon;
|
||||
|
||||
OutgoingVH(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
avatarView = itemView.findViewById(R.id.avatarView);
|
||||
msgText = itemView.findViewById(R.id.messageText);
|
||||
timeText = itemView.findViewById(R.id.timeText);
|
||||
statusIcon = itemView.findViewById(R.id.statusIcon);
|
||||
setupAvatarOutline();
|
||||
}
|
||||
|
||||
|
|
@ -157,11 +226,51 @@ public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, Recycl
|
|||
});
|
||||
}
|
||||
|
||||
void bind(ChatMessage message) {
|
||||
void bind(ChatMessage message, OnMessageLongClickListener listener) {
|
||||
if (message == null) return;
|
||||
msgText.setText(message.getMessage() != null ? message.getMessage() : "");
|
||||
timeText.setText(TIME_FORMAT.format(new Date(message.getTimestamp())));
|
||||
|
||||
// 显示消息状态图标(仅对发送的消息显示)
|
||||
if (statusIcon != null && message.getStatus() != null) {
|
||||
switch (message.getStatus()) {
|
||||
case SENDING:
|
||||
statusIcon.setImageResource(R.drawable.ic_clock_24);
|
||||
statusIcon.setColorFilter(itemView.getContext().getColor(android.R.color.darker_gray));
|
||||
statusIcon.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
case SENT:
|
||||
statusIcon.setImageResource(R.drawable.ic_check_24);
|
||||
statusIcon.setColorFilter(itemView.getContext().getColor(android.R.color.darker_gray));
|
||||
statusIcon.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
case READ:
|
||||
statusIcon.setImageResource(R.drawable.ic_check_double_24);
|
||||
statusIcon.setColorFilter(0xFF4CAF50);
|
||||
statusIcon.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
default:
|
||||
statusIcon.setVisibility(View.GONE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 清除之前的监听器,避免重复绑定
|
||||
itemView.setOnClickListener(null);
|
||||
itemView.setOnLongClickListener(null);
|
||||
|
||||
// 设置长按监听
|
||||
itemView.setOnLongClickListener(v -> {
|
||||
if (listener != null) {
|
||||
int position = getBindingAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onMessageLongClick(message, position, v);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// 确保头像圆形裁剪设置正确
|
||||
setupAvatarOutline();
|
||||
// 加载用户头像
|
||||
|
|
@ -177,10 +286,27 @@ public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, Recycl
|
|||
.getString("profile_avatar_uri", null);
|
||||
|
||||
if (!TextUtils.isEmpty(avatarUri)) {
|
||||
Uri uri = Uri.parse(avatarUri);
|
||||
// 如果是 file:// 协议,尝试转换为 FileProvider URI
|
||||
if ("file".equals(uri.getScheme())) {
|
||||
try {
|
||||
java.io.File file = new java.io.File(uri.getPath());
|
||||
if (file.exists()) {
|
||||
uri = FileProvider.getUriForFile(
|
||||
avatarView.getContext(),
|
||||
avatarView.getContext().getPackageName() + ".fileprovider",
|
||||
file
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 如果转换失败,使用原始 URI
|
||||
}
|
||||
}
|
||||
Glide.with(avatarView)
|
||||
.load(Uri.parse(avatarUri))
|
||||
.load(uri)
|
||||
.circleCrop()
|
||||
.error(R.drawable.ic_account_circle_24)
|
||||
.placeholder(R.drawable.ic_account_circle_24)
|
||||
.into(avatarView);
|
||||
return;
|
||||
}
|
||||
|
|
@ -213,12 +339,16 @@ public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, Recycl
|
|||
private static final DiffUtil.ItemCallback<ChatMessage> DIFF = new DiffUtil.ItemCallback<ChatMessage>() {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull ChatMessage oldItem, @NonNull ChatMessage newItem) {
|
||||
return oldItem.getTimestamp() == newItem.getTimestamp();
|
||||
// 使用messageId作为唯一标识
|
||||
return oldItem.getMessageId() != null && oldItem.getMessageId().equals(newItem.getMessageId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull ChatMessage oldItem, @NonNull ChatMessage newItem) {
|
||||
return oldItem.getTimestamp() == newItem.getTimestamp();
|
||||
// 比较消息内容、状态等是否相同
|
||||
return oldItem.getMessageId() != null && oldItem.getMessageId().equals(newItem.getMessageId()) &&
|
||||
oldItem.getStatus() == newItem.getStatus() &&
|
||||
oldItem.getMessage() != null && oldItem.getMessage().equals(newItem.getMessage());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import android.view.ViewGroup;
|
|||
import android.view.ViewOutlineProvider;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
|
@ -65,12 +66,6 @@ public class ConversationsAdapter extends ListAdapter<ConversationItem, Conversa
|
|||
binding.unreadBadge.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (item != null && item.isMuted()) {
|
||||
binding.muteIcon.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.muteIcon.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// 加载头像
|
||||
loadAvatar(item);
|
||||
|
||||
|
|
@ -81,24 +76,71 @@ public class ConversationsAdapter extends ListAdapter<ConversationItem, Conversa
|
|||
}
|
||||
|
||||
private void loadAvatar(ConversationItem item) {
|
||||
// TODO: 接入后端接口 - 加载会话对方头像
|
||||
// 接口路径: GET /api/users/{userId}/avatar 或直接从ConversationItem的avatarUrl字段获取
|
||||
// 如果ConversationItem对象包含avatarUrl字段,直接使用该URL加载头像
|
||||
// 如果没有avatarUrl,需要根据会话类型(私信/群聊)调用相应接口获取头像
|
||||
if (binding.avatar == null) return;
|
||||
|
||||
// 设置圆形裁剪
|
||||
setupAvatarOutline();
|
||||
|
||||
// 根据会话ID或标题加载对应的头像
|
||||
// 这里可以根据实际需求从服务器或本地加载
|
||||
// 暂时使用默认头像,或者根据会话类型加载不同头像
|
||||
try {
|
||||
String conversationId = item != null ? item.getId() : null;
|
||||
// 优先使用ConversationItem中的avatarUrl(从后端获取)
|
||||
// 尝试从 SharedPreferences 加载用户头像
|
||||
String avatarUri = binding.avatar.getContext()
|
||||
.getSharedPreferences("profile_prefs", android.content.Context.MODE_PRIVATE)
|
||||
.getString("profile_avatar_uri", null);
|
||||
|
||||
// 可以根据conversationId从SharedPreferences或其他地方加载头像
|
||||
// 这里先使用默认头像,使用Glide确保圆形裁剪
|
||||
if (!TextUtils.isEmpty(avatarUri)) {
|
||||
// 尝试解析 URI
|
||||
Uri uri = Uri.parse(avatarUri);
|
||||
// 如果是 file:// 协议,尝试转换为 FileProvider URI
|
||||
if ("file".equals(uri.getScheme())) {
|
||||
try {
|
||||
java.io.File file = new java.io.File(uri.getPath());
|
||||
if (file.exists()) {
|
||||
uri = FileProvider.getUriForFile(
|
||||
binding.avatar.getContext(),
|
||||
binding.avatar.getContext().getPackageName() + ".fileprovider",
|
||||
file
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 如果转换失败,使用原始 URI
|
||||
}
|
||||
}
|
||||
|
||||
Glide.with(binding.avatar)
|
||||
.load(uri)
|
||||
.circleCrop()
|
||||
.error(R.drawable.ic_account_circle_24)
|
||||
.placeholder(R.drawable.ic_account_circle_24)
|
||||
.into(binding.avatar);
|
||||
return;
|
||||
}
|
||||
|
||||
int avatarRes = binding.avatar.getContext()
|
||||
.getSharedPreferences("profile_prefs", android.content.Context.MODE_PRIVATE)
|
||||
.getInt("profile_avatar_res", 0);
|
||||
|
||||
if (avatarRes != 0) {
|
||||
Glide.with(binding.avatar)
|
||||
.load(avatarRes)
|
||||
.circleCrop()
|
||||
.error(R.drawable.ic_account_circle_24)
|
||||
.placeholder(R.drawable.ic_account_circle_24)
|
||||
.into(binding.avatar);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果没有保存的头像,使用默认头像
|
||||
Glide.with(binding.avatar)
|
||||
.load(R.drawable.ic_account_circle_24)
|
||||
.circleCrop()
|
||||
.into(binding.avatar);
|
||||
} catch (Exception e) {
|
||||
// 如果加载失败,使用默认头像
|
||||
Glide.with(binding.avatar)
|
||||
.load(R.drawable.ic_account_circle_24)
|
||||
.circleCrop()
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ public class DrawGuessActivity extends AppCompatActivity {
|
|||
|
||||
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
|
||||
bottomNavigation.setSelectedItemId(R.id.nav_friends);
|
||||
|
||||
// 更新未读消息徽章
|
||||
UnreadMessageManager.updateBadge(bottomNavigation);
|
||||
|
||||
bottomNavigation.setOnItemSelectedListener(item -> {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.nav_home) {
|
||||
|
|
@ -57,5 +61,16 @@ public class DrawGuessActivity extends AppCompatActivity {
|
|||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (binding != null) {
|
||||
BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
|
||||
bottomNav.setSelectedItemId(R.id.nav_friends);
|
||||
// 更新未读消息徽章
|
||||
UnreadMessageManager.updateBadge(bottomNav);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -216,7 +216,8 @@ public class EditProfileActivity extends AppCompatActivity {
|
|||
}
|
||||
out.flush();
|
||||
|
||||
return Uri.fromFile(file);
|
||||
// 使用 FileProvider 生成 URI,确保在 Android 10+ 上也能正常访问
|
||||
return FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", file);
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(this, "头像保存失败", Toast.LENGTH_SHORT).show();
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,15 @@ public class FansListActivity extends AppCompatActivity {
|
|||
Toast.makeText(this, "打开粉丝:" + item.getName(), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
|
||||
// TODO: 接入后端接口 - 获取粉丝列表
|
||||
// 接口路径: GET /api/fans
|
||||
// 请求参数:
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<User>>
|
||||
// User对象应包含: id, name, avatarUrl, bio, isLive, followTime等字段
|
||||
// 列表应按关注时间倒序排列(最新关注的在前)
|
||||
binding.fansRecyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
binding.fansRecyclerView.setAdapter(adapter);
|
||||
adapter.submitList(buildDemoFans());
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ public class FindGameActivity extends AppCompatActivity {
|
|||
|
||||
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
|
||||
bottomNavigation.setSelectedItemId(R.id.nav_friends);
|
||||
|
||||
// 更新未读消息徽章
|
||||
UnreadMessageManager.updateBadge(bottomNavigation);
|
||||
|
||||
bottomNavigation.setOnItemSelectedListener(item -> {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.nav_home) {
|
||||
|
|
@ -57,5 +61,16 @@ public class FindGameActivity extends AppCompatActivity {
|
|||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (binding != null) {
|
||||
BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
|
||||
bottomNav.setSelectedItemId(R.id.nav_friends);
|
||||
// 更新未读消息徽章
|
||||
UnreadMessageManager.updateBadge(bottomNav);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -320,6 +320,16 @@ public class FishPondActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void refreshUsers() {
|
||||
// TODO: 接入后端接口 - 获取附近用户(缘池功能)
|
||||
// 接口路径: GET /api/users/nearby
|
||||
// 请求参数:
|
||||
// - latitude: 当前用户纬度(必填)
|
||||
// - longitude: 当前用户经度(必填)
|
||||
// - radius (可选): 搜索半径(单位:米,默认5000)
|
||||
// - limit (可选): 返回数量,默认6
|
||||
// 返回数据格式: ApiResponse<List<User>>
|
||||
// User对象应包含: id, name, avatarUrl, location, bio, distance(距离,单位:米), isLive等字段
|
||||
// 返回距离最近的N个用户,用于显示在轨道上
|
||||
if (binding == null) return;
|
||||
|
||||
int[] avatars = new int[] {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,15 @@ public class FollowingListActivity extends AppCompatActivity {
|
|||
Toast.makeText(this, "打开关注:" + item.getName(), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
|
||||
// TODO: 接入后端接口 - 获取关注列表
|
||||
// 接口路径: GET /api/following
|
||||
// 请求参数:
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<User>>
|
||||
// User对象应包含: id, name, avatarUrl, bio, isLive, lastLiveTime, followTime等字段
|
||||
// 列表应按关注时间倒序或最后直播时间倒序排列
|
||||
binding.followingRecyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
binding.followingRecyclerView.setAdapter(adapter);
|
||||
adapter.submitList(buildDemoFollowing());
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ public class HeartbeatSignalActivity extends AppCompatActivity {
|
|||
|
||||
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
|
||||
bottomNavigation.setSelectedItemId(R.id.nav_friends);
|
||||
|
||||
// 更新未读消息徽章
|
||||
UnreadMessageManager.updateBadge(bottomNavigation);
|
||||
|
||||
bottomNavigation.setOnItemSelectedListener(item -> {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.nav_home) {
|
||||
|
|
@ -58,5 +62,16 @@ public class HeartbeatSignalActivity extends AppCompatActivity {
|
|||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (binding != null) {
|
||||
BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
|
||||
bottomNav.setSelectedItemId(R.id.nav_friends);
|
||||
// 更新未读消息徽章
|
||||
UnreadMessageManager.updateBadge(bottomNav);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ public class KTVTogetherActivity extends AppCompatActivity {
|
|||
|
||||
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
|
||||
bottomNavigation.setSelectedItemId(R.id.nav_friends);
|
||||
|
||||
// 更新未读消息徽章
|
||||
UnreadMessageManager.updateBadge(bottomNavigation);
|
||||
|
||||
bottomNavigation.setOnItemSelectedListener(item -> {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.nav_home) {
|
||||
|
|
@ -57,5 +61,16 @@ public class KTVTogetherActivity extends AppCompatActivity {
|
|||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (binding != null) {
|
||||
BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
|
||||
bottomNav.setSelectedItemId(R.id.nav_friends);
|
||||
// 更新未读消息徽章
|
||||
UnreadMessageManager.updateBadge(bottomNav);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,15 @@ public class LikesListActivity extends AppCompatActivity {
|
|||
Toast.makeText(this, "查看获赞:" + item.getTitle(), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
|
||||
// TODO: 接入后端接口 - 获取获赞列表
|
||||
// 接口路径: GET /api/likes
|
||||
// 请求参数:
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<LikeItem>>
|
||||
// LikeItem对象应包含: id, userId, username, avatarUrl, targetType (room/work), targetId, targetTitle, likeTime等字段
|
||||
// 列表应按点赞时间倒序排列(最新点赞的在前面)
|
||||
binding.likesRecyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
binding.likesRecyclerView.setAdapter(adapter);
|
||||
adapter.submitList(buildDemoLikes());
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ package com.example.livestreaming;
|
|||
|
||||
import android.app.Application;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
/**
|
||||
* 自定义Application类,用于初始化各种组件
|
||||
* 包括内存泄漏检测等
|
||||
|
|
@ -16,5 +14,8 @@ public class LiveStreamingApplication extends Application {
|
|||
|
||||
// 初始化LeakCanary内存泄漏检测(仅在debug版本中生效)
|
||||
// LeakCanary会自动在debug版本中初始化,无需手动调用
|
||||
|
||||
// 初始化通知渠道
|
||||
LocalNotificationManager.createNotificationChannel(this);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,186 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
/**
|
||||
* 本地通知管理器
|
||||
* 用于发送本地通知(即使后端未就绪)
|
||||
*/
|
||||
public class LocalNotificationManager {
|
||||
|
||||
private static final String CHANNEL_ID = "livestreaming_notifications";
|
||||
private static final String CHANNEL_NAME = "直播应用通知";
|
||||
private static final String PREFS_NAME = "notification_prefs";
|
||||
private static final String KEY_NOTIFICATION_ID = "notification_id";
|
||||
|
||||
/**
|
||||
* 初始化通知渠道(Android 8.0+需要)
|
||||
*/
|
||||
public static void createNotificationChannel(Context context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||
if (notificationManager == null) return;
|
||||
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
);
|
||||
channel.setDescription("直播应用通知渠道");
|
||||
channel.enableLights(true);
|
||||
channel.enableVibration(true);
|
||||
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送通知
|
||||
*/
|
||||
public static void sendNotification(Context context, String title, String content, NotificationItem.Type type) {
|
||||
if (context == null || title == null || content == null) return;
|
||||
|
||||
// 检查通知设置
|
||||
if (!shouldShowNotification(context, type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||
if (notificationManager == null) return;
|
||||
|
||||
// 创建点击通知后的Intent
|
||||
Intent intent = new Intent(context, NotificationsActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||
);
|
||||
|
||||
// 构建通知
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(getIconForType(type))
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(content));
|
||||
|
||||
// 发送通知
|
||||
int notificationId = getNextNotificationId(context);
|
||||
notificationManager.notify(notificationId, builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送系统通知
|
||||
*/
|
||||
public static void sendSystemNotification(Context context, String title, String content) {
|
||||
sendNotification(context, title, content, NotificationItem.Type.SYSTEM);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送互动通知
|
||||
*/
|
||||
public static void sendInteractionNotification(Context context, String title, String content) {
|
||||
sendNotification(context, title, content, NotificationItem.Type.INTERACTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送关注通知
|
||||
*/
|
||||
public static void sendFollowNotification(Context context, String title, String content) {
|
||||
sendNotification(context, title, content, NotificationItem.Type.FOLLOW);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送私信通知
|
||||
*/
|
||||
public static void sendMessageNotification(Context context, String title, String content) {
|
||||
sendNotification(context, title, content, NotificationItem.Type.MESSAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送直播通知
|
||||
*/
|
||||
public static void sendLiveNotification(Context context, String title, String content) {
|
||||
sendNotification(context, title, content, NotificationItem.Type.LIVE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否应该显示通知
|
||||
*/
|
||||
private static boolean shouldShowNotification(Context context, NotificationItem.Type type) {
|
||||
SharedPreferences prefs = context.getSharedPreferences("notification_settings", Context.MODE_PRIVATE);
|
||||
|
||||
// 检查系统通知总开关
|
||||
boolean systemEnabled = prefs.getBoolean("system_notifications", true);
|
||||
if (!systemEnabled) return false;
|
||||
|
||||
// 检查免打扰
|
||||
boolean dndEnabled = prefs.getBoolean("dnd_enabled", false);
|
||||
if (dndEnabled) {
|
||||
// 检查当前时间是否在免打扰时段
|
||||
// 这里简化处理,实际应该检查具体时间
|
||||
}
|
||||
|
||||
// 检查具体类型的通知开关
|
||||
switch (type) {
|
||||
case FOLLOW:
|
||||
return prefs.getBoolean("follow_notifications", true);
|
||||
case INTERACTION:
|
||||
return prefs.getBoolean("comment_notifications", true);
|
||||
case MESSAGE:
|
||||
return prefs.getBoolean("message_notifications", true);
|
||||
case LIVE:
|
||||
return prefs.getBoolean("live_notifications", true);
|
||||
case SYSTEM:
|
||||
default:
|
||||
return systemEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通知类型的图标
|
||||
*/
|
||||
private static int getIconForType(NotificationItem.Type type) {
|
||||
if (type == null) return android.R.drawable.ic_dialog_info;
|
||||
|
||||
switch (type) {
|
||||
case SYSTEM:
|
||||
return android.R.drawable.ic_dialog_info;
|
||||
case INTERACTION:
|
||||
return android.R.drawable.ic_menu_share;
|
||||
case FOLLOW:
|
||||
return android.R.drawable.ic_menu_myplaces;
|
||||
case MESSAGE:
|
||||
return android.R.drawable.ic_dialog_email;
|
||||
case LIVE:
|
||||
return android.R.drawable.ic_media_play;
|
||||
default:
|
||||
return android.R.drawable.ic_dialog_info;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一个通知ID
|
||||
*/
|
||||
private static int getNextNotificationId(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
int id = prefs.getInt(KEY_NOTIFICATION_ID, 0);
|
||||
id++;
|
||||
if (id > 10000) id = 1; // 防止ID过大
|
||||
prefs.edit().putInt(KEY_NOTIFICATION_ID, id).apply();
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -25,6 +25,7 @@ import androidx.annotation.NonNull;
|
|||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.GravityCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
|
|
@ -81,6 +82,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;
|
||||
|
|
@ -99,26 +101,36 @@ public class MainActivity extends AppCompatActivity {
|
|||
loadAvatarFromPrefs();
|
||||
setupSpeechRecognizer();
|
||||
|
||||
// TODO: 接入后端接口 - 获取未读消息总数
|
||||
// 接口路径: GET /api/messages/unread/count
|
||||
// 请求参数: 无(从token中获取userId)
|
||||
// 返回数据格式: ApiResponse<Integer> 或 ApiResponse<{unreadCount: number}>
|
||||
// 返回当前用户所有会话的未读消息总数
|
||||
// 初始化未读消息数量(演示数据)
|
||||
if (UnreadMessageManager.getUnreadCount(this) == 0) {
|
||||
// 从消息列表计算总未读数量
|
||||
UnreadMessageManager.setUnreadCount(this, calculateTotalUnreadCount());
|
||||
}
|
||||
|
||||
// 清除默认选中状态,让所有标签页初始显示为未选中样式
|
||||
// 初始化顶部标签页数据
|
||||
initializeTopTabData();
|
||||
|
||||
// 默认显示"发现"界面
|
||||
if (binding != null && binding.topTabs != null) {
|
||||
// 在布局完成后清除默认选中状态
|
||||
// 在布局完成后,默认选中"发现"标签(索引为1:关注=0, 发现=1, 附近=2)
|
||||
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;
|
||||
}
|
||||
TabLayout.Tab discoverTab = binding.topTabs.getTabAt(1); // "发现"标签的索引
|
||||
if (discoverTab != null) {
|
||||
// 选中"发现"标签,这会触发 onTabSelected 监听器,自动调用 showDiscoverTab()
|
||||
discoverTab.select();
|
||||
} else {
|
||||
// 如果找不到标签,直接显示发现页面
|
||||
showDiscoverTab();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 如果 topTabs 为空,直接显示发现页面
|
||||
showDiscoverTab();
|
||||
}
|
||||
|
||||
// 异步加载资源文件,避免阻塞主线程
|
||||
|
|
@ -241,12 +253,8 @@ public class MainActivity extends AppCompatActivity {
|
|||
animator.setChangeDuration(200); // 变更动画时长
|
||||
binding.roomsRecyclerView.setItemAnimator(animator);
|
||||
|
||||
// 立即显示演示数据,提升用户体验
|
||||
// 注意:如果后续需要从网络加载,骨架屏会在fetchRooms中显示
|
||||
allRooms.clear();
|
||||
allRooms.addAll(buildDemoRooms(20));
|
||||
// 使用带动画的筛选方法
|
||||
applyCategoryFilterWithAnimation(currentCategory);
|
||||
// 注意:房间数据会在 showDiscoverTab() 中从 discoverRooms 加载
|
||||
// 这里不再直接显示数据,而是等待顶部标签初始化完成后再显示
|
||||
}
|
||||
|
||||
private void setupUI() {
|
||||
|
|
@ -270,6 +278,16 @@ public class MainActivity extends AppCompatActivity {
|
|||
finish();
|
||||
});
|
||||
|
||||
// 设置通知图标点击事件(如果存在)
|
||||
try {
|
||||
View notificationIcon = findViewById(R.id.notificationIcon);
|
||||
if (notificationIcon != null) {
|
||||
notificationIcon.setOnClickListener(v -> NotificationsActivity.start(this));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 如果通知图标不存在,忽略错误
|
||||
}
|
||||
|
||||
binding.topTabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {
|
||||
|
|
@ -413,10 +431,14 @@ public class MainActivity extends AppCompatActivity {
|
|||
// 文本为空,显示麦克风图标
|
||||
binding.micIcon.setImageResource(R.drawable.ic_mic_24);
|
||||
binding.micIcon.setContentDescription("mic");
|
||||
// 清除搜索筛选,显示所有房间
|
||||
applySearchFilter("");
|
||||
} else {
|
||||
// 文本不为空,显示搜索图标
|
||||
binding.micIcon.setImageResource(R.drawable.ic_search_24);
|
||||
binding.micIcon.setContentDescription("search");
|
||||
// 实时筛选房间
|
||||
applySearchFilter(text);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -435,6 +457,13 @@ public class MainActivity extends AppCompatActivity {
|
|||
startVoiceRecognition();
|
||||
} else {
|
||||
// 如果文本不为空,执行搜索
|
||||
// TODO: 接入后端接口 - 搜索功能
|
||||
// 接口路径: GET /api/search
|
||||
// 请求参数:
|
||||
// - keyword: 搜索关键词
|
||||
// - type (可选): 搜索类型(room/user/all)
|
||||
// - page (可选): 页码
|
||||
// 返回数据格式: ApiResponse<{rooms: Room[], users: User[]}>
|
||||
// 跳转到搜索页面并传递搜索关键词
|
||||
SearchActivity.start(MainActivity.this, searchText);
|
||||
}
|
||||
|
|
@ -642,10 +671,27 @@ public class MainActivity extends AppCompatActivity {
|
|||
String avatarUri = getSharedPreferences("profile_prefs", MODE_PRIVATE)
|
||||
.getString("profile_avatar_uri", null);
|
||||
if (!TextUtils.isEmpty(avatarUri)) {
|
||||
Uri uri = Uri.parse(avatarUri);
|
||||
// 如果是 file:// 协议,尝试转换为 FileProvider URI
|
||||
if ("file".equals(uri.getScheme())) {
|
||||
try {
|
||||
java.io.File file = new java.io.File(uri.getPath());
|
||||
if (file.exists()) {
|
||||
uri = FileProvider.getUriForFile(
|
||||
this,
|
||||
getPackageName() + ".fileprovider",
|
||||
file
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 如果转换失败,使用原始 URI
|
||||
}
|
||||
}
|
||||
Glide.with(this)
|
||||
.load(Uri.parse(avatarUri))
|
||||
.load(uri)
|
||||
.circleCrop()
|
||||
.error(R.drawable.ic_account_circle_24)
|
||||
.placeholder(R.drawable.ic_account_circle_24)
|
||||
.into(binding.avatarButton);
|
||||
return;
|
||||
}
|
||||
|
|
@ -801,7 +847,15 @@ public class MainActivity extends AppCompatActivity {
|
|||
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
|
||||
ApiClient.getService(getApplicationContext()).createRoom(new CreateRoomRequest(title, streamer))
|
||||
// TODO: 接入后端接口 - 创建直播间
|
||||
// 接口路径: POST /api/rooms
|
||||
// 请求参数: CreateRoomRequest
|
||||
// - title: 直播间标题
|
||||
// - streamerName: 主播名称
|
||||
// - type: 直播类型(如"live")
|
||||
// 返回数据格式: ApiResponse<Room>
|
||||
// Room对象应包含: id, title, streamerName, streamKey, streamUrls (包含rtmp, flv, hls地址)等字段
|
||||
ApiClient.getService(getApplicationContext()).createRoom(new CreateRoomRequest(title, streamerName, "live"))
|
||||
.enqueue(new Callback<ApiResponse<Room>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<Room>> call, Response<ApiResponse<Room>> response) {
|
||||
|
|
@ -933,6 +987,14 @@ public class MainActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void fetchRooms() {
|
||||
// TODO: 接入后端接口 - 获取房间列表
|
||||
// 接口路径: GET /api/rooms
|
||||
// 请求参数:
|
||||
// - category (可选): 分类筛选,如"游戏"、"才艺"等
|
||||
// - page (可选): 页码,用于分页加载
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<Room>>
|
||||
// Room对象应包含: id, title, streamerName, type, isLive, coverUrl, viewerCount, streamUrls等字段
|
||||
// 避免重复请求
|
||||
if (isFetching) return;
|
||||
|
||||
|
|
@ -1161,6 +1223,47 @@ public class MainActivity extends AppCompatActivity {
|
|||
.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用搜索筛选
|
||||
* 根据搜索文本筛选房间列表(仅按房间标题和主播名称,不按分类筛选)
|
||||
*/
|
||||
private void applySearchFilter(String searchQuery) {
|
||||
String query = searchQuery != null ? searchQuery.trim().toLowerCase() : "";
|
||||
|
||||
// 如果搜索文本为空,恢复分类筛选
|
||||
if (query.isEmpty()) {
|
||||
applyCategoryFilterWithAnimation(currentCategory);
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据当前顶部标签选择要筛选的数据源
|
||||
List<Room> sourceRooms;
|
||||
if ("关注".equals(currentTopTab)) {
|
||||
sourceRooms = followRooms;
|
||||
} else if ("附近".equals(currentTopTab)) {
|
||||
// 附近页面不显示房间,不需要搜索筛选
|
||||
return;
|
||||
} else {
|
||||
// 发现页面
|
||||
sourceRooms = discoverRooms.isEmpty() ? allRooms : discoverRooms;
|
||||
}
|
||||
|
||||
// 只根据搜索文本筛选(按房间标题和主播名称),不考虑分类
|
||||
List<Room> searchFiltered = new ArrayList<>();
|
||||
for (Room r : sourceRooms) {
|
||||
if (r == null) continue;
|
||||
String title = r.getTitle() != null ? r.getTitle().toLowerCase() : "";
|
||||
String streamer = r.getStreamerName() != null ? r.getStreamerName().toLowerCase() : "";
|
||||
if (title.contains(query) || streamer.contains(query)) {
|
||||
searchFiltered.add(r);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新列表
|
||||
adapter.submitList(searchFiltered);
|
||||
updateEmptyStateForList(searchFiltered);
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复分类标签的选中状态
|
||||
*/
|
||||
|
|
@ -1445,6 +1548,15 @@ public class MainActivity extends AppCompatActivity {
|
|||
* 构建关注页面的房间列表(已关注主播的直播)
|
||||
*/
|
||||
private List<Room> buildFollowRooms() {
|
||||
// TODO: 接入后端接口 - 获取关注主播的直播间列表
|
||||
// 接口路径: GET /api/following/rooms 或 GET /api/rooms?type=following
|
||||
// 请求参数:
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<Room>>
|
||||
// Room对象应包含: id, title, streamerName, streamerId, type, isLive, coverUrl, viewerCount等字段
|
||||
// 只返回当前用户已关注的主播正在直播的房间
|
||||
List<Room> list = new ArrayList<>();
|
||||
|
||||
// 从FollowingListActivity获取已关注的主播列表
|
||||
|
|
@ -1478,6 +1590,15 @@ public class MainActivity extends AppCompatActivity {
|
|||
* 构建发现页面的房间列表(推荐算法前端实现)
|
||||
*/
|
||||
private List<Room> buildDiscoverRooms() {
|
||||
// TODO: 接入后端接口 - 获取推荐直播间列表
|
||||
// 接口路径: GET /api/rooms/recommend 或 GET /api/rooms?type=recommend
|
||||
// 请求参数:
|
||||
// - userId: 当前用户ID(从token中获取,用于个性化推荐)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<Room>>
|
||||
// Room对象应包含: id, title, streamerName, type, isLive, coverUrl, viewerCount, recommendScore等字段
|
||||
// 后端应根据用户观看历史、点赞记录、关注关系等进行个性化推荐
|
||||
List<Room> list = new ArrayList<>();
|
||||
|
||||
// 推荐算法:基于观看历史、点赞等模拟数据
|
||||
|
|
@ -1544,6 +1665,17 @@ public class MainActivity extends AppCompatActivity {
|
|||
* 构建附近页面的用户列表(使用模拟位置数据)
|
||||
*/
|
||||
private List<NearbyUser> buildNearbyUsers() {
|
||||
// TODO: 接入后端接口 - 获取附近用户列表
|
||||
// 接口路径: GET /api/users/nearby
|
||||
// 请求参数:
|
||||
// - latitude: 当前用户纬度(必填)
|
||||
// - longitude: 当前用户经度(必填)
|
||||
// - radius (可选): 搜索半径(单位:米,默认5000)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<NearbyUser>>
|
||||
// NearbyUser对象应包含: id, name, avatarUrl, distance (距离,单位:米), isLive, location等字段
|
||||
// 需要先获取用户位置权限,然后调用此接口
|
||||
List<NearbyUser> list = new ArrayList<>();
|
||||
|
||||
// 模拟位置数据:生成不同距离的用户
|
||||
|
|
|
|||
|
|
@ -7,28 +7,38 @@ import android.graphics.Color;
|
|||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.TypedValue;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
|
||||
|
||||
import com.example.livestreaming.databinding.ActivityMessagesBinding;
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class MessagesActivity extends AppCompatActivity {
|
||||
|
||||
private ActivityMessagesBinding binding;
|
||||
|
||||
private final List<ConversationItem> conversations = new ArrayList<>();
|
||||
private final List<ConversationItem> allConversations = new ArrayList<>(); // 保存所有会话,用于搜索
|
||||
private final List<ConversationItem> conversations = new ArrayList<>(); // 当前显示的会话列表
|
||||
private ConversationsAdapter conversationsAdapter;
|
||||
|
||||
private int swipedPosition = RecyclerView.NO_POSITION;
|
||||
|
|
@ -59,6 +69,7 @@ public class MessagesActivity extends AppCompatActivity {
|
|||
setContentView(binding.getRoot());
|
||||
|
||||
setupConversationList();
|
||||
setupSearch();
|
||||
|
||||
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
|
||||
bottomNavigation.setSelectedItemId(R.id.nav_messages);
|
||||
|
|
@ -106,27 +117,43 @@ public class MessagesActivity extends AppCompatActivity {
|
|||
|
||||
conversationsAdapter = new ConversationsAdapter(item -> {
|
||||
if (item == null) return;
|
||||
// 启动会话页面,传递未读数量
|
||||
Intent intent = new Intent(this, ConversationActivity.class);
|
||||
intent.putExtra("extra_conversation_id", item.getId());
|
||||
intent.putExtra("extra_conversation_title", item.getTitle());
|
||||
intent.putExtra("extra_unread_count", item.getUnreadCount());
|
||||
startActivity(intent);
|
||||
|
||||
// 用户点击会话时,减少该会话的未读数量
|
||||
if (item.getUnreadCount() > 0) {
|
||||
// 更新该会话的未读数量为0(在实际应用中,这里应该更新数据源)
|
||||
// 然后更新总未读数量
|
||||
UnreadMessageManager.decrementUnreadCount(this, item.getUnreadCount());
|
||||
// 更新列表中的未读数量显示
|
||||
updateConversationUnreadCount(item.getId(), 0);
|
||||
try {
|
||||
// 启动会话页面,传递未读数量
|
||||
Intent intent = new Intent(this, ConversationActivity.class);
|
||||
intent.putExtra("extra_conversation_id", item.getId());
|
||||
intent.putExtra("extra_conversation_title", item.getTitle());
|
||||
intent.putExtra("extra_unread_count", item.getUnreadCount());
|
||||
startActivity(intent);
|
||||
|
||||
// 用户点击会话时,减少该会话的未读数量
|
||||
if (item.getUnreadCount() > 0) {
|
||||
// 更新该会话的未读数量为0(在实际应用中,这里应该更新数据源)
|
||||
// 然后更新总未读数量
|
||||
UnreadMessageManager.decrementUnreadCount(this, item.getUnreadCount());
|
||||
// 更新列表中的未读数量显示
|
||||
updateConversationUnreadCount(item.getId(), 0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(this, "打开会话失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
binding.conversationsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
binding.conversationsRecyclerView.setAdapter(conversationsAdapter);
|
||||
|
||||
// TODO: 接入后端接口 - 获取会话列表
|
||||
// 接口路径: GET /api/conversations
|
||||
// 请求参数:
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<ConversationItem>>
|
||||
// ConversationItem对象应包含: id, title, lastMessage, timeText, unreadCount, isMuted, avatarUrl等字段
|
||||
// 会话列表应按最后一条消息时间倒序排列
|
||||
allConversations.clear();
|
||||
allConversations.addAll(buildDemoConversations());
|
||||
conversations.clear();
|
||||
conversations.addAll(buildDemoConversations());
|
||||
conversations.addAll(allConversations);
|
||||
conversationsAdapter.submitList(new ArrayList<>(conversations));
|
||||
|
||||
// 检查是否需要显示空状态
|
||||
|
|
@ -134,6 +161,91 @@ public class MessagesActivity extends AppCompatActivity {
|
|||
|
||||
attachSwipeToDelete(binding.conversationsRecyclerView);
|
||||
}
|
||||
|
||||
private void setupSearch() {
|
||||
TextInputEditText searchInput = binding.searchInput;
|
||||
if (searchInput == null) return;
|
||||
|
||||
// 监听搜索输入
|
||||
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) {
|
||||
filterConversations(s.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
});
|
||||
|
||||
// 监听搜索按钮点击
|
||||
searchInput.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_SEARCH ||
|
||||
(event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN)) {
|
||||
// 隐藏键盘
|
||||
View view = getCurrentFocus();
|
||||
if (view != null) {
|
||||
android.view.inputmethod.InputMethodManager imm = (android.view.inputmethod.InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private void filterConversations(String query) {
|
||||
// TODO: 接入后端接口 - 搜索会话
|
||||
// 接口路径: GET /api/conversations/search
|
||||
// 请求参数:
|
||||
// - keyword: 搜索关键词
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// 返回数据格式: ApiResponse<List<ConversationItem>>
|
||||
// 搜索范围包括:会话标题、最后一条消息内容、对方用户名等
|
||||
conversations.clear();
|
||||
|
||||
if (query == null || query.trim().isEmpty()) {
|
||||
// 搜索为空,显示所有会话
|
||||
conversations.addAll(allConversations);
|
||||
} else {
|
||||
// 过滤会话:按标题和最后一条消息内容搜索
|
||||
String lowerQuery = query.toLowerCase(Locale.getDefault());
|
||||
for (ConversationItem item : allConversations) {
|
||||
if (item == null) continue;
|
||||
|
||||
// 搜索会话标题
|
||||
String title = item.getTitle() != null ? item.getTitle().toLowerCase(Locale.getDefault()) : "";
|
||||
// 搜索最后一条消息内容
|
||||
String lastMessage = item.getLastMessage() != null ? item.getLastMessage().toLowerCase(Locale.getDefault()) : "";
|
||||
|
||||
if (title.contains(lowerQuery) || lastMessage.contains(lowerQuery)) {
|
||||
conversations.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conversationsAdapter.submitList(new ArrayList<>(conversations));
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新空状态显示
|
||||
*/
|
||||
private void updateEmptyState() {
|
||||
if (binding == null) return;
|
||||
|
||||
boolean isEmpty = conversations == null || conversations.isEmpty();
|
||||
if (binding.emptyStateView != null) {
|
||||
binding.emptyStateView.setVisibility(isEmpty ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
if (binding.conversationsRecyclerView != null) {
|
||||
binding.conversationsRecyclerView.setVisibility(isEmpty ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void attachSwipeToDelete(RecyclerView recyclerView) {
|
||||
// 两个按钮的总宽度:删除按钮 + 标记已读按钮
|
||||
|
|
@ -276,48 +388,89 @@ public class MessagesActivity extends AppCompatActivity {
|
|||
|
||||
new ItemTouchHelper(callback).attachToRecyclerView(recyclerView);
|
||||
|
||||
recyclerView.setOnTouchListener((v, event) -> {
|
||||
if (swipedPosition == RecyclerView.NO_POSITION || (deleteButtonRect == null && markReadButtonRect == null)) return false;
|
||||
// 使用 OnItemTouchListener 处理滑动删除按钮的点击
|
||||
// 注意:只在有展开的项时才处理,避免干扰正常的点击事件
|
||||
recyclerView.addOnItemTouchListener(new OnItemTouchListener() {
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
|
||||
int action = e.getActionMasked();
|
||||
|
||||
// 如果有展开的项,且点击的不是展开的项,恢复展开的项
|
||||
if (swipedPosition != RecyclerView.NO_POSITION && action == MotionEvent.ACTION_DOWN) {
|
||||
View child = rv.findChildViewUnder(e.getX(), e.getY());
|
||||
if (child != null) {
|
||||
RecyclerView.ViewHolder vh = rv.getChildViewHolder(child);
|
||||
if (vh != null && vh.getBindingAdapterPosition() != swipedPosition) {
|
||||
// 点击的不是展开的项,恢复展开的项
|
||||
recoverSwipedItem();
|
||||
return false; // 不拦截,让点击事件正常处理
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 只有在有展开的项时才处理按钮点击
|
||||
if (swipedPosition == RecyclerView.NO_POSITION || (deleteButtonRect == null && markReadButtonRect == null)) {
|
||||
return false; // 不拦截,让事件正常传递
|
||||
}
|
||||
|
||||
int action = event.getActionMasked();
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
touchDownX = event.getX();
|
||||
touchDownY = event.getY();
|
||||
touchIsClick = true;
|
||||
return false;
|
||||
}
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
touchDownX = e.getX();
|
||||
touchDownY = e.getY();
|
||||
touchIsClick = true;
|
||||
// 不拦截 DOWN 事件,让 RecyclerView 正常处理
|
||||
return false;
|
||||
}
|
||||
|
||||
if (action == MotionEvent.ACTION_MOVE) {
|
||||
float dx = Math.abs(event.getX() - touchDownX);
|
||||
float dy = Math.abs(event.getY() - touchDownY);
|
||||
if (dx > touchSlop || dy > touchSlop) {
|
||||
touchIsClick = false;
|
||||
if (action == MotionEvent.ACTION_MOVE) {
|
||||
float dx = Math.abs(e.getX() - touchDownX);
|
||||
float dy = Math.abs(e.getY() - touchDownY);
|
||||
if (dx > touchSlop || dy > touchSlop) {
|
||||
touchIsClick = false;
|
||||
}
|
||||
// 不拦截 MOVE 事件
|
||||
return false;
|
||||
}
|
||||
|
||||
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
||||
if (!touchIsClick) {
|
||||
// 如果不是点击,恢复展开的项
|
||||
recoverSwipedItem();
|
||||
return false;
|
||||
}
|
||||
|
||||
int pos = swipedPosition;
|
||||
float x = e.getX();
|
||||
float y = e.getY();
|
||||
|
||||
// 检查点击的是哪个按钮
|
||||
boolean hitMarkRead = markReadButtonRect != null && markReadButtonRect.contains(x, y);
|
||||
boolean hitDelete = deleteButtonRect != null && deleteButtonRect.contains(x, y);
|
||||
|
||||
if (hitMarkRead || hitDelete) {
|
||||
// 点击了按钮,拦截事件并处理
|
||||
recoverSwipedItem();
|
||||
if (hitMarkRead) {
|
||||
markAsReadAt(pos);
|
||||
} else if (hitDelete) {
|
||||
deleteConversationAt(pos);
|
||||
}
|
||||
return true; // 拦截事件,防止触发 RecyclerView 的点击
|
||||
}
|
||||
// 点击的不是按钮区域,不拦截,让 RecyclerView 正常处理点击
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (action == MotionEvent.ACTION_UP) {
|
||||
if (!touchIsClick) return false;
|
||||
|
||||
int pos = swipedPosition;
|
||||
float x = event.getX();
|
||||
float y = event.getY();
|
||||
|
||||
// 检查点击的是哪个按钮
|
||||
boolean hitMarkRead = markReadButtonRect != null && markReadButtonRect.contains(x, y);
|
||||
boolean hitDelete = deleteButtonRect != null && deleteButtonRect.contains(x, y);
|
||||
|
||||
recoverSwipedItem();
|
||||
|
||||
if (hitMarkRead) {
|
||||
markAsReadAt(pos);
|
||||
return true;
|
||||
} else if (hitDelete) {
|
||||
deleteConversationAt(pos);
|
||||
return true;
|
||||
}
|
||||
@Override
|
||||
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
|
||||
// 不需要处理
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
|
||||
// 不需要处理
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -335,6 +488,13 @@ public class MessagesActivity extends AppCompatActivity {
|
|||
* 标记会话为已读
|
||||
*/
|
||||
private void markAsReadAt(int position) {
|
||||
// TODO: 接入后端接口 - 标记会话为已读
|
||||
// 接口路径: POST /api/conversations/{conversationId}/read
|
||||
// 请求参数:
|
||||
// - conversationId: 会话ID(路径参数)
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// 返回数据格式: ApiResponse<{success: boolean}>
|
||||
// 标记成功后,更新本地未读数量为0,并更新总未读数量
|
||||
if (position < 0 || position >= conversations.size()) return;
|
||||
|
||||
ConversationItem item = conversations.get(position);
|
||||
|
|
@ -355,14 +515,26 @@ public class MessagesActivity extends AppCompatActivity {
|
|||
* 删除会话
|
||||
*/
|
||||
private void deleteConversationAt(int position) {
|
||||
// TODO: 接入后端接口 - 删除会话
|
||||
// 接口路径: DELETE /api/conversations/{conversationId}
|
||||
// 请求参数:
|
||||
// - conversationId: 会话ID(路径参数)
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// 返回数据格式: ApiResponse<{success: boolean}>
|
||||
// 删除成功后,从本地列表移除,并更新总未读数量
|
||||
if (position < 0 || position >= conversations.size()) return;
|
||||
|
||||
// 获取要删除的会话的未读数量
|
||||
ConversationItem itemToDelete = conversations.get(position);
|
||||
int unreadCountToRemove = itemToDelete != null ? itemToDelete.getUnreadCount() : 0;
|
||||
String itemId = itemToDelete != null ? itemToDelete.getId() : null;
|
||||
|
||||
// 删除会话
|
||||
// 从当前显示列表和全部列表中删除
|
||||
conversations.remove(position);
|
||||
if (itemId != null) {
|
||||
allConversations.removeIf(item -> item != null && item.getId().equals(itemId));
|
||||
}
|
||||
|
||||
if (conversationsAdapter != null) {
|
||||
conversationsAdapter.submitList(new ArrayList<>(conversations));
|
||||
}
|
||||
|
|
@ -375,6 +547,8 @@ public class MessagesActivity extends AppCompatActivity {
|
|||
UnreadMessageManager.updateBadge(binding.bottomNavInclude.bottomNavigation);
|
||||
}
|
||||
}
|
||||
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
private float dp(float value) {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,15 @@ public class MyFriendsActivity extends AppCompatActivity {
|
|||
binding.friendsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
binding.friendsRecyclerView.setAdapter(adapter);
|
||||
|
||||
// TODO: 接入后端接口 - 获取好友列表
|
||||
// 接口路径: GET /api/friends
|
||||
// 请求参数:
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<FriendItem>>
|
||||
// FriendItem对象应包含: id, name, avatarUrl, subtitle, isOnline, lastOnlineTime等字段
|
||||
// 列表应按最后在线时间倒序或添加时间倒序排列
|
||||
all.clear();
|
||||
all.addAll(buildDemoFriends());
|
||||
adapter.submitList(new ArrayList<>(all));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 通知数据模型
|
||||
*/
|
||||
public class NotificationItem {
|
||||
|
||||
public enum Type {
|
||||
SYSTEM, // 系统通知
|
||||
INTERACTION, // 互动通知(点赞、评论等)
|
||||
FOLLOW, // 关注通知
|
||||
MESSAGE, // 私信通知
|
||||
LIVE // 直播通知(开播提醒等)
|
||||
}
|
||||
|
||||
private String id;
|
||||
private Type type;
|
||||
private String title;
|
||||
private String content;
|
||||
private String avatarUrl;
|
||||
private Date timestamp;
|
||||
private boolean isRead;
|
||||
private String actionUrl; // 点击通知后的跳转链接
|
||||
|
||||
public NotificationItem() {
|
||||
}
|
||||
|
||||
public NotificationItem(String id, Type type, String title, String content, Date timestamp) {
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.timestamp = timestamp;
|
||||
this.isRead = false;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(Type type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getAvatarUrl() {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
public void setAvatarUrl(String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
public Date getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(Date timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public boolean isRead() {
|
||||
return isRead;
|
||||
}
|
||||
|
||||
public void setRead(boolean read) {
|
||||
isRead = read;
|
||||
}
|
||||
|
||||
public String getActionUrl() {
|
||||
return actionUrl;
|
||||
}
|
||||
|
||||
public void setActionUrl(String actionUrl) {
|
||||
this.actionUrl = actionUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
NotificationItem that = (NotificationItem) o;
|
||||
return id != null && id.equals(that.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return id != null ? id.hashCode() : 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.example.livestreaming.databinding.ActivityNotificationsBinding;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
public class NotificationsActivity extends AppCompatActivity {
|
||||
|
||||
private ActivityNotificationsBinding binding;
|
||||
private NotificationsAdapter adapter;
|
||||
private final List<NotificationItem> allNotifications = new ArrayList<>();
|
||||
private NotificationItem.Type currentFilter = null; // null表示显示全部
|
||||
|
||||
public static void start(Context context) {
|
||||
Intent intent = new Intent(context, NotificationsActivity.class);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = ActivityNotificationsBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
setupUI();
|
||||
loadNotifications();
|
||||
}
|
||||
|
||||
private void setupUI() {
|
||||
binding.backButton.setOnClickListener(v -> finish());
|
||||
binding.titleText.setText("通知");
|
||||
|
||||
// 设置适配器
|
||||
adapter = new NotificationsAdapter(item -> {
|
||||
// TODO: 接入后端接口 - 标记通知为已读
|
||||
// 接口路径: POST /api/notifications/{notificationId}/read
|
||||
// 请求参数:
|
||||
// - notificationId: 通知ID(路径参数)
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// 返回数据格式: ApiResponse<{success: boolean}>
|
||||
// 标记成功后,更新本地通知的isRead状态
|
||||
if (item == null) return;
|
||||
// 标记为已读
|
||||
item.setRead(true);
|
||||
adapter.notifyItemChanged(allNotifications.indexOf(item));
|
||||
|
||||
// 处理点击事件(可以根据actionUrl跳转)
|
||||
String actionUrl = item.getActionUrl();
|
||||
if (actionUrl != null && !actionUrl.isEmpty()) {
|
||||
// 这里可以根据actionUrl跳转到相应页面
|
||||
// 例如:跳转到直播间、个人主页等
|
||||
}
|
||||
});
|
||||
|
||||
binding.notificationsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
binding.notificationsRecyclerView.setAdapter(adapter);
|
||||
|
||||
// 设置分类标签
|
||||
binding.categoryTabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {
|
||||
if (tab == null) return;
|
||||
int position = tab.getPosition();
|
||||
switch (position) {
|
||||
case 0: // 全部
|
||||
currentFilter = null;
|
||||
break;
|
||||
case 1: // 系统
|
||||
currentFilter = NotificationItem.Type.SYSTEM;
|
||||
break;
|
||||
case 2: // 互动
|
||||
currentFilter = NotificationItem.Type.INTERACTION;
|
||||
break;
|
||||
case 3: // 关注
|
||||
currentFilter = NotificationItem.Type.FOLLOW;
|
||||
break;
|
||||
case 4: // 私信
|
||||
currentFilter = NotificationItem.Type.MESSAGE;
|
||||
break;
|
||||
case 5: // 直播
|
||||
currentFilter = NotificationItem.Type.LIVE;
|
||||
break;
|
||||
}
|
||||
filterNotifications();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadNotifications() {
|
||||
// TODO: 接入后端接口 - 获取通知列表
|
||||
// 接口路径: GET /api/notifications
|
||||
// 请求参数:
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// - type (可选): 通知类型(system/interaction/follow/message/live),null表示全部
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<NotificationItem>>
|
||||
// NotificationItem对象应包含: id, type, title, content, timestamp, isRead, actionUrl等字段
|
||||
// 列表应按时间倒序排列(最新的在前面)
|
||||
allNotifications.clear();
|
||||
|
||||
// 生成演示通知数据
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
Random random = new Random();
|
||||
|
||||
// 系统通知
|
||||
for (int i = 0; i < 3; i++) {
|
||||
calendar.add(Calendar.HOUR, -i * 2);
|
||||
NotificationItem item = new NotificationItem(
|
||||
"sys_" + i,
|
||||
NotificationItem.Type.SYSTEM,
|
||||
"系统通知",
|
||||
"欢迎使用直播应用!这里有最新的功能和活动。",
|
||||
calendar.getTime()
|
||||
);
|
||||
item.setRead(i > 0);
|
||||
allNotifications.add(item);
|
||||
}
|
||||
|
||||
// 互动通知
|
||||
String[] interactionTitles = {"有人点赞了你的作品", "有人评论了你的动态", "有人转发了你的内容"};
|
||||
for (int i = 0; i < 5; i++) {
|
||||
calendar.add(Calendar.MINUTE, -i * 30);
|
||||
NotificationItem item = new NotificationItem(
|
||||
"interaction_" + i,
|
||||
NotificationItem.Type.INTERACTION,
|
||||
interactionTitles[i % interactionTitles.length],
|
||||
"用户" + (1000 + i) + "给你点了个赞",
|
||||
calendar.getTime()
|
||||
);
|
||||
item.setRead(i > 2);
|
||||
allNotifications.add(item);
|
||||
}
|
||||
|
||||
// 关注通知
|
||||
for (int i = 0; i < 4; i++) {
|
||||
calendar.add(Calendar.HOUR, -i);
|
||||
NotificationItem item = new NotificationItem(
|
||||
"follow_" + i,
|
||||
NotificationItem.Type.FOLLOW,
|
||||
"新粉丝",
|
||||
"用户" + (2000 + i) + "关注了你",
|
||||
calendar.getTime()
|
||||
);
|
||||
item.setRead(i > 1);
|
||||
allNotifications.add(item);
|
||||
}
|
||||
|
||||
// 私信通知
|
||||
for (int i = 0; i < 6; i++) {
|
||||
calendar.add(Calendar.MINUTE, -i * 15);
|
||||
NotificationItem item = new NotificationItem(
|
||||
"message_" + i,
|
||||
NotificationItem.Type.MESSAGE,
|
||||
"新私信",
|
||||
"用户" + (3000 + i) + "给你发了一条消息",
|
||||
calendar.getTime()
|
||||
);
|
||||
item.setRead(i > 3);
|
||||
allNotifications.add(item);
|
||||
}
|
||||
|
||||
// 直播通知
|
||||
String[] liveTitles = {"开播提醒", "直播推荐", "关注的主播开播了"};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
calendar.add(Calendar.HOUR, -i * 4);
|
||||
NotificationItem item = new NotificationItem(
|
||||
"live_" + i,
|
||||
NotificationItem.Type.LIVE,
|
||||
liveTitles[i % liveTitles.length],
|
||||
"你关注的主播" + (4000 + i) + "正在直播",
|
||||
calendar.getTime()
|
||||
);
|
||||
item.setRead(i > 0);
|
||||
allNotifications.add(item);
|
||||
}
|
||||
|
||||
// 按时间倒序排序
|
||||
allNotifications.sort((a, b) -> {
|
||||
Date dateA = a.getTimestamp();
|
||||
Date dateB = b.getTimestamp();
|
||||
if (dateA == null && dateB == null) return 0;
|
||||
if (dateA == null) return 1;
|
||||
if (dateB == null) return -1;
|
||||
return dateB.compareTo(dateA);
|
||||
});
|
||||
|
||||
filterNotifications();
|
||||
}
|
||||
|
||||
private void filterNotifications() {
|
||||
List<NotificationItem> filtered = new ArrayList<>();
|
||||
|
||||
if (currentFilter == null) {
|
||||
filtered.addAll(allNotifications);
|
||||
} else {
|
||||
for (NotificationItem item : allNotifications) {
|
||||
if (item.getType() == currentFilter) {
|
||||
filtered.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adapter.submitList(filtered);
|
||||
|
||||
// 显示/隐藏空状态
|
||||
if (filtered.isEmpty()) {
|
||||
binding.emptyStateView.setVisibility(View.VISIBLE);
|
||||
binding.emptyStateView.setIcon(R.drawable.ic_notifications_24);
|
||||
binding.emptyStateView.setTitle("暂无通知");
|
||||
String message = currentFilter == null ? "还没有通知" : "还没有" + getFilterName() + "通知";
|
||||
binding.emptyStateView.setMessage(message);
|
||||
binding.emptyStateView.hideActionButton();
|
||||
} else {
|
||||
binding.emptyStateView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private String getFilterName() {
|
||||
if (currentFilter == null) return "";
|
||||
switch (currentFilter) {
|
||||
case SYSTEM:
|
||||
return "系统";
|
||||
case INTERACTION:
|
||||
return "互动";
|
||||
case FOLLOW:
|
||||
return "关注";
|
||||
case MESSAGE:
|
||||
return "私信";
|
||||
case LIVE:
|
||||
return "直播";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.text.TextUtils;
|
||||
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.bumptech.glide.Glide;
|
||||
import com.example.livestreaming.databinding.ItemNotificationBinding;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
public class NotificationsAdapter extends ListAdapter<NotificationItem, NotificationsAdapter.VH> {
|
||||
|
||||
public interface OnNotificationClickListener {
|
||||
void onNotificationClick(NotificationItem item);
|
||||
}
|
||||
|
||||
private final OnNotificationClickListener onNotificationClickListener;
|
||||
|
||||
public NotificationsAdapter(OnNotificationClickListener onNotificationClickListener) {
|
||||
super(DIFF);
|
||||
this.onNotificationClickListener = onNotificationClickListener;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
ItemNotificationBinding binding = ItemNotificationBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new VH(binding, onNotificationClickListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull VH holder, int position) {
|
||||
holder.bind(getItem(position));
|
||||
}
|
||||
|
||||
static class VH extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ItemNotificationBinding binding;
|
||||
private final OnNotificationClickListener onNotificationClickListener;
|
||||
private final SimpleDateFormat timeFormat = new SimpleDateFormat("MM-dd HH:mm", Locale.getDefault());
|
||||
private final SimpleDateFormat todayFormat = new SimpleDateFormat("HH:mm", Locale.getDefault());
|
||||
|
||||
VH(ItemNotificationBinding binding, OnNotificationClickListener onNotificationClickListener) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
this.onNotificationClickListener = onNotificationClickListener;
|
||||
}
|
||||
|
||||
void bind(NotificationItem item) {
|
||||
if (item == null) return;
|
||||
|
||||
// 设置标题和内容
|
||||
binding.title.setText(item.getTitle() != null ? item.getTitle() : "");
|
||||
binding.content.setText(item.getContent() != null ? item.getContent() : "");
|
||||
|
||||
// 设置时间
|
||||
Date timestamp = item.getTimestamp();
|
||||
if (timestamp != null) {
|
||||
String timeText = formatTime(timestamp);
|
||||
binding.timeText.setText(timeText);
|
||||
} else {
|
||||
binding.timeText.setText("");
|
||||
}
|
||||
|
||||
// 设置未读标记
|
||||
if (!item.isRead()) {
|
||||
binding.unreadDot.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.unreadDot.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// 加载头像
|
||||
loadAvatar(item);
|
||||
|
||||
// 设置点击事件
|
||||
binding.getRoot().setOnClickListener(v -> {
|
||||
if (onNotificationClickListener != null) {
|
||||
onNotificationClickListener.onNotificationClick(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String formatTime(Date date) {
|
||||
if (date == null) return "";
|
||||
|
||||
Date now = new Date();
|
||||
long diff = now.getTime() - date.getTime();
|
||||
|
||||
// 如果是今天,只显示时间
|
||||
if (diff < 24 * 60 * 60 * 1000 && isToday(date, now)) {
|
||||
return todayFormat.format(date);
|
||||
}
|
||||
|
||||
// 否则显示日期和时间
|
||||
return timeFormat.format(date);
|
||||
}
|
||||
|
||||
private boolean isToday(Date date, Date now) {
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
|
||||
return dateFormat.format(date).equals(dateFormat.format(now));
|
||||
}
|
||||
|
||||
private void loadAvatar(NotificationItem item) {
|
||||
if (binding.avatar == null) return;
|
||||
|
||||
String avatarUrl = item.getAvatarUrl();
|
||||
if (!TextUtils.isEmpty(avatarUrl)) {
|
||||
Glide.with(binding.avatar)
|
||||
.load(avatarUrl)
|
||||
.circleCrop()
|
||||
.placeholder(R.drawable.ic_account_circle_24)
|
||||
.error(R.drawable.ic_account_circle_24)
|
||||
.into(binding.avatar);
|
||||
} else {
|
||||
// 根据通知类型设置默认图标
|
||||
int iconRes = getIconForType(item.getType());
|
||||
Glide.with(binding.avatar)
|
||||
.load(iconRes)
|
||||
.circleCrop()
|
||||
.into(binding.avatar);
|
||||
}
|
||||
}
|
||||
|
||||
private int getIconForType(NotificationItem.Type type) {
|
||||
if (type == null) return R.drawable.ic_notifications_24;
|
||||
|
||||
switch (type) {
|
||||
case SYSTEM:
|
||||
return R.drawable.ic_notifications_24;
|
||||
case INTERACTION:
|
||||
return R.drawable.ic_chat_24;
|
||||
case FOLLOW:
|
||||
return R.drawable.ic_people_24;
|
||||
case MESSAGE:
|
||||
return R.drawable.ic_chat_24;
|
||||
case LIVE:
|
||||
return R.drawable.ic_voice_24;
|
||||
default:
|
||||
return R.drawable.ic_notifications_24;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final DiffUtil.ItemCallback<NotificationItem> DIFF = new DiffUtil.ItemCallback<NotificationItem>() {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull NotificationItem oldItem, @NonNull NotificationItem newItem) {
|
||||
String o = oldItem.getId();
|
||||
String n = newItem.getId();
|
||||
return o != null && o.equals(n);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull NotificationItem oldItem, @NonNull NotificationItem newItem) {
|
||||
return oldItem.equals(newItem) && oldItem.isRead() == newItem.isRead();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -32,6 +32,10 @@ public class OnlineDatingActivity extends AppCompatActivity {
|
|||
|
||||
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
|
||||
bottomNavigation.setSelectedItemId(R.id.nav_friends);
|
||||
|
||||
// 更新未读消息徽章
|
||||
UnreadMessageManager.updateBadge(bottomNavigation);
|
||||
|
||||
bottomNavigation.setOnItemSelectedListener(item -> {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.nav_home) {
|
||||
|
|
@ -57,5 +61,16 @@ public class OnlineDatingActivity extends AppCompatActivity {
|
|||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (binding != null) {
|
||||
BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
|
||||
bottomNav.setSelectedItemId(R.id.nav_friends);
|
||||
// 更新未读消息徽章
|
||||
UnreadMessageManager.updateBadge(bottomNav);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ public class PeaceEliteActivity extends AppCompatActivity {
|
|||
|
||||
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
|
||||
bottomNavigation.setSelectedItemId(R.id.nav_friends);
|
||||
|
||||
// 更新未读消息徽章
|
||||
UnreadMessageManager.updateBadge(bottomNavigation);
|
||||
|
||||
bottomNavigation.setOnItemSelectedListener(item -> {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.nav_home) {
|
||||
|
|
@ -57,5 +61,16 @@ public class PeaceEliteActivity extends AppCompatActivity {
|
|||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (binding != null) {
|
||||
BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
|
||||
bottomNav.setSelectedItemId(R.id.nav_friends);
|
||||
// 更新未读消息徽章
|
||||
UnreadMessageManager.updateBadge(bottomNav);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ import android.widget.Toast;
|
|||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
|
|
@ -122,6 +122,14 @@ public class ProfileActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void loadProfileFromPrefs() {
|
||||
// TODO: 接入后端接口 - 获取用户资料
|
||||
// 接口路径: GET /api/users/{userId}/profile
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(路径参数,当前用户从token中获取)
|
||||
// 返回数据格式: ApiResponse<UserProfile>
|
||||
// UserProfile对象应包含: id, name, avatarUrl, bio, level, badge, birthday, gender, location,
|
||||
// followingCount, fansCount, likesCount等字段
|
||||
// 首次加载时从接口获取,后续可从本地缓存读取
|
||||
String n = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_NAME, null);
|
||||
if (!TextUtils.isEmpty(n)) binding.name.setText(n);
|
||||
|
||||
|
|
@ -136,10 +144,27 @@ public class ProfileActivity extends AppCompatActivity {
|
|||
|
||||
String avatarUri = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_AVATAR_URI, null);
|
||||
if (!TextUtils.isEmpty(avatarUri)) {
|
||||
Uri uri = Uri.parse(avatarUri);
|
||||
// 如果是 file:// 协议,尝试转换为 FileProvider URI
|
||||
if ("file".equals(uri.getScheme())) {
|
||||
try {
|
||||
java.io.File file = new java.io.File(uri.getPath());
|
||||
if (file.exists()) {
|
||||
uri = FileProvider.getUriForFile(
|
||||
this,
|
||||
getPackageName() + ".fileprovider",
|
||||
file
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 如果转换失败,使用原始 URI
|
||||
}
|
||||
}
|
||||
Glide.with(this)
|
||||
.load(Uri.parse(avatarUri))
|
||||
.load(uri)
|
||||
.circleCrop()
|
||||
.error(R.drawable.ic_account_circle_24)
|
||||
.placeholder(R.drawable.ic_account_circle_24)
|
||||
.into(binding.avatar);
|
||||
} else {
|
||||
int avatarRes = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getInt(KEY_AVATAR_RES, 0);
|
||||
|
|
@ -158,6 +183,18 @@ public class ProfileActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void setupEditableAreas() {
|
||||
// TODO: 接入后端接口 - 更新用户资料
|
||||
// 接口路径: PUT /api/users/{userId}/profile
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(路径参数,从token中获取)
|
||||
// - name (可选): 昵称
|
||||
// - bio (可选): 个人签名
|
||||
// - avatarUrl (可选): 头像URL
|
||||
// - birthday (可选): 生日
|
||||
// - gender (可选): 性别
|
||||
// - location (可选): 所在地
|
||||
// 返回数据格式: ApiResponse<UserProfile>
|
||||
// 更新成功后,同步更新本地缓存和界面显示
|
||||
binding.name.setOnClickListener(v -> showEditDialog("编辑昵称", binding.name.getText() != null ? binding.name.getText().toString() : "", text -> {
|
||||
binding.name.setText(text);
|
||||
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putString(KEY_NAME, text).apply();
|
||||
|
|
@ -224,6 +261,12 @@ public class ProfileActivity extends AppCompatActivity {
|
|||
}
|
||||
});
|
||||
|
||||
// TODO: 接入后端接口 - 获取关注/粉丝/获赞数量
|
||||
// 接口路径: GET /api/users/{userId}/stats
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(路径参数)
|
||||
// 返回数据格式: ApiResponse<{followingCount: number, fansCount: number, likesCount: number}>
|
||||
// 在ProfileActivity加载时调用,更新关注、粉丝、获赞数量显示
|
||||
binding.following.setOnClickListener(v -> FollowingListActivity.start(this));
|
||||
binding.followers.setOnClickListener(v -> FansListActivity.start(this));
|
||||
binding.likes.setOnClickListener(v -> LikesListActivity.start(this));
|
||||
|
|
@ -236,20 +279,6 @@ 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, "加好友"));
|
||||
}
|
||||
|
|
@ -474,147 +503,6 @@ public class ProfileActivity extends AppCompatActivity {
|
|||
}
|
||||
}
|
||||
|
||||
private void loadAndDisplayTags() {
|
||||
String location = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_LOCATION, "");
|
||||
String gender = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_GENDER, "");
|
||||
String birthday = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_BIRTHDAY, "");
|
||||
|
||||
// 设置所在地标签 - 支持"省份-城市"格式
|
||||
if (!TextUtils.isEmpty(location)) {
|
||||
// 将"省份-城市"格式转换为"省份·城市"显示
|
||||
String displayLocation = location.replace("-", "·");
|
||||
binding.tagLocation.setText("IP:" + displayLocation);
|
||||
binding.tagLocation.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.tagLocation.setText("IP:广西");
|
||||
binding.tagLocation.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
// 设置性别标签
|
||||
if (!TextUtils.isEmpty(gender)) {
|
||||
if (gender.contains("男")) {
|
||||
binding.tagGender.setText("男");
|
||||
} else if (gender.contains("女")) {
|
||||
binding.tagGender.setText("女");
|
||||
} else {
|
||||
binding.tagGender.setText("H");
|
||||
}
|
||||
binding.tagGender.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.tagGender.setText("H");
|
||||
binding.tagGender.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
// 计算并设置年龄标签
|
||||
if (!TextUtils.isEmpty(birthday)) {
|
||||
int age = calculateAge(birthday);
|
||||
if (age > 0) {
|
||||
binding.tagAge.setText(age + "岁");
|
||||
binding.tagAge.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.tagAge.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
binding.tagAge.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// 计算并设置星座标签
|
||||
if (!TextUtils.isEmpty(birthday)) {
|
||||
String constellation = calculateConstellation(birthday);
|
||||
if (!TextUtils.isEmpty(constellation)) {
|
||||
binding.tagConstellation.setText(constellation);
|
||||
binding.tagConstellation.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.tagConstellation.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
binding.tagConstellation.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadProfileInfo() {
|
||||
String name = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_NAME, "爱你");
|
||||
String location = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_LOCATION, "广西");
|
||||
String bio = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_BIO, null);
|
||||
|
||||
if (binding.profileInfoLine1 != null) {
|
||||
binding.profileInfoLine1.setText("昵称:" + name);
|
||||
}
|
||||
|
||||
if (binding.profileInfoLine2 != null) {
|
||||
String locationText = !TextUtils.isEmpty(location) ? location : "广西";
|
||||
binding.profileInfoLine2.setText("地区:" + locationText);
|
||||
}
|
||||
|
||||
if (binding.profileInfoLine3 != null) {
|
||||
String bioText = (!TextUtils.isEmpty(bio) && !BIO_HINT_TEXT.equals(bio)) ? bio : "填写个人签名更容易获得关注";
|
||||
binding.profileInfoLine3.setText("签名:" + bioText);
|
||||
}
|
||||
}
|
||||
|
||||
private int calculateAge(String birthdayStr) {
|
||||
try {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
|
||||
Date birthDate = sdf.parse(birthdayStr);
|
||||
if (birthDate == null) return 0;
|
||||
|
||||
Calendar birth = Calendar.getInstance();
|
||||
birth.setTime(birthDate);
|
||||
Calendar now = Calendar.getInstance();
|
||||
|
||||
int age = now.get(Calendar.YEAR) - birth.get(Calendar.YEAR);
|
||||
if (now.get(Calendar.DAY_OF_YEAR) < birth.get(Calendar.DAY_OF_YEAR)) {
|
||||
age--;
|
||||
}
|
||||
return age;
|
||||
} catch (ParseException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private String calculateConstellation(String birthdayStr) {
|
||||
try {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
|
||||
Date birthDate = sdf.parse(birthdayStr);
|
||||
if (birthDate == null) return "";
|
||||
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(birthDate);
|
||||
int month = cal.get(Calendar.MONTH) + 1; // Calendar.MONTH 从0开始
|
||||
int day = cal.get(Calendar.DAY_OF_MONTH);
|
||||
|
||||
// 星座计算
|
||||
if ((month == 3 && day >= 21) || (month == 4 && day <= 19)) {
|
||||
return "白羊座";
|
||||
} else if ((month == 4 && day >= 20) || (month == 5 && day <= 20)) {
|
||||
return "金牛座";
|
||||
} else if ((month == 5 && day >= 21) || (month == 6 && day <= 21)) {
|
||||
return "双子座";
|
||||
} else if ((month == 6 && day >= 22) || (month == 7 && day <= 22)) {
|
||||
return "巨蟹座";
|
||||
} else if ((month == 7 && day >= 23) || (month == 8 && day <= 22)) {
|
||||
return "狮子座";
|
||||
} else if ((month == 8 && day >= 23) || (month == 9 && day <= 22)) {
|
||||
return "处女座";
|
||||
} else if ((month == 9 && day >= 23) || (month == 10 && day <= 23)) {
|
||||
return "天秤座";
|
||||
} else if ((month == 10 && day >= 24) || (month == 11 && day <= 22)) {
|
||||
return "天蝎座";
|
||||
} else if ((month == 11 && day >= 23) || (month == 12 && day <= 21)) {
|
||||
return "射手座";
|
||||
} else if ((month == 12 && day >= 22) || (month == 1 && day <= 19)) {
|
||||
return "摩羯座";
|
||||
} else if ((month == 1 && day >= 20) || (month == 2 && day <= 18)) {
|
||||
return "水瓶座";
|
||||
} else if ((month == 2 && day >= 19) || (month == 3 && day <= 20)) {
|
||||
return "双鱼座";
|
||||
}
|
||||
return "";
|
||||
} catch (ParseException e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示分享个人主页对话框
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import com.example.livestreaming.net.ApiClient;
|
|||
import com.example.livestreaming.net.ApiResponse;
|
||||
import com.example.livestreaming.net.Room;
|
||||
import com.example.livestreaming.net.StreamConfig;
|
||||
import com.example.livestreaming.ShareUtils;
|
||||
|
||||
import tv.danmaku.ijk.media.player.IMediaPlayer;
|
||||
import tv.danmaku.ijk.media.player.IjkMediaPlayer;
|
||||
|
|
@ -130,11 +131,21 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
binding.fullscreenButton.setOnClickListener(v -> toggleFullscreen());
|
||||
|
||||
// 关注按钮
|
||||
// TODO: 接入后端接口 - 关注/取消关注主播
|
||||
// 接口路径: POST /api/follow 或 DELETE /api/follow
|
||||
// 请求参数:
|
||||
// - streamerId: 主播用户ID
|
||||
// - action: "follow" 或 "unfollow"
|
||||
// 返回数据格式: ApiResponse<{success: boolean, message: string}>
|
||||
// 关注成功后,更新按钮状态为"已关注",并禁用按钮
|
||||
binding.followButton.setOnClickListener(v -> {
|
||||
Toast.makeText(this, "已关注主播", Toast.LENGTH_SHORT).show();
|
||||
binding.followButton.setText("已关注");
|
||||
binding.followButton.setEnabled(false);
|
||||
});
|
||||
|
||||
// 分享按钮
|
||||
binding.shareButton.setOnClickListener(v -> shareRoom());
|
||||
}
|
||||
|
||||
private void setupChat() {
|
||||
|
|
@ -160,6 +171,15 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void sendMessage() {
|
||||
// TODO: 接入后端接口 - 发送直播间弹幕消息
|
||||
// 接口路径: POST /api/rooms/{roomId}/messages
|
||||
// 请求参数:
|
||||
// - roomId: 房间ID(路径参数)
|
||||
// - message: 消息内容
|
||||
// - userId: 发送者用户ID(从token中获取)
|
||||
// 返回数据格式: ApiResponse<ChatMessage>
|
||||
// ChatMessage对象应包含: messageId, userId, username, avatarUrl, message, timestamp等字段
|
||||
// 发送成功后,将消息添加到本地列表并显示
|
||||
String message = binding.chatInput.getText() != null ?
|
||||
binding.chatInput.getText().toString().trim() : "";
|
||||
|
||||
|
|
@ -167,6 +187,14 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
addChatMessage(new ChatMessage("我", message));
|
||||
binding.chatInput.setText("");
|
||||
|
||||
// TODO: 接入后端接口 - 接收直播间弹幕消息(WebSocket或轮询)
|
||||
// 方案1: WebSocket实时推送
|
||||
// - 连接: ws://api.example.com/rooms/{roomId}/chat
|
||||
// - 接收消息格式: {type: "message", data: ChatMessage}
|
||||
// 方案2: 轮询获取新消息
|
||||
// - 接口路径: GET /api/rooms/{roomId}/messages?lastMessageId={lastId}
|
||||
// - 返回数据格式: ApiResponse<List<ChatMessage>>
|
||||
// - 每3-5秒轮询一次,获取lastMessageId之后的新消息
|
||||
// 模拟其他用户回复
|
||||
handler.postDelayed(() -> {
|
||||
if (random.nextFloat() < 0.3f) { // 30%概率有人回复
|
||||
|
|
@ -272,10 +300,18 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void startChatSimulation() {
|
||||
// TODO: 接入后端接口 - 初始化时获取历史弹幕消息
|
||||
// 接口路径: GET /api/rooms/{roomId}/messages
|
||||
// 请求参数:
|
||||
// - roomId: 房间ID(路径参数)
|
||||
// - limit (可选): 获取最近N条消息,默认50
|
||||
// 返回数据格式: ApiResponse<List<ChatMessage>>
|
||||
// 进入直播间时,先获取最近50条历史消息显示在聊天列表中
|
||||
stopChatSimulation();
|
||||
chatSimulationRunnable = () -> {
|
||||
if (isFinishing() || isDestroyed()) return;
|
||||
|
||||
// TODO: 这里应该改为从WebSocket或轮询接口获取新消息,而不是模拟生成
|
||||
// 随机生成弹幕,降低概率
|
||||
if (random.nextFloat() < 0.25f) { // 25%概率生成弹幕
|
||||
String user = simulatedUsers[random.nextInt(simulatedUsers.length)];
|
||||
|
|
@ -302,6 +338,12 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
private boolean isFirstLoad = true;
|
||||
|
||||
private void fetchRoom() {
|
||||
// TODO: 接入后端接口 - 获取房间详情
|
||||
// 接口路径: GET /api/rooms/{roomId}
|
||||
// 请求参数: roomId (路径参数)
|
||||
// 返回数据格式: ApiResponse<Room>
|
||||
// Room对象应包含: id, title, streamerName, streamerId, type, isLive, coverUrl, viewerCount,
|
||||
// streamUrls (包含flv, hls, rtmp地址), description, startTime等字段
|
||||
if (TextUtils.isEmpty(roomId)) {
|
||||
Toast.makeText(this, "房间不存在", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
|
|
@ -346,6 +388,18 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
});
|
||||
}
|
||||
|
||||
private void shareRoom() {
|
||||
if (room == null || roomId == null) {
|
||||
Toast.makeText(this, "房间信息不可用", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
String shareLink = ShareUtils.generateRoomShareLink(roomId);
|
||||
String title = room.getTitle() != null ? room.getTitle() : "直播间";
|
||||
String text = "来看看这个精彩的直播:" + title;
|
||||
ShareUtils.shareLink(this, shareLink, title, text);
|
||||
}
|
||||
|
||||
private void bindRoom(Room r) {
|
||||
String title = r.getTitle() != null ? r.getTitle() : "直播间";
|
||||
String streamer = r.getStreamerName() != null ? r.getStreamerName() : "主播";
|
||||
|
|
@ -362,6 +416,11 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
binding.liveTag.setVisibility(View.VISIBLE);
|
||||
binding.offlineLayout.setVisibility(View.GONE);
|
||||
|
||||
// TODO: 接入后端接口 - 获取实时观看人数
|
||||
// 接口路径: GET /api/rooms/{roomId}/viewers/count
|
||||
// 请求参数: roomId (路径参数)
|
||||
// 返回数据格式: ApiResponse<{viewerCount: number}>
|
||||
// 建议使用WebSocket实时推送观看人数变化,或每10-15秒轮询一次
|
||||
// 设置观看人数(模拟)
|
||||
int viewerCount = r.getViewerCount() > 0 ? r.getViewerCount() :
|
||||
100 + random.nextInt(500);
|
||||
|
|
|
|||
|
|
@ -73,6 +73,16 @@ public class SearchActivity extends AppCompatActivity {
|
|||
binding.resultsRecyclerView.setLayoutManager(glm);
|
||||
binding.resultsRecyclerView.setAdapter(adapter);
|
||||
|
||||
// TODO: 接入后端接口 - 搜索房间/主播
|
||||
// 接口路径: GET /api/search
|
||||
// 请求参数:
|
||||
// - keyword: 搜索关键词(必填)
|
||||
// - type (可选): 搜索类型(room/user/all),默认all
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<{rooms: Room[], users: User[]}>
|
||||
// Room对象应包含: id, title, streamerName, type, isLive, coverUrl等字段
|
||||
// User对象应包含: id, name, avatarUrl, bio, isLive等字段
|
||||
all.clear();
|
||||
all.addAll(buildDemoRooms(24));
|
||||
adapter.submitList(new ArrayList<>(all));
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
|
|
@ -54,7 +53,7 @@ public class SettingsPageActivity extends AppCompatActivity {
|
|||
page = getIntent() != null ? getIntent().getStringExtra(EXTRA_PAGE) : null;
|
||||
if (page == null) page = "";
|
||||
|
||||
String title = resolveTitle(currentPage);
|
||||
String title = resolveTitle(page);
|
||||
binding.titleText.setText(title);
|
||||
|
||||
adapter = new MoreAdapter(item -> {
|
||||
|
|
@ -62,6 +61,7 @@ public class SettingsPageActivity extends AppCompatActivity {
|
|||
if (item.getType() != MoreItem.Type.ROW) return;
|
||||
String t = item.getTitle() != null ? item.getTitle() : "";
|
||||
|
||||
// 处理服务器设置相关项目
|
||||
if ("服务器设置".equals(t)) {
|
||||
SettingsPageActivity.start(this, PAGE_SERVER);
|
||||
return;
|
||||
|
|
@ -97,7 +97,14 @@ public class SettingsPageActivity extends AppCompatActivity {
|
|||
return;
|
||||
}
|
||||
|
||||
Toast.makeText(this, "点击:" + t, Toast.LENGTH_SHORT).show();
|
||||
// 处理其他页面的点击事件
|
||||
if (PAGE_SERVER.equals(page)) {
|
||||
// 服务器设置页面的其他项目已在上面处理
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用统一的点击处理方法
|
||||
handleItemClick(item);
|
||||
});
|
||||
|
||||
binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
|
|
@ -109,7 +116,7 @@ public class SettingsPageActivity extends AppCompatActivity {
|
|||
private void handleItemClick(MoreItem item) {
|
||||
String title = item.getTitle() != null ? item.getTitle() : "";
|
||||
|
||||
switch (currentPage) {
|
||||
switch (page) {
|
||||
case PAGE_ACCOUNT_SECURITY:
|
||||
handleAccountSecurityClick(title);
|
||||
break;
|
||||
|
|
@ -152,7 +159,9 @@ public class SettingsPageActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void handleNotificationsClick(String title) {
|
||||
if ("系统通知".equals(title) || "免打扰".equals(title)) {
|
||||
if ("通知设置".equals(title)) {
|
||||
NotificationSettingsActivity.start(this);
|
||||
} else if ("系统通知".equals(title) || "免打扰".equals(title)) {
|
||||
NotificationSettingsActivity.start(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -187,14 +196,14 @@ public class SettingsPageActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void refreshItems() {
|
||||
if (PAGE_CLEAR_CACHE.equals(currentPage)) {
|
||||
if (PAGE_CLEAR_CACHE.equals(page)) {
|
||||
// 异步加载缓存大小
|
||||
updateCacheSize();
|
||||
} else if (PAGE_ABOUT.equals(currentPage)) {
|
||||
} else if (PAGE_ABOUT.equals(page)) {
|
||||
// 更新版本信息
|
||||
updateVersionInfo();
|
||||
} else {
|
||||
adapter.submitList(buildItems(currentPage));
|
||||
adapter.submitList(buildItems(page));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -278,6 +287,7 @@ public class SettingsPageActivity extends AppCompatActivity {
|
|||
|
||||
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));
|
||||
list.add(MoreItem.row("免打扰", "设置勿扰时段", R.drawable.ic_notifications_24));
|
||||
return list;
|
||||
|
|
@ -466,4 +476,281 @@ public class SettingsPageActivity extends AppCompatActivity {
|
|||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showChangePasswordDialog() {
|
||||
EditText oldPassword = new EditText(this);
|
||||
oldPassword.setHint("请输入旧密码");
|
||||
oldPassword.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
|
||||
EditText newPassword = new EditText(this);
|
||||
newPassword.setHint("请输入新密码");
|
||||
newPassword.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
|
||||
android.widget.LinearLayout layout = new android.widget.LinearLayout(this);
|
||||
layout.setOrientation(android.widget.LinearLayout.VERTICAL);
|
||||
layout.setPadding(dp(24), dp(16), dp(24), dp(8));
|
||||
layout.addView(oldPassword);
|
||||
layout.addView(newPassword);
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("修改密码")
|
||||
.setView(layout)
|
||||
.setPositiveButton("确定", (d, w) -> {
|
||||
Toast.makeText(this, "密码修改功能待实现", Toast.LENGTH_SHORT).show();
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showBindPhoneDialog() {
|
||||
EditText phoneInput = new EditText(this);
|
||||
phoneInput.setHint("请输入手机号");
|
||||
phoneInput.setInputType(android.text.InputType.TYPE_CLASS_PHONE);
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("绑定手机号")
|
||||
.setView(phoneInput)
|
||||
.setPositiveButton("确定", (d, w) -> {
|
||||
Toast.makeText(this, "手机号绑定功能待实现", Toast.LENGTH_SHORT).show();
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showDeviceManagementDialog() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("登录设备管理")
|
||||
.setMessage("当前登录设备:\n• 本设备(当前)\n\n功能开发中...")
|
||||
.setPositiveButton("确定", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showBlacklistDialog() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("黑名单管理")
|
||||
.setMessage("黑名单功能允许您屏蔽不想看到的用户。\n\n" +
|
||||
"功能说明:\n" +
|
||||
"• 被加入黑名单的用户无法给您发送消息\n" +
|
||||
"• 您将不会看到被屏蔽用户的直播和动态\n" +
|
||||
"• 可以随时解除屏蔽\n\n" +
|
||||
"注意:此功能需要登录后使用,目前功能正在开发中,敬请期待。")
|
||||
.setPositiveButton("确定", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showPermissionManagementDialog() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("权限管理")
|
||||
.setMessage("应用需要以下权限才能正常工作:\n\n" +
|
||||
"• 相机:用于直播和拍照\n" +
|
||||
"• 麦克风:用于直播和语音聊天\n" +
|
||||
"• 位置:用于附近直播和发现功能\n" +
|
||||
"• 存储:用于保存图片和视频\n\n" +
|
||||
"点击确定将跳转到系统设置页面,您可以在那里管理应用权限。")
|
||||
.setPositiveButton("前往设置", (d, w) -> {
|
||||
try {
|
||||
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
intent.setData(android.net.Uri.parse("package:" + getPackageName()));
|
||||
startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(this, "无法打开设置页面", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showPrivacyPolicyDialog() {
|
||||
String privacyPolicy = "隐私政策\n\n" +
|
||||
"我们非常重视您的隐私保护。本隐私政策说明了我们如何收集、使用和保护您的个人信息。\n\n" +
|
||||
"1. 信息收集\n" +
|
||||
"我们可能收集以下信息:\n" +
|
||||
"• 账户信息:昵称、头像、个人资料\n" +
|
||||
"• 设备信息:设备型号、操作系统版本\n" +
|
||||
"• 使用信息:观看记录、互动记录\n" +
|
||||
"• 位置信息:仅在您授权时收集\n\n" +
|
||||
"2. 信息使用\n" +
|
||||
"我们使用收集的信息用于:\n" +
|
||||
"• 提供和改进服务\n" +
|
||||
"• 个性化推荐\n" +
|
||||
"• 保障账户安全\n\n" +
|
||||
"3. 信息保护\n" +
|
||||
"我们采用行业标准的安全措施保护您的信息,不会向第三方出售您的个人信息。\n\n" +
|
||||
"4. 您的权利\n" +
|
||||
"您可以随时访问、修改或删除您的个人信息。\n\n" +
|
||||
"如有疑问,请联系客服。";
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("隐私政策")
|
||||
.setMessage(privacyPolicy)
|
||||
.setPositiveButton("确定", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showClearAllCacheDialog() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("清理缓存")
|
||||
.setMessage("确定要清理所有缓存吗?")
|
||||
.setPositiveButton("确定", (d, w) -> {
|
||||
CacheManager.clearAllCache(this, new CacheManager.OnCacheClearListener() {
|
||||
@Override
|
||||
public void onSuccess(long clearedSize) {
|
||||
runOnUiThread(() -> {
|
||||
refreshItems();
|
||||
Toast.makeText(SettingsPageActivity.this, "缓存已清理", Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
runOnUiThread(() -> {
|
||||
Toast.makeText(SettingsPageActivity.this, "清理缓存失败", Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showClearImageCacheDialog() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("清理图片缓存")
|
||||
.setMessage("确定要清理图片缓存吗?")
|
||||
.setPositiveButton("确定", (d, w) -> {
|
||||
CacheManager.clearImageCache(this, new CacheManager.OnCacheClearListener() {
|
||||
@Override
|
||||
public void onSuccess(long clearedSize) {
|
||||
runOnUiThread(() -> {
|
||||
refreshItems();
|
||||
Toast.makeText(SettingsPageActivity.this, "图片缓存已清理", Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
runOnUiThread(() -> {
|
||||
Toast.makeText(SettingsPageActivity.this, "清理图片缓存失败", Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showFAQDialog() {
|
||||
String faq = "常见问题\n\n" +
|
||||
"Q1: 如何开始直播?\n" +
|
||||
"A: 在首页点击\"创建直播间\"按钮,输入房间信息后即可开始直播。\n\n" +
|
||||
"Q2: 如何关注主播?\n" +
|
||||
"A: 在直播间或主播个人主页点击\"关注\"按钮即可。\n\n" +
|
||||
"Q3: 如何发送消息?\n" +
|
||||
"A: 在直播间底部输入框输入消息后发送,或在消息页面与好友聊天。\n\n" +
|
||||
"Q4: 如何修改个人资料?\n" +
|
||||
"A: 进入个人中心,点击\"编辑资料\"即可修改昵称、签名等信息。\n\n" +
|
||||
"Q5: 如何清理缓存?\n" +
|
||||
"A: 在设置页面选择\"清理缓存\",可以清理应用缓存和图片缓存。\n\n" +
|
||||
"Q6: 忘记密码怎么办?\n" +
|
||||
"A: 请联系客服或通过绑定的手机号找回密码。\n\n" +
|
||||
"Q7: 如何举报不良内容?\n" +
|
||||
"A: 在直播间或用户主页可以举报违规内容,我们会及时处理。\n\n" +
|
||||
"更多问题请联系客服。";
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("常见问题")
|
||||
.setMessage(faq)
|
||||
.setPositiveButton("确定", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showFeedbackDialog() {
|
||||
EditText feedbackInput = new EditText(this);
|
||||
feedbackInput.setHint("请输入您的意见或建议");
|
||||
feedbackInput.setMinLines(5);
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("意见反馈")
|
||||
.setView(feedbackInput)
|
||||
.setPositiveButton("提交", (d, w) -> {
|
||||
Toast.makeText(this, "反馈提交功能待实现", Toast.LENGTH_SHORT).show();
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showCustomerServiceDialog() {
|
||||
String customerService = "联系客服\n\n" +
|
||||
"我们提供多种客服联系方式:\n\n" +
|
||||
"📞 客服电话:\n" +
|
||||
"400-XXX-XXXX\n" +
|
||||
"服务时间:9:00-22:00\n\n" +
|
||||
"💬 在线客服:\n" +
|
||||
"在应用内消息页面联系客服\n\n" +
|
||||
"📧 邮箱:\n" +
|
||||
"support@livestreaming.com\n\n" +
|
||||
"🕐 服务时间:\n" +
|
||||
"周一至周日 9:00-22:00\n\n" +
|
||||
"我们会尽快回复您的问题。";
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("联系客服")
|
||||
.setMessage(customerService)
|
||||
.setPositiveButton("确定", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showVersionInfoDialog() {
|
||||
try {
|
||||
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
|
||||
String versionName = packageInfo.versionName;
|
||||
int versionCode = packageInfo.versionCode;
|
||||
String message = "Live Streaming\n版本:" + versionName + "\n构建号:" + versionCode;
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("版本信息")
|
||||
.setMessage(message)
|
||||
.setPositiveButton("确定", null)
|
||||
.show();
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("版本信息")
|
||||
.setMessage("Live Streaming 1.0")
|
||||
.setPositiveButton("确定", null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void showUserAgreementDialog() {
|
||||
String userAgreement = "用户协议\n\n" +
|
||||
"欢迎使用直播应用!使用本应用即表示您同意遵守以下条款:\n\n" +
|
||||
"1. 服务条款\n" +
|
||||
"• 您必须年满18周岁才能使用本服务\n" +
|
||||
"• 您需要对自己的账户安全负责\n" +
|
||||
"• 禁止发布违法违规内容\n\n" +
|
||||
"2. 用户行为规范\n" +
|
||||
"• 禁止发布色情、暴力、赌博等违法内容\n" +
|
||||
"• 禁止骚扰、辱骂其他用户\n" +
|
||||
"• 禁止传播虚假信息\n" +
|
||||
"• 禁止进行任何形式的欺诈行为\n\n" +
|
||||
"3. 知识产权\n" +
|
||||
"• 应用内的所有内容受知识产权法保护\n" +
|
||||
"• 未经授权不得复制、传播\n\n" +
|
||||
"4. 免责声明\n" +
|
||||
"• 用户发布的内容不代表本平台观点\n" +
|
||||
"• 平台不对用户行为承担责任\n\n" +
|
||||
"5. 服务变更\n" +
|
||||
"我们保留随时修改或终止服务的权利。\n\n" +
|
||||
"如有疑问,请联系客服。";
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("用户协议")
|
||||
.setMessage(userAgreement)
|
||||
.setPositiveButton("确定", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private int dp(int value) {
|
||||
return (int) (value * getResources().getDisplayMetrics().density);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import android.widget.Toast;
|
|||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
|
||||
|
|
@ -453,10 +454,27 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
if (!TextUtils.isEmpty(avatarUri)) {
|
||||
Uri uri = Uri.parse(avatarUri);
|
||||
// 如果是 file:// 协议,尝试转换为 FileProvider URI
|
||||
if ("file".equals(uri.getScheme())) {
|
||||
try {
|
||||
java.io.File file = new java.io.File(uri.getPath());
|
||||
if (file.exists()) {
|
||||
uri = FileProvider.getUriForFile(
|
||||
this,
|
||||
getPackageName() + ".fileprovider",
|
||||
file
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 如果转换失败,使用原始 URI
|
||||
}
|
||||
}
|
||||
Glide.with(this)
|
||||
.load(Uri.parse(avatarUri))
|
||||
.load(uri)
|
||||
.circleCrop()
|
||||
.error(R.drawable.ic_account_circle_24)
|
||||
.placeholder(R.drawable.ic_account_circle_24)
|
||||
.into(binding.moreAvatar);
|
||||
} else if (avatarRes != 0) {
|
||||
binding.moreAvatar.setImageResource(avatarRes);
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ public class TableGamesActivity extends AppCompatActivity {
|
|||
|
||||
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
|
||||
bottomNavigation.setSelectedItemId(R.id.nav_friends);
|
||||
|
||||
// 更新未读消息徽章
|
||||
UnreadMessageManager.updateBadge(bottomNavigation);
|
||||
|
||||
bottomNavigation.setOnItemSelectedListener(item -> {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.nav_home) {
|
||||
|
|
@ -57,5 +61,16 @@ public class TableGamesActivity extends AppCompatActivity {
|
|||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (binding != null) {
|
||||
BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
|
||||
bottomNav.setSelectedItemId(R.id.nav_friends);
|
||||
// 更新未读消息徽章
|
||||
UnreadMessageManager.updateBadge(bottomNav);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,13 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
|
|||
boolean isFriend = isFriend(userId);
|
||||
updateAddFriendButton(isFriend);
|
||||
|
||||
// TODO: 接入后端接口 - 发送好友请求
|
||||
// 接口路径: POST /api/friends/request
|
||||
// 请求参数:
|
||||
// - targetUserId: 目标用户ID
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// 返回数据格式: ApiResponse<{success: boolean, message: string}>
|
||||
// 发送成功后,更新按钮状态为"已发送"或"已添加"
|
||||
binding.addFriendButton.setOnClickListener(v -> {
|
||||
if (TextUtils.isEmpty(userId)) {
|
||||
Toast.makeText(this, "用户信息缺失", Toast.LENGTH_SHORT).show();
|
||||
|
|
@ -133,6 +140,21 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void bindDemoStatsAndWorks(String userId) {
|
||||
// TODO: 接入后端接口 - 获取其他用户资料和统计数据
|
||||
// 接口路径: GET /api/users/{userId}/profile
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(路径参数)
|
||||
// 返回数据格式: ApiResponse<UserProfile>
|
||||
// UserProfile对象应包含: id, name, avatarUrl, bio, location, worksCount, followingCount,
|
||||
// followersCount, likesCount等字段
|
||||
// TODO: 接入后端接口 - 获取用户作品列表
|
||||
// 接口路径: GET /api/users/{userId}/works
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(路径参数)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<WorkItem>>
|
||||
// WorkItem对象应包含: id, coverUrl, title, likeCount, viewCount等字段
|
||||
if (binding == null) return;
|
||||
|
||||
String seed = !TextUtils.isEmpty(userId) ? userId : "demo";
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ public class VoiceMatchActivity extends AppCompatActivity {
|
|||
|
||||
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
|
||||
bottomNavigation.setSelectedItemId(R.id.nav_friends);
|
||||
|
||||
// 更新未读消息徽章
|
||||
UnreadMessageManager.updateBadge(bottomNavigation);
|
||||
|
||||
bottomNavigation.setOnItemSelectedListener(item -> {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.nav_home) {
|
||||
|
|
@ -58,5 +62,16 @@ public class VoiceMatchActivity extends AppCompatActivity {
|
|||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (binding != null) {
|
||||
BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
|
||||
bottomNav.setSelectedItemId(R.id.nav_friends);
|
||||
// 更新未读消息徽章
|
||||
UnreadMessageManager.updateBadge(bottomNav);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,15 @@ public class WatchHistoryActivity extends AppCompatActivity {
|
|||
startActivity(intent);
|
||||
});
|
||||
|
||||
// TODO: 接入后端接口 - 获取观看历史
|
||||
// 接口路径: GET /api/watch/history
|
||||
// 请求参数:
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<Room>>
|
||||
// Room对象应包含: id, title, streamerName, type, isLive, coverUrl, watchTime(观看时间)等字段
|
||||
// 列表应按观看时间倒序排列(最近观看的在前面)
|
||||
StaggeredGridLayoutManager glm = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
|
||||
glm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
|
||||
binding.roomsRecyclerView.setLayoutManager(glm);
|
||||
|
|
|
|||
|
|
@ -1 +1,145 @@
|
|||
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 -> {
int id = item.getItemId();
if (id == R.id.nav_wish_tree) {
return true;
}
if (id == R.id.nav_home) {
startActivity(new Intent(this, MainActivity.class));
finish();
return true;
}
if (id == R.id.nav_friends) {
startActivity(new Intent(this, FishPondActivity.class));
finish();
return true;
}
if (id == R.id.nav_messages) {
MessagesActivity.start(this);
finish();
return true;
}
if (id == R.id.nav_profile) {
ProfileActivity.start(this);
finish();
return true;
}
return true;
});
}
@Override
protected void onResume() {
super.onResume();
if (binding != null) {
binding.bottomNavInclude.bottomNavigation.setSelectedItemId(R.id.nav_wish_tree);
}
}
}
|
||||
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);
|
||||
|
||||
// 更新未读消息徽章
|
||||
UnreadMessageManager.updateBadge(bottomNavigation);
|
||||
|
||||
bottomNavigation.setOnItemSelectedListener(item -> {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.nav_wish_tree) {
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.nav_home) {
|
||||
startActivity(new Intent(this, MainActivity.class));
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.nav_friends) {
|
||||
startActivity(new Intent(this, FishPondActivity.class));
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.nav_messages) {
|
||||
MessagesActivity.start(this);
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.nav_profile) {
|
||||
ProfileActivity.start(this);
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (binding != null) {
|
||||
BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
|
||||
bottomNav.setSelectedItemId(R.id.nav_wish_tree);
|
||||
// 更新未读消息徽章
|
||||
UnreadMessageManager.updateBadge(bottomNav);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
android-app/app/src/main/res/drawable/bg_circle_red.xml
Normal file
6
android-app/app/src/main/res/drawable/bg_circle_red.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#FF4444" />
|
||||
</shape>
|
||||
|
||||
11
android-app/app/src/main/res/drawable/ic_check_24.xml
Normal file
11
android-app/app/src/main/res/drawable/ic_check_24.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#AAAAAA"
|
||||
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
|
||||
</vector>
|
||||
|
||||
16
android-app/app/src/main/res/drawable/ic_check_double_24.xml
Normal file
16
android-app/app/src/main/res/drawable/ic_check_double_24.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!-- 第一个勾 -->
|
||||
<path
|
||||
android:fillColor="#4CAF50"
|
||||
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
|
||||
<!-- 第二个勾(稍微偏移) -->
|
||||
<path
|
||||
android:fillColor="#4CAF50"
|
||||
android:pathData="M11,16.17L6.83,12l-1.42,1.41L11,19 23,7l-1.41,-1.41z" />
|
||||
</vector>
|
||||
|
||||
10
android-app/app/src/main/res/drawable/ic_share_24.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_share_24.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M18,16.08c-0.7,0 -1.3,-0.3 -1.8,-0.8l-6.2,-3.6c0.1,-0.3 0.2,-0.6 0.2,-1 0,-0.4 -0.1,-0.7 -0.2,-1l6.2,-3.6c0.5,0.5 1.1,0.8 1.8,0.8 1.4,0 2.5,-1.1 2.5,-2.5s-1.1,-2.5 -2.5,-2.5 -2.5,1.1 -2.5,2.5c0,0.4 0.1,0.7 0.2,1l-6.2,3.6c-0.5,-0.5 -1.1,-0.8 -1.8,-0.8 -1.4,0 -2.5,1.1 -2.5,2.5s1.1,2.5 2.5,2.5c0.7,0 1.3,-0.3 1.8,-0.8l6.2,3.6c-0.1,0.3 -0.2,0.6 -0.2,1 0,1.4 1.1,2.5 2.5,2.5s2.5,-1.1 2.5,-2.5 -1.1,-2.5 -2.5,-2.5z"/>
|
||||
</vector>
|
||||
|
||||
|
|
@ -32,6 +32,42 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/searchInputLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="搜索会话或消息"
|
||||
app:boxBackgroundMode="outline"
|
||||
app:boxCornerRadiusBottomEnd="20dp"
|
||||
app:boxCornerRadiusBottomStart="20dp"
|
||||
app:boxCornerRadiusTopEnd="20dp"
|
||||
app:boxCornerRadiusTopStart="20dp"
|
||||
app:boxStrokeWidth="0dp"
|
||||
app:boxStrokeWidthFocused="0dp"
|
||||
app:hintTextColor="#AAAAAA"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/title">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/searchInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:imeOptions="actionSearch"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:textColor="#111111"
|
||||
android:textColorHint="#AAAAAA"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
|
|
|||
114
android-app/app/src/main/res/layout/activity_notifications.xml
Normal file
114
android-app/app/src/main/res/layout/activity_notifications.xml
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/white">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/white">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="14dp"
|
||||
android:paddingBottom="14dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/backButton"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="返回"
|
||||
android:src="@drawable/ic_arrow_back_24"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="通知"
|
||||
android:textColor="#111111"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/backButton"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<!-- 分类标签 -->
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/categoryTabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/white"
|
||||
app:tabIndicatorColor="@color/purple_500"
|
||||
app:tabIndicatorFullWidth="false"
|
||||
app:tabMode="scrollable"
|
||||
app:tabSelectedTextColor="#111111"
|
||||
app:tabTextColor="#666666"
|
||||
app:tabPaddingStart="16dp"
|
||||
app:tabPaddingEnd="16dp">
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="全部" />
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="系统" />
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="互动" />
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="关注" />
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="私信" />
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="直播" />
|
||||
|
||||
</com.google.android.material.tabs.TabLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/notificationsRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="16dp"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
<com.example.livestreaming.EmptyStateView
|
||||
android:id="@+id/emptyStateView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
|
|
@ -217,6 +217,16 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/shareButton"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="分享"
|
||||
android:src="@drawable/ic_share_24"
|
||||
android:tint="#666666" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/followButton"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
|
|
|
|||
|
|
@ -59,16 +59,6 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/title" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/muteIcon"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:contentDescription="mute"
|
||||
android:src="@android:drawable/ic_lock_silent_mode"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/lastMessage"
|
||||
app:layout_constraintEnd_toStartOf="@id/unreadBadge" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/unreadBadge"
|
||||
|
|
|
|||
|
|
@ -47,17 +47,35 @@
|
|||
app:layout_constraintHorizontal_bias="1"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/timeText"
|
||||
<LinearLayout
|
||||
android:id="@+id/timeStatusLayout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="1dp"
|
||||
android:includeFontPadding="false"
|
||||
android:text="12:31"
|
||||
android:textColor="#AAAAAA"
|
||||
android:textSize="11sp"
|
||||
android:visibility="gone"
|
||||
android:layout_marginStart="4dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
app:layout_constraintEnd_toEndOf="@id/messageText"
|
||||
app:layout_constraintTop_toBottomOf="@id/messageText" />
|
||||
app:layout_constraintTop_toBottomOf="@id/messageText">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/timeText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:includeFontPadding="false"
|
||||
android:text="12:31"
|
||||
android:textColor="#AAAAAA"
|
||||
android:textSize="11sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/statusIcon"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:visibility="gone"
|
||||
android:contentDescription="消息状态" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
|||
86
android-app/app/src/main/res/layout/item_notification.xml
Normal file
86
android-app/app/src/main/res/layout/item_notification.xml
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/white"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:minHeight="72dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:background="@drawable/bg_avatar_circle_transparent"
|
||||
android:clipToOutline="true"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/ic_notifications_24"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<!-- 未读标记 -->
|
||||
<View
|
||||
android:id="@+id/unreadDot"
|
||||
android:layout_width="8dp"
|
||||
android:layout_height="8dp"
|
||||
android:background="@drawable/bg_circle_red"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="@id/avatar"
|
||||
app:layout_constraintTop_toTopOf="@id/avatar" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:text="通知标题"
|
||||
android:textColor="#111111"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toStartOf="@id/timeText"
|
||||
app:layout_constraintStart_toEndOf="@id/avatar"
|
||||
app:layout_constraintTop_toTopOf="@id/avatar"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="10dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:text="通知内容预览"
|
||||
android:textColor="#666666"
|
||||
android:textSize="13sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/avatar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/title"
|
||||
android:layout_marginTop="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/timeText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="12:30"
|
||||
android:textColor="#999999"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/title" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginStart="56dp"
|
||||
android:background="#0F000000"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/avatar" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<cache-path name="cache_images" path="images/" />
|
||||
<files-path name="avatars" path="avatars/" />
|
||||
</paths>
|
||||
|
|
|
|||
|
|
@ -534,24 +534,30 @@
|
|||
|
||||
**预计工作量**: 2-3天
|
||||
|
||||
#### 9. **消息功能完善(前端)** ⭐⭐ ✅ 已完成
|
||||
#### 9. **消息功能完善(前端)** ⭐⭐ ⚠️ 待实现
|
||||
**为什么重要**: 完善核心社交功能
|
||||
|
||||
- [x] 完善消息列表UI
|
||||
- [x] 实现消息状态显示(发送中、已发送、已读)
|
||||
- [x] 优化消息列表性能
|
||||
- [x] 添加消息操作(复制、删除等)
|
||||
- [x] 实现消息搜索(本地)
|
||||
- [x] 完善消息列表UI(会话列表)
|
||||
- [ ] 实现消息状态显示(发送中、已发送、已读)
|
||||
- [ ] 优化消息列表性能(使用messageId)
|
||||
- [ ] 添加消息操作(复制、删除等)
|
||||
- [ ] 实现消息搜索(本地)
|
||||
|
||||
**完成内容**:
|
||||
- 为ChatMessage添加了MessageStatus枚举(发送中、已发送、已读)
|
||||
- 在发送的消息气泡旁显示状态图标(时钟、单勾、双勾)
|
||||
- 实现了消息长按菜单,支持复制和删除操作
|
||||
- 优化了DiffUtil,使用messageId作为唯一标识,提升列表更新性能
|
||||
- 在MessagesActivity中添加了搜索功能,支持按会话标题和消息内容搜索
|
||||
- 优化了消息列表UI,包括消息预览(处理图片/语音消息)、时间显示、布局优化
|
||||
- 添加了内存泄漏防护(onDestroy中清理延迟任务)
|
||||
- 优化了滚动行为(使用smoothScrollToPosition)
|
||||
**已完成内容**:
|
||||
- ✅ 会话列表展示(MessagesActivity)
|
||||
- ✅ 滑动删除/标记已读功能(MessagesActivity)
|
||||
- ✅ 消息发送和列表展示(ConversationActivity)
|
||||
- ✅ 未读消息徽章管理
|
||||
|
||||
**待实现内容**:
|
||||
- ⚠️ 为ChatMessage添加MessageStatus枚举(发送中、已发送、已读)
|
||||
- ⚠️ 在发送的消息气泡旁显示状态图标(时钟、单勾、双勾)
|
||||
- ⚠️ 实现消息长按菜单,支持复制和删除操作
|
||||
- ⚠️ 优化DiffUtil,使用messageId作为唯一标识,提升列表更新性能(当前使用timestamp)
|
||||
- ⚠️ 在MessagesActivity中添加搜索功能,支持按会话标题和消息内容搜索
|
||||
- ⚠️ 优化消息列表UI,包括消息预览(处理图片/语音消息)、时间显示、布局优化
|
||||
- ⚠️ 添加内存泄漏防护(onDestroy中清理延迟任务)
|
||||
- ⚠️ 优化滚动行为(使用smoothScrollToPosition替代scrollToPosition)
|
||||
|
||||
**预计工作量**: 3-4天
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user