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 @@
+
+
+
+
+
+
+