语音、图片的发送功能编写,设置聊天室的封面
This commit is contained in:
parent
124026ba62
commit
987e67dcb0
|
|
@ -14,6 +14,7 @@ import com.zbkj.service.service.ChatRoomService;
|
|||
import com.zbkj.service.service.ChatRoomEntryRecordService;
|
||||
import com.zbkj.service.service.UserService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
|
@ -202,6 +203,84 @@ public class ChatRoomFrontController {
|
|||
return CommonResult.success(page);
|
||||
}
|
||||
|
||||
@ApiOperation(value = "创建聊天室")
|
||||
@PostMapping("/create")
|
||||
public CommonResult<ChatRoomResponse> createChatRoom(@RequestBody CreateChatRoomRequest request) {
|
||||
Integer uid = null;
|
||||
try {
|
||||
uid = frontTokenComponent.getUserId();
|
||||
} catch (Exception e) {
|
||||
return CommonResult.failed("请先登录");
|
||||
}
|
||||
|
||||
if (uid == null) {
|
||||
return CommonResult.failed("请先登录");
|
||||
}
|
||||
|
||||
// 验证参数
|
||||
if (request.getRoomTitle() == null || request.getRoomTitle().trim().isEmpty()) {
|
||||
return CommonResult.failed("房间名称不能为空");
|
||||
}
|
||||
|
||||
// 构建聊天室对象
|
||||
ChatRoom chatRoom = new ChatRoom();
|
||||
chatRoom.setOwnerId(uid);
|
||||
chatRoom.setRoomTitle(request.getRoomTitle().trim());
|
||||
chatRoom.setRoomDescription(request.getRoomDescription());
|
||||
chatRoom.setRoomImage(request.getRoomImage());
|
||||
chatRoom.setRoomType(request.getRoomType() != null ? request.getRoomType() : 0);
|
||||
chatRoom.setEntryFee(request.getEntryFee() != null ? request.getEntryFee() : 0);
|
||||
chatRoom.setMaxMembers(request.getMaxMembers() != null ? Math.min(request.getMaxMembers(), 24) : 24);
|
||||
chatRoom.setCurrentMembers(0);
|
||||
chatRoom.setStatus(1);
|
||||
chatRoom.setIsRecommended(0);
|
||||
chatRoom.setIsPrivate(0);
|
||||
chatRoom.setTotalMessages(0);
|
||||
chatRoom.setTotalMembers(0);
|
||||
chatRoom.setCreateTime(new Date());
|
||||
chatRoom.setUpdateTime(new Date());
|
||||
|
||||
// 生成房间号
|
||||
chatRoom.setRoomCode("CR" + System.currentTimeMillis() % 100000000);
|
||||
|
||||
// 如果是收费房间,必须设置费用
|
||||
if (chatRoom.getRoomType() == 1 && (chatRoom.getEntryFee() == null || chatRoom.getEntryFee() <= 0)) {
|
||||
return CommonResult.failed("收费房间必须设置入场费用");
|
||||
}
|
||||
|
||||
boolean saved = chatRoomService.save(chatRoom);
|
||||
if (!saved) {
|
||||
return CommonResult.failed("创建聊天室失败");
|
||||
}
|
||||
|
||||
log.info("用户 {} 创建聊天室: {}", uid, chatRoom.getRoomTitle());
|
||||
|
||||
// 返回创建的聊天室信息
|
||||
ChatRoomResponse response = chatRoomService.getDetail(chatRoom.getId());
|
||||
return CommonResult.success(response);
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class CreateChatRoomRequest {
|
||||
@ApiModelProperty(value = "房间名称", required = true)
|
||||
private String roomTitle;
|
||||
|
||||
@ApiModelProperty(value = "房间描述")
|
||||
private String roomDescription;
|
||||
|
||||
@ApiModelProperty(value = "房间封面图")
|
||||
private String roomImage;
|
||||
|
||||
@ApiModelProperty(value = "房间类型:0=免费,1=收费")
|
||||
private Integer roomType;
|
||||
|
||||
@ApiModelProperty(value = "进入房间费用(金币)")
|
||||
private Integer entryFee;
|
||||
|
||||
@ApiModelProperty(value = "房间最大人数(不能超过24)")
|
||||
private Integer maxMembers;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class PayEntryResponse {
|
||||
private Boolean success;
|
||||
|
|
|
|||
|
|
@ -98,8 +98,9 @@ public class UploadServiceImpl implements UploadService {
|
|||
logger.info("文件是否为空: {}", multipartFile.isEmpty());
|
||||
|
||||
// 判断是否使用远程上传服务器
|
||||
// works-作品, cover-封面, avatar-头像 都使用远程服务器
|
||||
if ("works".equals(model) || "cover".equals(model) || "avatar".equals(model)) {
|
||||
// works-作品, cover-封面, avatar-头像, chatroom-聊天室, chat-聊天 都使用远程服务器
|
||||
if ("works".equals(model) || "cover".equals(model) || "avatar".equals(model)
|
||||
|| "chatroom".equals(model) || "chat".equals(model)) {
|
||||
logger.info("使用远程上传服务器 - model: {}", model);
|
||||
fileResultVo = remoteUploadService.uploadToRemoteServer(multipartFile, model);
|
||||
} else {
|
||||
|
|
@ -293,10 +294,23 @@ public class UploadServiceImpl implements UploadService {
|
|||
logger.info("文件是否已存在: {}", file.exists());
|
||||
|
||||
try {
|
||||
multipartFile.transferTo(file);
|
||||
// 使用绝对路径的File对象,避免transferTo使用相对路径
|
||||
File absoluteFile = file.getAbsoluteFile();
|
||||
logger.info("使用绝对路径: {}", absoluteFile.getAbsolutePath());
|
||||
|
||||
// 使用输入流方式写入文件,避免transferTo的路径问题
|
||||
try (java.io.InputStream inputStream = multipartFile.getInputStream();
|
||||
java.io.FileOutputStream outputStream = new java.io.FileOutputStream(absoluteFile)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
outputStream.flush();
|
||||
}
|
||||
logger.info("文件保存成功!");
|
||||
} catch (IOException e) {
|
||||
logger.error("transferTo失败!", e);
|
||||
logger.error("文件保存失败!", e);
|
||||
logger.error("文件路径: {}", file.getAbsolutePath());
|
||||
logger.error("文件大小: {} bytes", multipartFile.getSize());
|
||||
logger.error("文件名: {}", multipartFile.getOriginalFilename());
|
||||
|
|
@ -312,7 +326,17 @@ public class UploadServiceImpl implements UploadService {
|
|||
|
||||
logger.info("云存储模式,先保存到本地: {}", file.getAbsolutePath());
|
||||
try {
|
||||
multipartFile.transferTo(file);
|
||||
// 使用输入流方式写入文件
|
||||
File absoluteFile = file.getAbsoluteFile();
|
||||
try (java.io.InputStream inputStream = multipartFile.getInputStream();
|
||||
java.io.FileOutputStream outputStream = new java.io.FileOutputStream(absoluteFile)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
outputStream.flush();
|
||||
}
|
||||
logger.info("本地文件保存成功!");
|
||||
} catch (IOException e) {
|
||||
logger.error("云存储模式下本地保存失败!", e);
|
||||
|
|
@ -484,8 +508,17 @@ public class UploadServiceImpl implements UploadService {
|
|||
systemAttachment.setImageType(1);
|
||||
systemAttachment.setPid(pid);
|
||||
|
||||
// 保存文件
|
||||
multipartFile.transferTo(file);
|
||||
// 保存文件 - 使用输入流方式避免transferTo路径问题
|
||||
File absoluteFile = file.getAbsoluteFile();
|
||||
try (java.io.InputStream inputStream = multipartFile.getInputStream();
|
||||
java.io.FileOutputStream outputStream = new java.io.FileOutputStream(absoluteFile)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
outputStream.flush();
|
||||
}
|
||||
systemAttachmentService.save(systemAttachment);
|
||||
|
||||
logger.info("视频上传成功: {}, 大小: {} MB", fileName, String.format("%.2f", fileSize));
|
||||
|
|
@ -520,10 +553,20 @@ public class UploadServiceImpl implements UploadService {
|
|||
throw new CrmebException(CommonResultCode.VALIDATE_FAILED, String.format("语音文件最大允许 10 MB,当前文件大小为 %.2f MB", fileSize));
|
||||
}
|
||||
|
||||
if (fileName.length() > 99) {
|
||||
fileName = StrUtil.subPre(fileName, 90).concat(".").concat(extName);
|
||||
}
|
||||
// 使用远程上传服务器上传语音
|
||||
logger.info("=== 开始上传语音 ===");
|
||||
logger.info("文件名: {}, 大小: {} MB, 格式: {}", fileName, String.format("%.2f", fileSize), extName);
|
||||
|
||||
FileResultVo resultFile = remoteUploadService.uploadToRemoteServer(multipartFile, "voice");
|
||||
logger.info("语音上传成功: {}", resultFile.getUrl());
|
||||
|
||||
return resultFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 语音上传(本地存储 - 备用方法)
|
||||
*/
|
||||
private FileResultVo voiceUploadLocal(MultipartFile multipartFile, String model, Integer pid, String fileName, String extName, float fileSize) throws IOException {
|
||||
// 服务器存储地址 - 使用绝对路径
|
||||
String rootPath = crmebConfig.getAbsoluteImagePath();
|
||||
String modelPath = "public/" + model + "/";
|
||||
|
|
@ -552,15 +595,20 @@ public class UploadServiceImpl implements UploadService {
|
|||
systemAttachment.setImageType(1);
|
||||
systemAttachment.setPid(pid);
|
||||
|
||||
// 保存文件
|
||||
multipartFile.transferTo(file);
|
||||
// 保存文件 - 使用输入流方式避免transferTo路径问题
|
||||
File absoluteFile = file.getAbsoluteFile();
|
||||
try (java.io.InputStream inputStream = multipartFile.getInputStream();
|
||||
java.io.FileOutputStream outputStream = new java.io.FileOutputStream(absoluteFile)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
outputStream.flush();
|
||||
}
|
||||
systemAttachmentService.save(systemAttachment);
|
||||
|
||||
// TODO: 提取语音时长
|
||||
// 可以使用 FFmpeg 或其他音频处理库来提取音频时长
|
||||
// resultFile.setDuration(extractAudioDuration(file));
|
||||
|
||||
logger.info("语音上传成功: {}, 大小: {} MB", fileName, String.format("%.2f", fileSize));
|
||||
logger.info("语音本地上传成功: {}, 大小: {} MB", fileName, String.format("%.2f", fileSize));
|
||||
return resultFile;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -414,7 +414,18 @@ public class ChatRoomDetailActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
try {
|
||||
voiceFilePath = getCacheDir() + "/voice_" + System.currentTimeMillis() + ".m4a";
|
||||
// 使用 File 对象正确拼接路径
|
||||
File cacheDir = getCacheDir();
|
||||
if (!cacheDir.exists()) {
|
||||
cacheDir.mkdirs();
|
||||
}
|
||||
|
||||
File voiceFile = new File(cacheDir, "voice_" + System.currentTimeMillis() + ".m4a");
|
||||
voiceFilePath = voiceFile.getAbsolutePath();
|
||||
|
||||
Log.d(TAG, "录音文件路径: " + voiceFilePath);
|
||||
Log.d(TAG, "缓存目录存在: " + cacheDir.exists() + ", 可写: " + cacheDir.canWrite());
|
||||
|
||||
mediaRecorder = new MediaRecorder();
|
||||
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
|
||||
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
|
||||
|
|
@ -429,7 +440,11 @@ public class ChatRoomDetailActivity extends AppCompatActivity {
|
|||
binding.voiceButton.setAlpha(0.5f);
|
||||
Toast.makeText(this, "正在录音,松开发送", Toast.LENGTH_SHORT).show();
|
||||
} catch (IOException e) {
|
||||
Toast.makeText(this, "录音失败", Toast.LENGTH_SHORT).show();
|
||||
Log.e(TAG, "录音失败", e);
|
||||
Toast.makeText(this, "录音失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "录音异常", e);
|
||||
Toast.makeText(this, "录音异常: " + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -447,12 +462,35 @@ public class ChatRoomDetailActivity extends AppCompatActivity {
|
|||
int duration = (int) ((System.currentTimeMillis() - voiceStartTime) / 1000);
|
||||
if (duration < 1) {
|
||||
Toast.makeText(this, "录音时间太短", Toast.LENGTH_SHORT).show();
|
||||
new File(voiceFilePath).delete();
|
||||
if (voiceFilePath != null) {
|
||||
new File(voiceFilePath).delete();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查录音文件是否成功创建
|
||||
File voiceFile = new File(voiceFilePath);
|
||||
if (!voiceFile.exists() || voiceFile.length() == 0) {
|
||||
Log.e(TAG, "录音文件不存在或为空: " + voiceFilePath);
|
||||
Toast.makeText(this, "录音失败:文件未生成", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(TAG, "录音完成,文件大小: " + voiceFile.length() + " bytes, 时长: " + duration + "s");
|
||||
uploadAndSendVoice(voiceFilePath, duration);
|
||||
} catch (RuntimeException e) {
|
||||
Log.e(TAG, "停止录音失败", e);
|
||||
Toast.makeText(this, "录音失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
isRecording = false;
|
||||
binding.voiceButton.setAlpha(1.0f);
|
||||
if (mediaRecorder != null) {
|
||||
try {
|
||||
mediaRecorder.release();
|
||||
} catch (Exception ignored) {}
|
||||
mediaRecorder = null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "停止录音异常", e);
|
||||
isRecording = false;
|
||||
binding.voiceButton.setAlpha(1.0f);
|
||||
}
|
||||
|
|
@ -460,12 +498,22 @@ public class ChatRoomDetailActivity extends AppCompatActivity {
|
|||
|
||||
private void uploadAndSendVoice(String filePath, int duration) {
|
||||
File voiceFile = new File(filePath);
|
||||
RequestBody requestFile = RequestBody.create(MediaType.parse("audio/*"), voiceFile);
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!voiceFile.exists()) {
|
||||
Log.e(TAG, "语音文件不存在: " + filePath);
|
||||
Toast.makeText(this, "发送失败:录音文件不存在", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(TAG, "上传语音文件: " + filePath + ", 大小: " + voiceFile.length() + " bytes");
|
||||
|
||||
RequestBody requestFile = RequestBody.create(MediaType.parse("audio/mp4"), voiceFile);
|
||||
MultipartBody.Part body = MultipartBody.Part.createFormData("file", voiceFile.getName(), requestFile);
|
||||
RequestBody model = RequestBody.create(MediaType.parse("text/plain"), "chatroom");
|
||||
RequestBody model = RequestBody.create(MediaType.parse("text/plain"), "chat");
|
||||
RequestBody pid = RequestBody.create(MediaType.parse("text/plain"), "0");
|
||||
|
||||
ApiClient.getService(this).uploadImage(body, model, pid)
|
||||
ApiClient.getService(this).uploadVoice(body, model, pid)
|
||||
.enqueue(new Callback<ApiResponse<FileUploadResponse>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<FileUploadResponse>> call,
|
||||
|
|
@ -475,16 +523,23 @@ public class ChatRoomDetailActivity extends AppCompatActivity {
|
|||
FileUploadResponse data = response.body().getData();
|
||||
if (data != null && data.getUrl() != null) {
|
||||
sendVoiceMessage(data.getUrl(), duration);
|
||||
} else {
|
||||
Toast.makeText(ChatRoomDetailActivity.this, "发送失败:上传返回为空", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(ChatRoomDetailActivity.this, "发送失败", Toast.LENGTH_SHORT).show();
|
||||
String errorMsg = "发送失败";
|
||||
if (response.body() != null && response.body().getMessage() != null) {
|
||||
errorMsg = response.body().getMessage();
|
||||
}
|
||||
Toast.makeText(ChatRoomDetailActivity.this, errorMsg, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<FileUploadResponse>> call, Throwable t) {
|
||||
voiceFile.delete();
|
||||
Toast.makeText(ChatRoomDetailActivity.this, "发送失败", Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(ChatRoomDetailActivity.this, "发送失败:" + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
Log.e(TAG, "语音上传失败", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -542,6 +597,10 @@ public class ChatRoomDetailActivity extends AppCompatActivity {
|
|||
if (mediaRecorder != null) {
|
||||
try { mediaRecorder.release(); } catch (Exception ignored) {}
|
||||
}
|
||||
// 释放消息适配器资源(停止语音播放)
|
||||
if (messageAdapter != null) {
|
||||
messageAdapter.release();
|
||||
}
|
||||
}
|
||||
|
||||
public static class ChatRoomMessage {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.media.MediaPlayer;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.example.livestreaming.util.UrlUtils;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
|
@ -24,12 +28,17 @@ import de.hdodenhof.circleimageview.CircleImageView;
|
|||
*/
|
||||
public class ChatRoomMessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
|
||||
private static final String TAG = "ChatRoomMsgAdapter";
|
||||
private static final int TYPE_TEXT_LEFT = 0;
|
||||
private static final int TYPE_TEXT_RIGHT = 1;
|
||||
private static final int TYPE_SYSTEM = 2;
|
||||
|
||||
private final List<ChatRoomDetailActivity.ChatRoomMessage> messages;
|
||||
private final SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm", Locale.getDefault());
|
||||
|
||||
// 语音播放器
|
||||
private MediaPlayer mediaPlayer;
|
||||
private int currentPlayingPosition = -1;
|
||||
|
||||
public ChatRoomMessageAdapter(List<ChatRoomDetailActivity.ChatRoomMessage> messages) {
|
||||
this.messages = messages;
|
||||
|
|
@ -101,6 +110,8 @@ public class ChatRoomMessageAdapter extends RecyclerView.Adapter<RecyclerView.Vi
|
|||
}
|
||||
|
||||
void bind(ChatRoomDetailActivity.ChatRoomMessage msg) {
|
||||
final int position = getAdapterPosition();
|
||||
|
||||
if (nickname != null) {
|
||||
nickname.setText(msg.nickname != null ? msg.nickname : "用户" + msg.userId);
|
||||
}
|
||||
|
|
@ -108,11 +119,13 @@ public class ChatRoomMessageAdapter extends RecyclerView.Adapter<RecyclerView.Vi
|
|||
time.setText(timeFormat.format(new Date(msg.timestamp)));
|
||||
}
|
||||
|
||||
// 加载头像
|
||||
// 加载头像 - 处理URL
|
||||
if (avatar != null) {
|
||||
if (msg.avatar != null && !msg.avatar.isEmpty()) {
|
||||
String avatarUrl = UrlUtils.getFullUrl(msg.avatar);
|
||||
if (avatarUrl != null && !avatarUrl.isEmpty()) {
|
||||
Log.d(TAG, "加载头像: " + avatarUrl);
|
||||
Glide.with(itemView.getContext())
|
||||
.load(msg.avatar)
|
||||
.load(avatarUrl)
|
||||
.placeholder(R.drawable.ic_person_24)
|
||||
.error(R.drawable.ic_person_24)
|
||||
.circleCrop()
|
||||
|
|
@ -129,8 +142,10 @@ public class ChatRoomMessageAdapter extends RecyclerView.Adapter<RecyclerView.Vi
|
|||
if (content != null) content.setVisibility(View.GONE);
|
||||
if (imageContent != null) {
|
||||
imageContent.setVisibility(View.VISIBLE);
|
||||
String imageUrl = UrlUtils.getFullUrl(msg.imageUrl);
|
||||
Log.d(TAG, "加载图片: " + imageUrl);
|
||||
Glide.with(itemView.getContext())
|
||||
.load(msg.imageUrl)
|
||||
.load(imageUrl)
|
||||
.into(imageContent);
|
||||
}
|
||||
if (voiceContainer != null) voiceContainer.setVisibility(View.GONE);
|
||||
|
|
@ -142,6 +157,16 @@ public class ChatRoomMessageAdapter extends RecyclerView.Adapter<RecyclerView.Vi
|
|||
if (voiceDuration != null) {
|
||||
voiceDuration.setText(msg.voiceDuration + "s");
|
||||
}
|
||||
|
||||
// 设置语音点击播放
|
||||
voiceContainer.setOnClickListener(v -> {
|
||||
String voiceUrl = UrlUtils.getFullUrl(msg.voiceUrl);
|
||||
Log.d(TAG, "播放语音: " + voiceUrl);
|
||||
playVoice(voiceUrl, position, voiceContainer);
|
||||
});
|
||||
|
||||
// 更新播放状态UI
|
||||
updateVoicePlayingState(voiceContainer, position == currentPlayingPosition);
|
||||
}
|
||||
} else {
|
||||
if (content != null) {
|
||||
|
|
@ -152,6 +177,12 @@ public class ChatRoomMessageAdapter extends RecyclerView.Adapter<RecyclerView.Vi
|
|||
if (voiceContainer != null) voiceContainer.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateVoicePlayingState(View container, boolean isPlaying) {
|
||||
if (container != null) {
|
||||
container.setAlpha(isPlaying ? 0.7f : 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SystemViewHolder extends RecyclerView.ViewHolder {
|
||||
|
|
@ -188,4 +219,92 @@ public class ChatRoomMessageAdapter extends RecyclerView.Adapter<RecyclerView.Vi
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放语音消息
|
||||
*/
|
||||
private void playVoice(String voiceUrl, int position, View container) {
|
||||
if (voiceUrl == null || voiceUrl.isEmpty()) {
|
||||
Log.e(TAG, "语音URL为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果正在播放同一条语音,则停止
|
||||
if (currentPlayingPosition == position && mediaPlayer != null && mediaPlayer.isPlaying()) {
|
||||
stopVoice();
|
||||
return;
|
||||
}
|
||||
|
||||
// 停止之前的播放
|
||||
stopVoice();
|
||||
|
||||
try {
|
||||
currentPlayingPosition = position;
|
||||
mediaPlayer = new MediaPlayer();
|
||||
mediaPlayer.setDataSource(voiceUrl);
|
||||
|
||||
mediaPlayer.setOnPreparedListener(mp -> {
|
||||
Log.d(TAG, "语音准备完成,开始播放");
|
||||
mp.start();
|
||||
if (container != null) {
|
||||
container.setAlpha(0.7f);
|
||||
}
|
||||
});
|
||||
|
||||
mediaPlayer.setOnCompletionListener(mp -> {
|
||||
Log.d(TAG, "语音播放完成");
|
||||
stopVoice();
|
||||
if (container != null) {
|
||||
container.setAlpha(1.0f);
|
||||
}
|
||||
notifyItemChanged(position);
|
||||
});
|
||||
|
||||
mediaPlayer.setOnErrorListener((mp, what, extra) -> {
|
||||
Log.e(TAG, "语音播放错误: what=" + what + ", extra=" + extra);
|
||||
stopVoice();
|
||||
if (container != null && container.getContext() != null) {
|
||||
Toast.makeText(container.getContext(), "播放失败", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
mediaPlayer.prepareAsync();
|
||||
Log.d(TAG, "开始准备语音: " + voiceUrl);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "播放语音异常", e);
|
||||
stopVoice();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止语音播放
|
||||
*/
|
||||
private void stopVoice() {
|
||||
if (mediaPlayer != null) {
|
||||
try {
|
||||
if (mediaPlayer.isPlaying()) {
|
||||
mediaPlayer.stop();
|
||||
}
|
||||
mediaPlayer.release();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "停止语音异常", e);
|
||||
}
|
||||
mediaPlayer = null;
|
||||
}
|
||||
int oldPosition = currentPlayingPosition;
|
||||
currentPlayingPosition = -1;
|
||||
if (oldPosition >= 0 && oldPosition < messages.size()) {
|
||||
notifyItemChanged(oldPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源,在Activity销毁时调用
|
||||
*/
|
||||
public void release() {
|
||||
stopVoice();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@ import android.content.ClipData;
|
|||
import android.content.ClipboardManager;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.provider.MediaStore;
|
||||
import android.speech.RecognitionListener;
|
||||
import android.speech.RecognizerIntent;
|
||||
import android.speech.SpeechRecognizer;
|
||||
|
|
@ -20,9 +22,15 @@ import android.text.TextWatcher;
|
|||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
|
@ -39,6 +47,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
|||
import com.bumptech.glide.Glide;
|
||||
import com.example.livestreaming.databinding.ActivityMainBinding;
|
||||
import com.example.livestreaming.databinding.DialogCreateRoomBinding;
|
||||
import com.example.livestreaming.net.FileUploadResponse;
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.textfield.MaterialAutoCompleteTextView;
|
||||
|
|
@ -46,6 +55,7 @@ import com.google.android.material.textfield.TextInputLayout;
|
|||
import com.example.livestreaming.net.ApiClient;
|
||||
import com.example.livestreaming.net.ApiResponse;
|
||||
import com.example.livestreaming.net.ApiService;
|
||||
import com.example.livestreaming.net.ChatRoomResponse;
|
||||
import com.example.livestreaming.net.CommunityResponse;
|
||||
import com.example.livestreaming.net.ConversationResponse;
|
||||
import com.example.livestreaming.net.CreateRoomRequest;
|
||||
|
|
@ -54,14 +64,21 @@ import com.example.livestreaming.net.Room;
|
|||
import com.example.livestreaming.net.StreamConfig;
|
||||
import com.example.livestreaming.net.WorksResponse;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
|
@ -112,6 +129,13 @@ public class MainActivity extends AppCompatActivity {
|
|||
private boolean isFetching;
|
||||
private long lastFetchMs;
|
||||
|
||||
// 聊天室封面上传相关
|
||||
private Uri chatRoomCoverUri;
|
||||
private String chatRoomCoverUrl;
|
||||
private ImageView chatRoomCoverImageView;
|
||||
private View chatRoomCoverPlaceholder;
|
||||
private ActivityResultLauncher<Intent> chatRoomCoverPickerLauncher;
|
||||
|
||||
private static final int REQUEST_RECORD_AUDIO_PERMISSION = 200;
|
||||
private static final int REQUEST_LOCATION_PERMISSION = 201;
|
||||
private SpeechRecognizer speechRecognizer;
|
||||
|
|
@ -140,6 +164,9 @@ public class MainActivity extends AppCompatActivity {
|
|||
Log.d(TAG, "BuildConfig DEVICE: " + com.example.livestreaming.BuildConfig.API_BASE_URL_DEVICE);
|
||||
Log.d(TAG, "==============================");
|
||||
|
||||
// 初始化聊天室封面选择器
|
||||
setupChatRoomCoverPicker();
|
||||
|
||||
// 立即显示缓存数据,提升启动速度
|
||||
setupRecyclerView();
|
||||
setupUI();
|
||||
|
|
@ -1136,17 +1163,20 @@ public class MainActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void showCreateRoomDialogInternal() {
|
||||
// 显示选择对话框:手机开播 或 OBS推流
|
||||
// 显示选择对话框:手机开播、OBS推流 或 创建聊天室
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("选择开播方式")
|
||||
.setItems(new String[]{"📱 手机开播", "💻 OBS推流"}, (dialog, which) -> {
|
||||
.setItems(new String[]{"📱 手机开播", "💻 OBS推流", "💬 创建聊天室"}, (dialog, which) -> {
|
||||
if (which == 0) {
|
||||
// 手机开播 - 跳转到 BroadcastActivity
|
||||
Intent intent = new Intent(MainActivity.this, BroadcastActivity.class);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
} else if (which == 1) {
|
||||
// OBS推流 - 显示创建直播间对话框
|
||||
showOBSCreateRoomDialog();
|
||||
} else {
|
||||
// 创建聊天室
|
||||
showCreateChatRoomDialog();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
|
|
@ -1339,6 +1369,266 @@ public class MainActivity extends AppCompatActivity {
|
|||
dialog.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示创建聊天室对话框
|
||||
*/
|
||||
private void showCreateChatRoomDialog() {
|
||||
View dialogView = getLayoutInflater().inflate(R.layout.dialog_create_chatroom, null);
|
||||
|
||||
EditText titleEdit = dialogView.findViewById(R.id.chatroom_title_edit);
|
||||
EditText descEdit = dialogView.findViewById(R.id.chatroom_desc_edit);
|
||||
RadioGroup typeGroup = dialogView.findViewById(R.id.chatroom_type_group);
|
||||
EditText feeEdit = dialogView.findViewById(R.id.chatroom_fee_edit);
|
||||
EditText maxMembersEdit = dialogView.findViewById(R.id.chatroom_max_members_edit);
|
||||
View feeLayout = dialogView.findViewById(R.id.chatroom_fee_layout);
|
||||
|
||||
// 封面相关视图
|
||||
FrameLayout coverContainer = dialogView.findViewById(R.id.chatroom_cover_container);
|
||||
chatRoomCoverImageView = dialogView.findViewById(R.id.chatroom_cover_image);
|
||||
chatRoomCoverPlaceholder = dialogView.findViewById(R.id.chatroom_cover_placeholder);
|
||||
|
||||
// 重置封面状态
|
||||
chatRoomCoverUri = null;
|
||||
chatRoomCoverUrl = null;
|
||||
if (chatRoomCoverImageView != null) {
|
||||
chatRoomCoverImageView.setVisibility(View.GONE);
|
||||
}
|
||||
if (chatRoomCoverPlaceholder != null) {
|
||||
chatRoomCoverPlaceholder.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
// 设置封面点击事件
|
||||
if (coverContainer != null) {
|
||||
coverContainer.setOnClickListener(v -> {
|
||||
// 打开图片选择器
|
||||
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
||||
intent.setType("image/*");
|
||||
chatRoomCoverPickerLauncher.launch(intent);
|
||||
});
|
||||
}
|
||||
|
||||
// 默认隐藏费用输入
|
||||
feeLayout.setVisibility(View.GONE);
|
||||
|
||||
// 监听房间类型切换
|
||||
typeGroup.setOnCheckedChangeListener((group, checkedId) -> {
|
||||
if (checkedId == R.id.radio_paid) {
|
||||
feeLayout.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
feeLayout.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(this)
|
||||
.setTitle("💬 创建聊天室")
|
||||
.setView(dialogView)
|
||||
.setNegativeButton("取消", null)
|
||||
.setPositiveButton("创建", null)
|
||||
.create();
|
||||
|
||||
dialog.setOnShowListener(d -> {
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
|
||||
String title = titleEdit.getText().toString().trim();
|
||||
String desc = descEdit.getText().toString().trim();
|
||||
int roomType = typeGroup.getCheckedRadioButtonId() == R.id.radio_paid ? 1 : 0;
|
||||
String feeStr = feeEdit.getText().toString().trim();
|
||||
String maxMembersStr = maxMembersEdit.getText().toString().trim();
|
||||
|
||||
// 验证
|
||||
if (TextUtils.isEmpty(title)) {
|
||||
titleEdit.setError("请输入聊天室名称");
|
||||
return;
|
||||
}
|
||||
|
||||
int entryFee = 0;
|
||||
if (roomType == 1) {
|
||||
if (TextUtils.isEmpty(feeStr)) {
|
||||
feeEdit.setError("请输入入场费用");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
entryFee = Integer.parseInt(feeStr);
|
||||
if (entryFee <= 0) {
|
||||
feeEdit.setError("费用必须大于0");
|
||||
return;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
feeEdit.setError("请输入有效数字");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int maxMembers = 24;
|
||||
if (!TextUtils.isEmpty(maxMembersStr)) {
|
||||
try {
|
||||
maxMembers = Integer.parseInt(maxMembersStr);
|
||||
if (maxMembers < 2 || maxMembers > 24) {
|
||||
maxMembersEdit.setError("人数范围2-24");
|
||||
return;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
maxMembersEdit.setError("请输入有效数字");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 构建请求
|
||||
Map<String, Object> request = new HashMap<>();
|
||||
request.put("roomTitle", title);
|
||||
request.put("roomDescription", desc);
|
||||
request.put("roomType", roomType);
|
||||
request.put("entryFee", entryFee);
|
||||
request.put("roomImage", chatRoomCoverUrl != null ? chatRoomCoverUrl : "");
|
||||
request.put("maxMembers", maxMembers);
|
||||
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
|
||||
ApiClient.getService(getApplicationContext()).createChatRoom(request)
|
||||
.enqueue(new Callback<ApiResponse<ChatRoomResponse>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<ChatRoomResponse>> call,
|
||||
Response<ApiResponse<ChatRoomResponse>> response) {
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
|
||||
|
||||
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
|
||||
ChatRoomResponse chatRoom = response.body().getData();
|
||||
dialog.dismiss();
|
||||
Toast.makeText(MainActivity.this, "聊天室创建成功", Toast.LENGTH_SHORT).show();
|
||||
|
||||
// 刷新列表
|
||||
fetchDiscoverRooms();
|
||||
|
||||
// 直接进入聊天室
|
||||
if (chatRoom != null && chatRoom.getId() != null) {
|
||||
ChatRoomDetailActivity.start(MainActivity.this, chatRoom.getId());
|
||||
}
|
||||
} else {
|
||||
String errorMsg = "创建失败";
|
||||
if (response.body() != null && response.body().getMessage() != null) {
|
||||
errorMsg = response.body().getMessage();
|
||||
}
|
||||
Toast.makeText(MainActivity.this, errorMsg, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<ChatRoomResponse>> call, Throwable t) {
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
|
||||
Toast.makeText(MainActivity.this, "网络错误: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化聊天室封面选择器
|
||||
*/
|
||||
private void setupChatRoomCoverPicker() {
|
||||
chatRoomCoverPickerLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == RESULT_OK && result.getData() != null) {
|
||||
Uri selectedImageUri = result.getData().getData();
|
||||
if (selectedImageUri != null) {
|
||||
chatRoomCoverUri = selectedImageUri;
|
||||
// 显示选中的图片
|
||||
if (chatRoomCoverImageView != null) {
|
||||
chatRoomCoverImageView.setVisibility(View.VISIBLE);
|
||||
Glide.with(this)
|
||||
.load(selectedImageUri)
|
||||
.centerCrop()
|
||||
.into(chatRoomCoverImageView);
|
||||
}
|
||||
if (chatRoomCoverPlaceholder != null) {
|
||||
chatRoomCoverPlaceholder.setVisibility(View.GONE);
|
||||
}
|
||||
// 上传图片
|
||||
uploadChatRoomCover(selectedImageUri);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传聊天室封面图片
|
||||
*/
|
||||
private void uploadChatRoomCover(Uri imageUri) {
|
||||
try {
|
||||
// 从Uri获取文件
|
||||
File imageFile = getFileFromUri(imageUri);
|
||||
if (imageFile == null) {
|
||||
Toast.makeText(this, "无法读取图片文件", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建RequestBody
|
||||
RequestBody requestFile = RequestBody.create(MediaType.parse("image/*"), imageFile);
|
||||
MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", imageFile.getName(), requestFile);
|
||||
RequestBody modelPart = RequestBody.create(MediaType.parse("text/plain"), "chatroom");
|
||||
RequestBody pidPart = RequestBody.create(MediaType.parse("text/plain"), "0");
|
||||
|
||||
// 调用上传接口
|
||||
ApiClient.getService(getApplicationContext()).uploadImage(filePart, modelPart, pidPart)
|
||||
.enqueue(new Callback<ApiResponse<FileUploadResponse>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<FileUploadResponse>> call,
|
||||
Response<ApiResponse<FileUploadResponse>> response) {
|
||||
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
|
||||
FileUploadResponse uploadResponse = response.body().getData();
|
||||
if (uploadResponse != null && uploadResponse.getUrl() != null) {
|
||||
chatRoomCoverUrl = uploadResponse.getUrl();
|
||||
Log.d(TAG, "聊天室封面上传成功: " + chatRoomCoverUrl);
|
||||
Toast.makeText(MainActivity.this, "封面上传成功", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "聊天室封面上传失败");
|
||||
Toast.makeText(MainActivity.this, "封面上传失败,请重试", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<FileUploadResponse>> call, Throwable t) {
|
||||
Log.e(TAG, "聊天室封面上传网络错误: " + t.getMessage());
|
||||
Toast.makeText(MainActivity.this, "网络错误,封面上传失败", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "上传封面异常: " + e.getMessage());
|
||||
Toast.makeText(this, "上传封面失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Uri获取File对象
|
||||
*/
|
||||
private File getFileFromUri(Uri uri) {
|
||||
try {
|
||||
InputStream inputStream = getContentResolver().openInputStream(uri);
|
||||
if (inputStream == null) return null;
|
||||
|
||||
// 创建临时文件
|
||||
File tempFile = new File(getCacheDir(), "chatroom_cover_" + System.currentTimeMillis() + ".jpg");
|
||||
FileOutputStream outputStream = new FileOutputStream(tempFile);
|
||||
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
outputStream.close();
|
||||
inputStream.close();
|
||||
|
||||
return tempFile;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "从Uri获取文件失败: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void showOptimizedStreamInfo(Room room) {
|
||||
String streamKey = room != null ? room.getStreamKey() : null;
|
||||
String rtmp = room != null && room.getStreamUrls() != null ? room.getStreamUrls().getRtmp() : null;
|
||||
|
|
|
|||
|
|
@ -1059,4 +1059,20 @@ public interface ApiService {
|
|||
Call<ApiResponse<PageResponse<Map<String, Object>>>> getMyChatRoomEntryRecords(
|
||||
@Query("page") int page,
|
||||
@Query("limit") int limit);
|
||||
|
||||
/**
|
||||
* 语音上传
|
||||
*/
|
||||
@Multipart
|
||||
@POST("api/front/upload/chat/voice")
|
||||
Call<ApiResponse<FileUploadResponse>> uploadVoice(
|
||||
@Part MultipartBody.Part file,
|
||||
@Part("model") RequestBody model,
|
||||
@Part("pid") RequestBody pid);
|
||||
|
||||
/**
|
||||
* 创建聊天室
|
||||
*/
|
||||
@POST("api/front/chatroom/create")
|
||||
Call<ApiResponse<ChatRoomResponse>> createChatRoom(@Body Map<String, Object> request);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
package com.example.livestreaming.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.example.livestreaming.net.ApiClient;
|
||||
|
||||
/**
|
||||
* URL工具类,处理相对路径和完整URL的转换
|
||||
*/
|
||||
public class UrlUtils {
|
||||
|
||||
// 文件服务器基础URL
|
||||
private static final String FILE_SERVER_BASE = "http://1.15.149.240";
|
||||
|
||||
/**
|
||||
* 获取完整的文件URL
|
||||
* 如果是相对路径,则补全为完整URL
|
||||
*/
|
||||
public static String getFullUrl(String url) {
|
||||
if (TextUtils.isEmpty(url)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 已经是完整URL
|
||||
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// 相对路径,补全为完整URL
|
||||
if (url.startsWith("/")) {
|
||||
return FILE_SERVER_BASE + url;
|
||||
}
|
||||
|
||||
// 没有斜杠开头的相对路径
|
||||
return FILE_SERVER_BASE + "/" + url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取完整的文件URL(使用API服务器地址)
|
||||
*/
|
||||
public static String getFullUrl(Context context, String url) {
|
||||
if (TextUtils.isEmpty(url)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 已经是完整URL
|
||||
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// 使用API服务器地址
|
||||
String baseUrl = ApiClient.getCurrentBaseUrl(context);
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
|
||||
}
|
||||
|
||||
// 相对路径,补全为完整URL
|
||||
if (url.startsWith("/")) {
|
||||
return baseUrl + url;
|
||||
}
|
||||
|
||||
return baseUrl + "/" + url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查URL是否有效
|
||||
*/
|
||||
public static boolean isValidUrl(String url) {
|
||||
if (TextUtils.isEmpty(url)) {
|
||||
return false;
|
||||
}
|
||||
return url.startsWith("http://") || url.startsWith("https://");
|
||||
}
|
||||
}
|
||||
11
android-app/app/src/main/res/drawable/bg_dashed_border.xml
Normal file
11
android-app/app/src/main/res/drawable/bg_dashed_border.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="8dp" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#CCCCCC"
|
||||
android:dashWidth="4dp"
|
||||
android:dashGap="2dp" />
|
||||
<solid android:color="#F5F5F5" />
|
||||
</shape>
|
||||
10
android-app/app/src/main/res/drawable/ic_add_photo.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_add_photo.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?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="#757575"
|
||||
android:pathData="M19,7v2.99s-1.99,0.01 -2,0L17,7h-3s0.01,-1.99 0,-2h3L17,2h2v3h3v2h-3zM16,11L16,8h-3L13,5L5,5c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-8h-3zM5,19l3,-4 2,3 3,-4 4,5L5,19z"/>
|
||||
</vector>
|
||||
168
android-app/app/src/main/res/layout/dialog_create_chatroom.xml
Normal file
168
android-app/app/src/main/res/layout/dialog_create_chatroom.xml
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<!-- 封面图片选择 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="聊天室封面"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/chatroom_cover_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="120dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@drawable/bg_dashed_border"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/chatroom_cover_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/chatroom_cover_placeholder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:src="@drawable/ic_add_photo"
|
||||
android:tint="@android:color/darker_gray" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="点击上传封面"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<!-- 聊天室名称 -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="聊天室名称 *"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/chatroom_title_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLength="20"
|
||||
android:inputType="text" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- 聊天室描述 -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="聊天室描述(可选)"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/chatroom_desc_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLength="100"
|
||||
android:inputType="textMultiLine"
|
||||
android:minLines="2"
|
||||
android:maxLines="3" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- 房间类型 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="房间类型"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/chatroom_type_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radio_free"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="免费"
|
||||
android:checked="true" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radio_paid"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="收费" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
<!-- 入场费用(收费时显示) -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/chatroom_fee_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="入场费用(金币)"
|
||||
android:visibility="gone"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/chatroom_fee_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- 最大人数 -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="最大人数(2-24,默认24)"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/chatroom_max_members_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number"
|
||||
android:text="24" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
Loading…
Reference in New Issue
Block a user