From d19d0cc631d40e06dab73806e463c6a32b5816a1 Mon Sep 17 00:00:00 2001 From: xiao12feng8 <16507319+xiao12feng8@user.noreply.gitee.com> Date: Fri, 9 Jan 2026 14:45:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=BB=E9=A2=98=EF=BC=9A=E7=9B=B4=E6=92=AD?= =?UTF-8?q?=E5=92=8C=E4=BD=9C=E5=93=81=E6=8C=89=E9=92=AE=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android-app/app/src/main/AndroidManifest.xml | 5 + .../example/livestreaming/MainActivity.java | 3 +- .../livestreaming/ProfileActivity.java | 6 +- .../livestreaming/PublishCenterActivity.java | 373 +++++++++++++ .../example/livestreaming/WorksAdapter.java | 34 +- .../main/res/drawable/bg_dashed_border.xml | 2 +- .../app/src/main/res/drawable/bg_spinner.xml | 9 + .../main/res/drawable/ic_close_circle_24.xml | 10 + .../res/layout/activity_publish_center.xml | 494 ++++++++++++++++++ .../app/src/main/res/layout/item_works.xml | 26 +- 10 files changed, 951 insertions(+), 11 deletions(-) create mode 100644 android-app/app/src/main/java/com/example/livestreaming/PublishCenterActivity.java create mode 100644 android-app/app/src/main/res/drawable/bg_spinner.xml create mode 100644 android-app/app/src/main/res/drawable/ic_close_circle_24.xml create mode 100644 android-app/app/src/main/res/layout/activity_publish_center.xml diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml index 256be8a3..539128e7 100644 --- a/android-app/app/src/main/AndroidManifest.xml +++ b/android-app/app/src/main/AndroidManifest.xml @@ -197,6 +197,11 @@ android:name="com.example.livestreaming.UserProfileActivity" android:exported="false" /> + + diff --git a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java index c03b56ef..33731ff1 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java @@ -609,7 +609,8 @@ public class MainActivity extends AppCompatActivity { binding.fabAddLive.setOnClickListener(new DebounceClickListener() { @Override public void onDebouncedClick(View v) { - showCreateRoomDialog(); + // 跳转到发布中心页面 + PublishCenterActivity.start(MainActivity.this); } }); diff --git a/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java b/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java index 36fb9673..bfa93d29 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java @@ -539,7 +539,7 @@ public class ProfileActivity extends AppCompatActivity { if (!AuthHelper.requireLogin(this, "发布作品需要登录")) { return; } - PublishWorkActivity.start(this); + PublishCenterActivity.start(this); }); // 悬浮按钮(固定显示) @@ -548,7 +548,7 @@ public class ProfileActivity extends AppCompatActivity { if (!AuthHelper.requireLogin(this, "发布作品需要登录")) { return; } - PublishWorkActivity.start(this); + PublishCenterActivity.start(this); }); binding.likedGoBrowseBtn.setOnClickListener(v -> startActivity(new Intent(this, MainActivity.class))); binding.favGoBrowseBtn.setOnClickListener(v -> startActivity(new Intent(this, MainActivity.class))); @@ -578,7 +578,7 @@ public class ProfileActivity extends AppCompatActivity { if (!AuthHelper.requireLogin(this, "发布作品需要登录")) { return; } - PublishWorkActivity.start(this); + PublishCenterActivity.start(this); }); loadMyWorks(); diff --git a/android-app/app/src/main/java/com/example/livestreaming/PublishCenterActivity.java b/android-app/app/src/main/java/com/example/livestreaming/PublishCenterActivity.java new file mode 100644 index 00000000..40eea510 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/PublishCenterActivity.java @@ -0,0 +1,373 @@ +package com.example.livestreaming; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.net.Uri; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.AppCompatActivity; + +import com.bumptech.glide.Glide; +import com.example.livestreaming.databinding.ActivityPublishCenterBinding; +import com.example.livestreaming.net.ApiClient; +import com.example.livestreaming.net.ApiResponse; +import com.example.livestreaming.net.CategoryResponse; +import com.example.livestreaming.net.CreateRoomRequest; +import com.example.livestreaming.net.FileUploadResponse; +import com.example.livestreaming.net.Room; +import com.example.livestreaming.net.StreamUrls; +import com.example.livestreaming.net.WorksRequest; +import com.google.android.material.tabs.TabLayout; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class PublishCenterActivity extends AppCompatActivity { + + private ActivityPublishCenterBinding binding; + private int currentTab = 0; + private List categoryList = new ArrayList<>(); + private int selectedCategoryId = -1; + private Uri dynamicCoverUri = null; + private ActivityResultLauncher imagePickerLauncher; + + public static void start(Context context) { + context.startActivity(new Intent(context, PublishCenterActivity.class)); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (!AuthHelper.requireLogin(this, "发布内容需要登录")) { + finish(); + return; + } + binding = ActivityPublishCenterBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + setupImagePicker(); + setupToolbar(); + setupTabs(); + setupClickListeners(); + loadCategories(); + showTab(0); + } + + private void setupImagePicker() { + imagePickerLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { + Uri uri = result.getData().getData(); + if (uri != null) { + dynamicCoverUri = uri; + showDynamicCover(uri); + } + } + } + ); + } + + private void setupToolbar() { + binding.btnBack.setOnClickListener(v -> finish()); + } + + private void setupTabs() { + binding.tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { showTab(tab.getPosition()); } + @Override + public void onTabUnselected(TabLayout.Tab tab) {} + @Override + public void onTabReselected(TabLayout.Tab tab) {} + }); + } + + private void setupClickListeners() { + binding.btnStartLive.setOnClickListener(v -> startMobileLive()); + binding.btnPublishDynamic.setOnClickListener(v -> publishDynamic()); + binding.dynamicCoverContainer.setOnClickListener(v -> pickDynamicCover()); + binding.btnRemoveDynamicCover.setOnClickListener(v -> removeDynamicCover()); + binding.btnPublishWork.setOnClickListener(v -> PublishWorkActivity.start(this)); + binding.btnCopyStreamUrl.setOnClickListener(v -> copyToClipboard("推流地址", binding.tvStreamUrl.getText().toString())); + binding.btnCopyStreamKey.setOnClickListener(v -> copyToClipboard("推流码", binding.tvStreamKey.getText().toString())); + binding.btnGetStreamInfo.setOnClickListener(v -> createPcLiveRoom()); + } + + private void showTab(int position) { + currentTab = position; + binding.contentMobileLive.setVisibility(View.GONE); + binding.contentDynamic.setVisibility(View.GONE); + binding.contentWork.setVisibility(View.GONE); + binding.contentPcLive.setVisibility(View.GONE); + switch (position) { + case 0: binding.contentMobileLive.setVisibility(View.VISIBLE); break; + case 1: binding.contentDynamic.setVisibility(View.VISIBLE); break; + case 2: binding.contentWork.setVisibility(View.VISIBLE); break; + case 3: binding.contentPcLive.setVisibility(View.VISIBLE); break; + } + } + + private void loadCategories() { + ApiClient.getService(this).getLiveRoomCategories().enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, Response>> response) { + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + categoryList = response.body().getData(); + if (categoryList == null) categoryList = new ArrayList<>(); + } + setupCategorySpinner(); + } + @Override + public void onFailure(Call>> call, Throwable t) { + setupCategorySpinner(); + } + }); + } + + private void setupCategorySpinner() { + List names = new ArrayList<>(); + names.add("请选择分类"); + for (CategoryResponse cat : categoryList) names.add(cat.getName()); + ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, names); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + binding.spinnerPcCategory.setAdapter(adapter); + binding.spinnerPcCategory.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(android.widget.AdapterView parent, View view, int pos, long id) { + selectedCategoryId = (pos > 0 && pos <= categoryList.size()) ? categoryList.get(pos - 1).getId() : -1; + } + @Override + public void onNothingSelected(android.widget.AdapterView parent) { selectedCategoryId = -1; } + }); + } + + private void startMobileLive() { + String title = binding.etLiveTitle.getText().toString().trim(); + if (TextUtils.isEmpty(title)) title = "我的直播间"; + Intent intent = new Intent(this, BroadcastActivity.class); + intent.putExtra("title", title); + startActivity(intent); + finish(); + } + + private void pickDynamicCover() { + Intent intent = new Intent(Intent.ACTION_PICK); + intent.setType("image/*"); + imagePickerLauncher.launch(intent); + } + + private void showDynamicCover(Uri uri) { + binding.ivDynamicCover.setVisibility(View.VISIBLE); + binding.dynamicCoverPlaceholder.setVisibility(View.GONE); + binding.btnRemoveDynamicCover.setVisibility(View.VISIBLE); + Glide.with(this).load(uri).centerCrop().into(binding.ivDynamicCover); + } + + private void removeDynamicCover() { + dynamicCoverUri = null; + binding.ivDynamicCover.setVisibility(View.GONE); + binding.dynamicCoverPlaceholder.setVisibility(View.VISIBLE); + binding.btnRemoveDynamicCover.setVisibility(View.GONE); + } + + private void publishDynamic() { + String content = binding.etDynamicContent.getText().toString().trim(); + if (TextUtils.isEmpty(content)) { + Toast.makeText(this, "请输入动态内容", Toast.LENGTH_SHORT).show(); + return; + } + if (content.length() > 1000) { + Toast.makeText(this, "内容不能超过1000字", Toast.LENGTH_SHORT).show(); + return; + } + binding.btnPublishDynamic.setEnabled(false); + binding.btnPublishDynamic.setText("发布中..."); + + if (dynamicCoverUri != null) { + // 用户选择了封面,上传后发布 + uploadDynamicCoverAndPublish(content); + } else { + // 用户没选封面,使用特殊标记(纯文字动态) + // 使用 "text_only" 作为标记,在展示时识别为纯文字动态 + doPublishDynamic(content, "text_only"); + } + } + + private void uploadDynamicCoverAndPublish(String content) { + try { + File file = uriToFile(dynamicCoverUri); + if (file == null) { + Toast.makeText(this, "无法读取图片", Toast.LENGTH_SHORT).show(); + resetDynamicButton(); + return; + } + RequestBody requestFile = RequestBody.create(MediaType.parse("image/*"), file); + MultipartBody.Part body = MultipartBody.Part.createFormData("file", file.getName(), requestFile); + RequestBody model = RequestBody.create(MediaType.parse("text/plain"), "work"); + RequestBody pid = RequestBody.create(MediaType.parse("text/plain"), "0"); + ApiClient.getService(this).uploadImage(body, model, pid).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + FileUploadResponse data = response.body().getData(); + doPublishDynamic(content, data != null ? data.getUrl() : null); + } else { + resetDynamicButton(); + Toast.makeText(PublishCenterActivity.this, "封面上传失败", Toast.LENGTH_SHORT).show(); + } + } + @Override + public void onFailure(Call> call, Throwable t) { + resetDynamicButton(); + Toast.makeText(PublishCenterActivity.this, "网络错误", Toast.LENGTH_SHORT).show(); + } + }); + } catch (Exception e) { + resetDynamicButton(); + Toast.makeText(this, "图片处理失败", Toast.LENGTH_SHORT).show(); + } + } + + private void doPublishDynamic(String content, String coverUrl) { + WorksRequest request = new WorksRequest(); + request.setTitle(""); + request.setDescription(content); + request.setType("IMAGE"); + request.setImageUrls(new ArrayList<>()); + request.setCoverUrl(coverUrl != null ? coverUrl : ""); + ApiClient.getService(this).publishWork(request).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + resetDynamicButton(); + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + Toast.makeText(PublishCenterActivity.this, "动态发布成功", Toast.LENGTH_SHORT).show(); + binding.etDynamicContent.setText(""); + removeDynamicCover(); + finish(); + } else { + String msg = response.body() != null ? response.body().getMessage() : "发布失败"; + Toast.makeText(PublishCenterActivity.this, msg, Toast.LENGTH_SHORT).show(); + } + } + @Override + public void onFailure(Call> call, Throwable t) { + resetDynamicButton(); + Toast.makeText(PublishCenterActivity.this, "网络错误", Toast.LENGTH_SHORT).show(); + } + }); + } + + private void resetDynamicButton() { + binding.btnPublishDynamic.setEnabled(true); + binding.btnPublishDynamic.setText("发布动态"); + } + + private File uriToFile(Uri uri) { + try { + InputStream is = getContentResolver().openInputStream(uri); + if (is == null) return null; + File file = new File(getCacheDir(), "temp_cover_" + System.currentTimeMillis() + ".jpg"); + FileOutputStream os = new FileOutputStream(file); + byte[] buf = new byte[4096]; + int len; + while ((len = is.read(buf)) != -1) os.write(buf, 0, len); + is.close(); + os.close(); + return file; + } catch (Exception e) { + return null; + } + } + + private void createPcLiveRoom() { + String title = binding.etPcLiveTitle.getText().toString().trim(); + if (TextUtils.isEmpty(title)) { + Toast.makeText(this, "请输入直播标题", Toast.LENGTH_SHORT).show(); + return; + } + if (selectedCategoryId <= 0) { + Toast.makeText(this, "请选择直播分类", Toast.LENGTH_SHORT).show(); + return; + } + binding.btnGetStreamInfo.setEnabled(false); + binding.btnGetStreamInfo.setText("创建中..."); + binding.streamInfoLoading.setVisibility(View.VISIBLE); + + // 获取当前用户昵称作为主播名称 + String streamerName = com.example.livestreaming.net.AuthStore.getNickname(this); + if (TextUtils.isEmpty(streamerName)) { + streamerName = title; // 如果没有昵称,使用标题 + } + + CreateRoomRequest request = new CreateRoomRequest(title, streamerName, "pc"); + request.setCategoryId(selectedCategoryId); + + ApiClient.getService(this).createRoom(request).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + binding.btnGetStreamInfo.setEnabled(true); + binding.btnGetStreamInfo.setText("创建直播间并获取推流信息"); + binding.streamInfoLoading.setVisibility(View.GONE); + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + Room room = response.body().getData(); + if (room != null) { + showStreamInfo(room); + } else { + Toast.makeText(PublishCenterActivity.this, "创建成功但未获取到推流信息", Toast.LENGTH_SHORT).show(); + } + } else { + String msg = response.body() != null ? response.body().getMessage() : "创建直播间失败"; + Toast.makeText(PublishCenterActivity.this, msg, Toast.LENGTH_SHORT).show(); + } + } + @Override + public void onFailure(Call> call, Throwable t) { + binding.btnGetStreamInfo.setEnabled(true); + binding.btnGetStreamInfo.setText("创建直播间并获取推流信息"); + binding.streamInfoLoading.setVisibility(View.GONE); + Toast.makeText(PublishCenterActivity.this, "网络错误", Toast.LENGTH_SHORT).show(); + } + }); + } + + private void showStreamInfo(Room room) { + binding.streamInfoContent.setVisibility(View.VISIBLE); + String streamKey = room.getStreamKey(); + StreamUrls streamUrls = room.getStreamUrls(); + String pushUrl = (streamUrls != null && streamUrls.getRtmp() != null) ? streamUrls.getRtmp() : ""; + binding.tvStreamUrl.setText(!TextUtils.isEmpty(pushUrl) ? pushUrl : "未获取到推流地址"); + binding.tvStreamKey.setText(!TextUtils.isEmpty(streamKey) ? streamKey : "未获取到推流码"); + Toast.makeText(this, "直播间创建成功!", Toast.LENGTH_SHORT).show(); + } + + private void copyToClipboard(String label, String text) { + if (TextUtils.isEmpty(text) || text.startsWith("未获取")) { + Toast.makeText(this, "内容为空", Toast.LENGTH_SHORT).show(); + return; + } + ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + if (clipboard != null) { + clipboard.setPrimaryClip(ClipData.newPlainText(label, text)); + Toast.makeText(this, label + "已复制", Toast.LENGTH_SHORT).show(); + } + } +} diff --git a/android-app/app/src/main/java/com/example/livestreaming/WorksAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/WorksAdapter.java index c9b9c577..61762602 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/WorksAdapter.java +++ b/android-app/app/src/main/java/com/example/livestreaming/WorksAdapter.java @@ -59,7 +59,10 @@ public class WorksAdapter extends RecyclerView.Adapter - + diff --git a/android-app/app/src/main/res/drawable/bg_spinner.xml b/android-app/app/src/main/res/drawable/bg_spinner.xml new file mode 100644 index 00000000..7fc6e3ad --- /dev/null +++ b/android-app/app/src/main/res/drawable/bg_spinner.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/android-app/app/src/main/res/drawable/ic_close_circle_24.xml b/android-app/app/src/main/res/drawable/ic_close_circle_24.xml new file mode 100644 index 00000000..be19842c --- /dev/null +++ b/android-app/app/src/main/res/drawable/ic_close_circle_24.xml @@ -0,0 +1,10 @@ + + + + diff --git a/android-app/app/src/main/res/layout/activity_publish_center.xml b/android-app/app/src/main/res/layout/activity_publish_center.xml new file mode 100644 index 00000000..57a868f4 --- /dev/null +++ b/android-app/app/src/main/res/layout/activity_publish_center.xml @@ -0,0 +1,494 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/item_works.xml b/android-app/app/src/main/res/layout/item_works.xml index 3e541fd4..abdb06d0 100644 --- a/android-app/app/src/main/res/layout/item_works.xml +++ b/android-app/app/src/main/res/layout/item_works.xml @@ -12,8 +12,9 @@ android:layout_height="wrap_content" android:orientation="vertical"> - + @@ -49,6 +50,29 @@ + + + + + + +