Send Audio file

This commit is contained in:
2025-09-08 09:48:54 +03:30
parent 4d7cfecd05
commit a4e8ee76ad
5 changed files with 291 additions and 15 deletions
+1
View File
@@ -16,6 +16,7 @@ module org.to.telegramfinalproject {
requires javax.servlet.api;
requires mp3agic;
requires jdk.internal.le;
requires javafx.media;
// FXML کنترلرها در این پکیج‌اند:
opens org.to.telegramfinalproject.UI to javafx.fxml;
@@ -19,10 +19,13 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.shape.Circle;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.json.JSONArray;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
@@ -33,6 +36,7 @@ import org.to.telegramfinalproject.Models.ChatEntry;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
@@ -583,7 +587,6 @@ public class ChatPageController {
String name = f.getName().toLowerCase();
if (name.matches(".*\\.(png|jpg|jpeg|gif|bmp|webp)$")) return "IMAGE";
if (name.matches(".*\\.(mp3|wav|m4a|ogg|aac)$")) return "AUDIO";
// اگر خواستی دقیق‌تر: با Files.probeContentType هم تست کن
return null;
}
@@ -816,11 +819,13 @@ public class ChatPageController {
leaveGroupItem.setOnAction(e -> onLeaveGroupMenuClicked(entry));
}
case "channel" -> {
archiveItem.setVisible(true);
viewChannelItem.setVisible(true);
leaveChannelItem.setVisible(true);
archiveItem.setOnAction(e -> toggleArchive(entry));
viewChannelItem.setOnAction(e -> openInfoScene(entry));
leaveChannelItem.setOnAction(e -> leaveChannelButton(entry));
leaveChannelItem.setOnAction(e -> onLeaveChannelMenuClicked(entry));
}
}
@@ -972,7 +977,8 @@ public class ChatPageController {
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
// TODO: close chat + refresh sidebar
MainController.getInstance().refreshChatListUI();
AppRouter.showMain();
MainController.getInstance().closeCurrentChat();
}
}
@@ -1236,15 +1242,13 @@ public class ChatPageController {
bubble.getChildren().add(imageNode);
} else if ("AUDIO".equals(t)) {
Label ph = new Label("🎵 Audio");
ph.setWrapText(true);
String bg = outgoing ? mine : theirs;
ph.setStyle("-fx-background-color:" + bg + ";" +
"-fx-padding:8 12;" +
"-fx-background-radius:12;" +
"-fx-max-width: 520;");
bubble.getChildren().add(ph);
final MediaPathResolver resolver =
new MediaPathResolver(Paths.get(System.getProperty("user.dir"), "uploads"));
String playable = resolver.toFileUri(fileUrl);
Node audioNode = buildAudioNodeLocal(playable, content);
bubble.getChildren().add(audioNode);
} else {
Label ph = new Label("[" + t + "]");
ph.setWrapText(true);
@@ -1679,6 +1683,114 @@ public class ChatPageController {
}
}
}
// imports لازم:
// import javafx.scene.media.Media;
// import javafx.scene.media.MediaPlayer;
// import javafx.util.Duration;
private Node buildAudioNodeLocal(String localPathOrUri, String caption) {
HBox root = new HBox(8);
root.setAlignment(Pos.CENTER_LEFT);
Button btn = new Button("");
btn.setMinWidth(32);
Slider seek = new Slider(0, 1, 0);
seek.setPrefWidth(180);
Label time = new Label("0:00");
VBox right = new VBox(4, new HBox(8, btn, seek, time));
if (caption != null && !caption.isBlank()) {
Label cap = new Label(caption);
cap.setWrapText(true);
right.getChildren().add(cap);
}
root.getChildren().add(right);
// مسیر را به file URI تبدیل کن
String playable = toFileUri(localPathOrUri);
if (playable == null) {
btn.setDisable(true); seek.setDisable(true); time.setText("no audio");
return root;
}
// قبل از ساخت Media، وجود فایل را چک کن
try {
java.net.URI uri = java.net.URI.create(playable);
java.nio.file.Path p = java.nio.file.Paths.get(uri);
if (!java.nio.file.Files.exists(p)) {
btn.setDisable(true); seek.setDisable(true); time.setText("not found");
System.err.println("Audio file not found: " + p);
return root;
}
} catch (Exception ignore) { /* اگر نشد، ادامه می‌دهیم */ }
MediaPlayer[] holder = new MediaPlayer[1];
try {
Media media = new Media(playable);
MediaPlayer player = new MediaPlayer(media);
holder[0] = player;
player.setOnReady(() -> {
double secs = player.getMedia().getDuration().toSeconds();
if (secs <= 0) secs = 1;
seek.setMax(secs);
time.setText(fmtClock(secs));
});
player.currentTimeProperty().addListener((o, ov, nv) -> {
if (!seek.isValueChanging()) seek.setValue(nv.toSeconds());
});
seek.valueChangingProperty().addListener((obs, was, is) -> {
if (!is && holder[0] != null) holder[0].seek(Duration.seconds(seek.getValue()));
});
seek.setOnMouseReleased(e -> {
if (holder[0] != null) holder[0].seek(Duration.seconds(seek.getValue()));
});
btn.setOnAction(e -> {
if (holder[0] == null) return;
if (holder[0].getStatus() == MediaPlayer.Status.PLAYING) {
holder[0].pause(); btn.setText("");
} else {
holder[0].play(); btn.setText("");
}
});
player.setOnEndOfMedia(() -> { btn.setText(""); player.seek(Duration.ZERO); });
media.setOnError(() -> System.err.println("Media error: " + media.getError()));
player.setOnError(() -> System.err.println("Player error: " + player.getError()));
root.sceneProperty().addListener((obs, old, nw) -> {
if (old != null && nw == null && holder[0] != null) { holder[0].dispose(); holder[0] = null; }
});
} catch (Exception ex) {
btn.setDisable(true); seek.setDisable(true); time.setText("error");
System.err.println("Build media failed: " + ex.getMessage() + " | uri=" + playable);
}
return root;
}
private String toFileUri(String pathOrUri) {
if (pathOrUri == null || pathOrUri.isBlank()) return null;
if (pathOrUri.startsWith("file:/")) return pathOrUri;
java.io.File f = new java.io.File(pathOrUri);
return f.toURI().toString(); // -> file:///C:/... یا file:///home/...
}
private String fmtClock(double secsD) {
int secs = (int)Math.round(secsD);
return (secs/60) + ":" + String.format("%02d", secs%60);
}
public void onRealTimeMessageDeleted(JSONObject ev) {
String msgId = str(ev, "message_id");
@@ -2727,8 +2839,7 @@ public class ChatPageController {
return;
}
// ========== Owner case ==========
// گرفتن لیست ادمین‌ها
JSONObject req = new JSONObject()
.put("action", "view_group_admins")
.put("group_id", entry.getId().toString());
@@ -2802,7 +2913,6 @@ public class ChatPageController {
return;
}
// ارسال درخواست انتقال مالکیت
JSONObject promoteReq = new JSONObject()
.put("action", "transfer_group_ownership")
.put("group_id", entry.getId().toString());
@@ -2820,7 +2930,6 @@ public class ChatPageController {
return;
}
// بعد از انتقال، لفت بده
sendLeaveRequest(entry.getId(), myUuidStr);
}
@@ -2858,4 +2967,143 @@ public class ChatPageController {
// === CHANNEL: Leave/Transfer logic ===
private void onLeaveChannelMenuClicked(ChatEntry entry) {
if (entry == null || !"channel".equalsIgnoreCase(entry.getType())) {
alert(Alert.AlertType.INFORMATION, "This action is only available for channels.");
return;
}
String myUuidStr = Session.getUserUUID();
if (myUuidStr == null || myUuidStr.isBlank()) {
alert(Alert.AlertType.ERROR, "Cannot determine your identity.");
return;
}
// If I'm not the owner → simple leave
if (!entry.isOwner()) {
if (confirm("Leave Channel", "Are you sure you want to leave this channel?")) {
sendLeaveChannelRequest(entry.getId(), myUuidStr);
}
return;
}
// I'm the owner → must pick a new owner among existing admins
JSONObject req = new JSONObject()
.put("action", "view_channel_admins")
.put("channel_id", entry.getId().toString()); // server expects channel_id here
JSONObject res = ActionHandler.sendWithResponse(req);
if (res == null || !"success".equalsIgnoreCase(res.optString("status"))) {
alert(Alert.AlertType.ERROR, "Failed to fetch channel admins.");
return;
}
JSONArray admins = res.getJSONObject("data").optJSONArray("admins");
if (admins == null || admins.isEmpty()) {
alert(Alert.AlertType.WARNING, "No other admins available. Promote someone first.");
return;
}
// Build candidate list (exclude myself)
List<JSONObject> candidates = new ArrayList<>();
for (int i = 0; i < admins.length(); i++) {
JSONObject a = admins.optJSONObject(i);
if (a == null) continue;
String uuid = a.optString("internal_uuid", "");
if (!uuid.isBlank() && uuid.equals(myUuidStr)) continue; // skip myself
candidates.add(a);
}
if (candidates.isEmpty()) {
alert(Alert.AlertType.WARNING, "No other admins available.");
return;
}
// Dialog to choose the new owner
Dialog<JSONObject> dialog = new Dialog<>();
dialog.setTitle("Transfer Ownership");
dialog.setHeaderText("Select a new owner before leaving the channel");
dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
ListView<JSONObject> listView = new ListView<>();
listView.getItems().addAll(candidates);
listView.setCellFactory(v -> new ListCell<>() {
@Override protected void updateItem(JSONObject item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
String displayName = item.optString("profile_name", "Unknown");
String displayId = item.optString("user_id", "");
setText(displayName + (displayId.isBlank() ? "" : " (" + displayId + ")"));
}
}
});
dialog.getDialogPane().setContent(listView);
Node okBtn = dialog.getDialogPane().lookupButton(ButtonType.OK);
okBtn.setDisable(true);
listView.getSelectionModel().selectedItemProperty().addListener((obs, oldV, sel) -> {
okBtn.setDisable(sel == null);
});
dialog.setResultConverter(bt ->
bt == ButtonType.OK ? listView.getSelectionModel().getSelectedItem() : null);
Optional<JSONObject> pick = dialog.showAndWait();
if (pick.isEmpty()) return;
JSONObject selected = pick.get();
String newOwnerUuid = selected.optString("internal_uuid", "").trim();
String newOwnerUserId = selected.optString("user_id", "").trim();
if (newOwnerUuid.isEmpty() && newOwnerUserId.isEmpty()) {
alert(Alert.AlertType.ERROR, "Selected admin has no valid id.");
return;
}
// Transfer ownership
JSONObject promoteReq = new JSONObject()
.put("action", "transfer_channel_ownership")
.put("channel_id", entry.getId().toString());
// Prefer internal UUID; include user_id only if UUID missing (server quirks!)
if (!newOwnerUuid.isEmpty()) {
promoteReq.put("new_owner_id", newOwnerUuid);
} else {
promoteReq.put("new_owner_user_id", newOwnerUserId);
}
JSONObject promoteRes = ActionHandler.sendWithResponse(promoteReq);
if (promoteRes == null || !"success".equalsIgnoreCase(promoteRes.optString("status"))) {
String msg = (promoteRes != null) ? promoteRes.optString("message", "Ownership transfer failed.") : "null response";
alert(Alert.AlertType.ERROR, msg);
return;
}
// After successful transfer, leave the channel myself
sendLeaveChannelRequest(entry.getId(), myUuidStr);
}
private void sendLeaveChannelRequest(UUID channelId, String myUuidStr) {
JSONObject req = new JSONObject()
.put("action", "leave_chat")
.put("user_id", myUuidStr)
.put("chat_id", channelId.toString())
.put("chat_type", "channel");
JSONObject res = ActionHandler.sendWithResponse(req);
if (res != null && "success".equalsIgnoreCase(res.optString("status"))) {
alert(Alert.AlertType.INFORMATION, "You left the channel.");
MainController.getInstance().refreshChatListUI();
AppRouter.showMain();
} else {
String msg = (res != null) ? res.optString("message", "Leave failed.") : "null response";
alert(Alert.AlertType.ERROR, msg);
}
}
}
@@ -0,0 +1,27 @@
package org.to.telegramfinalproject.UI;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
public final class MediaPathResolver {
private final Path uploadsRoot;
public MediaPathResolver(Path uploadsRoot) {
this.uploadsRoot = uploadsRoot;
}
public String toFileUri(String raw) {
if (raw == null || raw.isBlank()) return null;
if (raw.startsWith("file:/")) return raw;
Path p;
if (raw.startsWith("/")) {
String sub = raw.substring(1).replace("/", File.separator);
p = uploadsRoot.resolve(sub).normalize();
} else {
p = Paths.get(raw).normalize();
}
return p.toUri().toString();
}
}