Merge remote-tracking branch 'origin/Main-UI' into Main-UI

This commit is contained in:
2025-09-08 00:57:54 +03:30
37 changed files with 1929 additions and 602 deletions
@@ -679,4 +679,83 @@ public class GroupDatabase {
// return result; // return result;
// } // }
public static JSONObject getGroupInfo(UUID groupId, UUID viewerUuid) throws SQLException {
JSONObject result = new JSONObject();
try (Connection conn = ConnectionDb.connect()) {
// === Group header (name, member count, etc.)
String groupQuery = """
SELECT g.internal_uuid, g.group_id, g.group_name, g.image_url,
COUNT(m.user_id) as member_count
FROM groups g
LEFT JOIN group_members m ON g.internal_uuid = m.group_id
WHERE g.internal_uuid = ?
GROUP BY g.internal_uuid, g.group_id, g.group_name, g.image_url
""";
try (PreparedStatement ps = conn.prepareStatement(groupQuery)) {
ps.setObject(1, groupId); // ✅ compare UUID with UUID
ResultSet rs = ps.executeQuery();
if (rs.next()) {
result.put("internal_uuid", rs.getString("internal_uuid"));
result.put("group_id", rs.getString("group_id")); // varchar handle
result.put("group_name", rs.getString("group_name"));
result.put("member_count", rs.getInt("member_count"));
result.put("image_url", rs.getString("image_url"));
} else {
return null; // no such group
}
}
// === Members (id, name, role, status, image)
JSONArray membersArr = new JSONArray();
String membersQuery = """
SELECT u.internal_uuid, u.profile_name, u.user_id, u.image_url,
gm.role, u.status, u.last_seen
FROM group_members gm
JOIN users u ON gm.user_id = u.internal_uuid
WHERE gm.group_id = ?
""";
try (PreparedStatement ps = conn.prepareStatement(membersQuery)) {
ps.setObject(1, groupId); // ✅ gm.group_id is UUID
ResultSet rs = ps.executeQuery();
while (rs.next()) {
JSONObject member = new JSONObject();
member.put("user_id", rs.getString("internal_uuid"));
member.put("profile_name", rs.getString("profile_name"));
member.put("username", rs.getString("user_id"));
member.put("image_url", rs.getString("image_url"));
member.put("role", rs.getString("role"));
member.put("status", rs.getString("status"));
member.put("last_seen", rs.getString("last_seen"));
membersArr.put(member);
}
}
result.put("members", membersArr);
// === Viewer role (to decide if they can delete group, etc.)
String roleQuery = """
SELECT role
FROM group_members
WHERE group_id = ? AND user_id = ?
""";
try (PreparedStatement ps = conn.prepareStatement(roleQuery)) {
ps.setObject(1, groupId);
ps.setObject(2, viewerUuid);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
result.put("my_role", rs.getString("role"));
} else {
result.put("my_role", ""); // not a member
}
}
}
return result;
}
} }
@@ -1,23 +0,0 @@
package org.to.telegramfinalproject;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class HelloApplication extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
Scene scene = new Scene(fxmlLoader.load(), 320, 240);
stage.setTitle("Hello!");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
@@ -1,14 +0,0 @@
package org.to.telegramfinalproject;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
public class HelloController {
@FXML
private Label welcomeText;
@FXML
protected void onHelloButtonClick() {
welcomeText.setText("Welcome to JavaFX Application!");
}
}
@@ -3005,7 +3005,29 @@ public class ClientHandler implements Runnable {
break; break;
} }
case "view_group": {
if (currentUser == null) {
response = new ResponseModel("error", "Unauthorized. Please login first.");
break;
}
try {
UUID groupId = UUID.fromString(requestJson.getString("group_id"));
UUID viewerId = UUID.fromString(requestJson.getString("viewer_id"));
// Query group details
JSONObject groupData = GroupDatabase.getGroupInfo(groupId, viewerId);
if (groupData == null) {
response = new ResponseModel("error", "Group not found.");
} else {
response = new ResponseModel("success", "Group info fetched.", groupData);
}
} catch (Exception e) {
response = new ResponseModel("error", "Error processing group info: " + e.getMessage());
}
break;
}
default: default:
@@ -35,6 +35,13 @@ public class AddMembersController {
private String groupDisplayId; private String groupDisplayId;
private File groupImageFile; private File groupImageFile;
public enum Mode {
CREATE, // from New Group
ADD // from Group Info
}
private Mode mode = Mode.CREATE; // default
// انتخاب‌ها // انتخاب‌ها
private final Set<Contact> selectedContacts = new HashSet<>(); private final Set<Contact> selectedContacts = new HashSet<>();
// همه‌ی کانتکت‌ها // همه‌ی کانتکت‌ها
@@ -42,7 +49,7 @@ public class AddMembersController {
@FXML @FXML
public void initialize() { public void initialize() {
// اسکرول نرم // Smooth scroll feel
contactsScroll.getStylesheets().add( contactsScroll.getStylesheets().add(
getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm() getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
); );
@@ -81,21 +88,28 @@ public class AddMembersController {
renderContacts(allContacts); renderContacts(allContacts);
} }
// این متد را NewGroupController بعد از ساخت گروه صدا بزند // Called when creating a new group from sidebar
public void setGroupInfo(UUID internalId, String groupName, String displayId, File groupImageFile) { public void setGroupInfo(UUID internalId, String groupName, String displayId, File groupImageFile) {
this.groupInternalId = internalId; this.groupInternalId = internalId;
this.groupName = groupName; this.groupName = groupName;
this.groupDisplayId = displayId; this.groupDisplayId = displayId;
this.groupImageFile = groupImageFile; this.groupImageFile = groupImageFile;
this.mode = Mode.CREATE;
updateActionButtonText();
} }
// اگر هنوز امضای قدیمی را صدا می‌زنی، موقتاً این اوِرلود هست (displayId را می‌گیرد اما internal_id لازم است) // Called when adding to an existing group
public void setGroupInfo(String groupName, String groupId, File groupImageFile) { public void setGroupForAdd(UUID internalId, String groupName) {
// فقط برای سازگاری موقت؛ حتماً NewGroupController را طوری به‌روزرسانی کن this.groupInternalId = internalId;
// که internal_id را بدهد (امضای بالایی).
this.groupName = groupName; this.groupName = groupName;
this.groupDisplayId = groupId; this.mode = Mode.ADD;
this.groupImageFile = groupImageFile; updateActionButtonText();
}
private void updateActionButtonText() {
if (createButton != null) {
createButton.setText(mode == Mode.CREATE ? "Create" : "Add");
}
} }
private void loadContactsFromSession() { private void loadContactsFromSession() {
@@ -51,7 +51,7 @@ public class BlockedUsersController {
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> updateBackIcon(newVal)); ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> updateBackIcon(newVal));
updateBackIcon(ThemeManager.getInstance().isDarkMode()); updateBackIcon(ThemeManager.getInstance().isDarkMode());
// اسکرول نرم // Smooth scroll feel
blockedList.getStylesheets().add( blockedList.getStylesheets().add(
getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm() getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
); );
@@ -1132,28 +1132,38 @@ public class ChatPageController {
} }
private void configureHeaderActions(ChatEntry entry) { private void configureHeaderActions(ChatEntry entry) {
chatMoreMenu.getItems().clear(); // Hide everything by default
archiveItem.setVisible(false);
viewProfileItem.setVisible(false);
deleteChatItem.setVisible(false);
viewGroupItem.setVisible(false);
leaveGroupItem.setVisible(false);
viewChannelItem.setVisible(false);
leaveChannelItem.setVisible(false);
switch (entry.getType().toLowerCase(Locale.ROOT)) { switch (entry.getType().toLowerCase(Locale.ROOT)) {
case "private" -> { case "private" -> {
archiveItem.setVisible(true);
viewProfileItem.setVisible(true);
deleteChatItem.setVisible(true);
archiveItem.setOnAction(e -> toggleArchive(entry)); archiveItem.setOnAction(e -> toggleArchive(entry));
viewProfileItem.setOnAction(e -> openInfoScene(entry)); viewProfileItem.setOnAction(e -> openInfoScene(entry));
deleteChatItem.setOnAction(e -> deleteChatButton(entry)); deleteChatItem.setOnAction(e -> deleteChatButton(entry));
chatMoreMenu.getItems().addAll(archiveItem, viewProfileItem, deleteChatItem);
} }
case "group" -> { case "group" -> {
MenuItem viewGroup = new MenuItem("View group info"); viewGroupItem.setVisible(true);
viewGroup.setOnAction(e -> openInfoScene(entry)); leaveGroupItem.setVisible(true);
MenuItem leaveGroup = new MenuItem("Leave group");
leaveGroup.setOnAction(e -> leaveGroupButton(entry)); viewGroupItem.setOnAction(e -> openInfoScene(entry));
chatMoreMenu.getItems().addAll(viewGroup, leaveGroup); leaveGroupItem.setOnAction(e -> leaveGroupButton(entry));
} }
case "channel" -> { case "channel" -> {
MenuItem viewChannel = new MenuItem("View channel info"); viewChannelItem.setVisible(true);
viewChannel.setOnAction(e -> openInfoScene(entry)); leaveChannelItem.setVisible(true);
MenuItem leaveChannel = new MenuItem("Leave channel");
leaveChannel.setOnAction(e -> leaveChannelButton(entry)); viewChannelItem.setOnAction(e -> openInfoScene(entry));
chatMoreMenu.getItems().addAll(viewChannel, leaveChannel); leaveChannelItem.setOnAction(e -> leaveChannelButton(entry));
} }
} }
@@ -1205,7 +1215,8 @@ public class ChatPageController {
} }
case "group" -> { case "group" -> {
req.put("action", "view_group") req.put("action", "view_group")
.put("group_id", entry.getId().toString()); .put("group_id", entry.getId().toString())
.put("viewer_id", Session.getUserUUID());
} }
case "channel" -> { case "channel" -> {
req.put("action", "view_channel") req.put("action", "view_channel")
@@ -3615,6 +3626,14 @@ public class ChatPageController {
if (deleteChatItem != null && deleteChatItem.getGraphic() instanceof ImageView iv) { if (deleteChatItem != null && deleteChatItem.getGraphic() instanceof ImageView iv) {
iv.setImage(loadIcon("delete_red.png")); // stays red in both themes iv.setImage(loadIcon("delete_red.png")); // stays red in both themes
} }
if (viewGroupItem != null && viewGroupItem.getGraphic() instanceof ImageView iv) {
iv.setImage(loadIcon("group" + suffix));
}
if (viewChannelItem != null && viewChannelItem.getGraphic() instanceof ImageView iv) {
iv.setImage(loadIcon("group" + suffix));
}
} }
// ChatPageController // ChatPageController
@@ -1,11 +1,325 @@
package org.to.telegramfinalproject.UI; package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.json.JSONObject; import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.AvatarLocalResolver;
import org.to.telegramfinalproject.Client.Session;
import org.to.telegramfinalproject.Models.ChatEntry; import org.to.telegramfinalproject.Models.ChatEntry;
import java.io.IOException;
import java.net.URL;
import java.util.UUID;
public class GroupInfoController { public class GroupInfoController {
@FXML private VBox groupCard;
@FXML private Pane overlayBackground;
@FXML private Button closeButton;
@FXML private ImageView groupImage;
@FXML private Label groupName;
@FXML private Label memberCount;
@FXML private Button infoMoreButton;
@FXML private ContextMenu infoMoreMenu;
@FXML private MenuItem addMemberItem;
@FXML private MenuItem manageGroupItem;
@FXML private MenuItem deleteGroupItem;
@FXML private ImageView moreIcon;
@FXML private VBox membersList;
@FXML private Label membersHeader;
@FXML private Button addMemberButton;
@FXML private ImageView membersIcon;
@FXML private ScrollPane membersScroll;
private String groupId; // the UUID of this group
private static final String ICON_PATH = "/org/to/telegramfinalproject/Icons/";
@FXML
private void initialize() {
closeButton.setOnAction(e ->
MainController.getInstance().closeOverlay(groupCard.getParent()));
overlayBackground.setOnMouseClicked(e ->
MainController.getInstance().closeOverlay(groupCard.getParent()));
infoMoreButton.setOnAction(e -> {
if (infoMoreMenu != null) infoMoreMenu.show(infoMoreButton, javafx.geometry.Side.BOTTOM, 0, 0);
});
if (manageGroupItem != null) {
manageGroupItem.setOnAction(e -> openManageGroupScene());
}
deleteGroupItem.setOnAction(e -> handleDeleteGroup());
Platform.runLater(() -> {
if (groupCard.getScene() != null) {
ThemeManager.getInstance().registerScene(groupCard.getScene());
}
});
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> {
updateIcons(newVal);
});
updateIcons(ThemeManager.getInstance().isDarkMode());
// Smooth scroll feel
membersScroll.getStylesheets().add(
getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
);
membersScroll.skinProperty().addListener((obs, oldSkin, newSkin) -> {
if (newSkin != null) {
ScrollBar vBar = (ScrollBar) membersScroll.lookup(".scroll-bar:vertical");
if (vBar != null) {
membersScroll.setOnScroll(event -> {
double deltaY = event.getDeltaY() * 0.003;
double newValue = vBar.getValue() - deltaY;
vBar.setValue(Math.max(0, Math.min(newValue, 1)));
});
}
}
});
if (addMemberButton != null) {
addMemberButton.setOnAction(e -> openAddMemberScene());
}
if (addMemberItem != null) {
addMemberItem.setOnAction(e -> openAddMemberScene());
}
}
public void setGroupDataFromJson(ChatEntry entry, JSONObject data) { public void setGroupDataFromJson(ChatEntry entry, JSONObject data) {
// read name, status, bio, image url from JSON this.groupId = data.optString("internal_uuid", entry.getId().toString());
// update labels/images accordingly
groupName.setText(data.optString("group_name", entry.getName()));
memberCount.setText(data.optInt("member_count", 0) + " members");
// --- Role-based UI ---
String myRole = data.optString("my_role", "member").toLowerCase();
// Delete group only owner
deleteGroupItem.setVisible("owner".equals(myRole));
// Add member (button + menu item) owner or admin
boolean canAdd = "owner".equals(myRole) || "admin".equals(myRole);
addMemberButton.setVisible(canAdd);
addMemberButton.setManaged(canAdd);
if (addMemberItem != null) {
addMemberItem.setVisible(canAdd);
}
// More menu (3-dot) hide entirely for plain members
boolean showMore = "owner".equals(myRole) || "admin".equals(myRole);
infoMoreButton.setVisible(showMore);
infoMoreButton.setManaged(showMore);
// --- Group picture ---
String imgUrl = data.optString("image_url", "");
if (!imgUrl.isBlank()) {
try {
Image img = AvatarLocalResolver.load(imgUrl);
if (img != null) groupImage.setImage(img);
} catch (Exception ignore) {}
} else {
groupImage.setImage(
new Image(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Avatars/default_group_profile.png"))
);
}
// --- Members list ---
membersList.getChildren().clear();
var arr = data.optJSONArray("members");
if (arr != null) {
membersHeader.setText(arr.length() + " MEMBERS");
for (int i = 0; i < arr.length(); i++) {
JSONObject m = arr.getJSONObject(i);
addMemberRow(m);
}
}
}
private void addMemberRow(JSONObject m) {
HBox row = new HBox(10);
row.getStyleClass().add("member-row");
row.setAlignment(Pos.CENTER_LEFT);
// === Avatar ===
ImageView avatar = new ImageView();
avatar.setFitWidth(36);
avatar.setFitHeight(36);
avatar.setPreserveRatio(true);
AvatarFX.circleClip(avatar, 36);
String imgUrl = m.optString("image_url", "");
if (!imgUrl.isBlank()) {
Image img = AvatarLocalResolver.load(imgUrl);
if (img != null) avatar.setImage(img);
} else {
avatar.setImage(new Image(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"
)));
}
// === Name + Status ===
VBox nameBox = new VBox(2);
Label name = new Label(m.optString("profile_name", "Unknown"));
name.getStyleClass().add("member-name");
// Status = online / last seen recently
String status;
if (m.optBoolean("is_online", false)) {
status = "online";
} else {
status = ChatPageController.getInstance().userStatusText(
false,
m.optString("last_seen", null)
);
}
Label statusLbl = new Label(status);
statusLbl.getStyleClass().add("member-status");
nameBox.getChildren().addAll(name, statusLbl);
// === Role (owner/admin/member) ===
Label role = new Label();
String roleStr = m.optString("role", "");
if (!roleStr.isBlank()) {
role.setText(roleStr.toLowerCase());
role.getStyleClass().add("member-role");
}
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
row.getChildren().addAll(avatar, nameBox, spacer, role);
membersList.getChildren().add(row);
}
private void openManageGroupScene() {
new Thread(() -> {
try {
JSONObject req = new JSONObject()
.put("action", "view_group")
.put("group_id", groupId)
.put("viewer_id", Session.getUserUUID());
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) {
Platform.runLater(() -> MainController.getInstance().showAlert(
"Error",
resp != null ? resp.optString("message") : "Server not responding.",
Alert.AlertType.ERROR
));
return;
}
JSONObject data = resp.optJSONObject("data");
if (data == null) {
Platform.runLater(() -> MainController.getInstance().showAlert(
"Error",
"Malformed server response.",
Alert.AlertType.ERROR
));
return;
}
Platform.runLater(() -> {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/manage_group.fxml"));
Node overlay = loader.load();
ManageGroupController controller = loader.getController();
controller.setGroupData(data); // pass the full JSON
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error",
"Could not load Manage Group scene.",
Alert.AlertType.ERROR
);
}
});
} catch (Exception e) {
e.printStackTrace();
Platform.runLater(() -> MainController.getInstance().showAlert(
"Error",
"Error while fetching group info: " + e.getMessage(),
Alert.AlertType.ERROR
));
}
}).start();
}
private void openAddMemberScene() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/add_member.fxml"));
Node overlay = loader.load();
AddMembersController controller = loader.getController();
controller.setGroupForAdd(UUID.fromString(groupId), String.valueOf(groupName));
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error",
"Could not load Add Member scene.",
Alert.AlertType.ERROR
);
}
}
private void handleDeleteGroup() {
System.out.println("Deleting group...");
// TODO: implement backend call
}
private void updateIcons(boolean dark) {
String suffix = dark ? "_light.png" : "_dark.png";
moreIcon.setImage(loadImage(ICON_PATH + "more" + suffix));
membersIcon.setImage(loadImage(ICON_PATH + "group" + suffix));
addMemberButton.setGraphic(makeIcon(ICON_PATH + "add_member" + suffix));
membersIcon.setImage(loadImage(ICON_PATH + "group_member" + suffix));
}
// --- helpers -------------------------------------------------------------
private ImageView makeIcon(String path) {
ImageView iv = new ImageView();
Image img = loadImage(path);
if (img != null) {
iv.setImage(img);
iv.setFitWidth(22);
iv.setFitHeight(22);
iv.setPreserveRatio(true);
}
return iv;
}
private Image loadImage(String path) {
URL res = getClass().getResource(path);
if (res == null) return null;
return new Image(res.toExternalForm());
} }
} }
@@ -0,0 +1,170 @@
package org.to.telegramfinalproject.UI;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
public class ManageGroupController {
@FXML private VBox manageGroupCard;
@FXML private Pane overlayBackground;
@FXML private Button closeButton, changePicButton, cancelButton, saveButton;
@FXML private ImageView groupImage;
@FXML private TextField groupNameField;
@FXML private Label adminCount, memberCount;
@FXML private TextField groupIdField;
@FXML private Button manageAdminsButton;
@FXML private Button manageMembersButton;
private String groupId; // Internal UUID of the current group
private String originalName;
private String originalGroupId;
private String originalImageUrl;
private File selectedImageFile;
private JSONObject data;
@FXML
public void initialize() {
closeButton.setOnAction(e -> MainController.getInstance().closeOverlay(manageGroupCard.getParent()));
cancelButton.setOnAction(e -> MainController.getInstance().closeOverlay(manageGroupCard.getParent()));
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(manageGroupCard.getParent()));
changePicButton.setOnAction(e -> choosePicture());
saveButton.setOnAction(e -> saveChanges());
manageAdminsButton.setOnAction(e -> openManageAdminsScene());
manageMembersButton.setOnAction(e -> openManageMembersScene(data));
}
private void openManageAdminsScene() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/manage_admins.fxml"));
Node overlay = loader.load();
// ManageAdminsController controller = loader.getController();
// controller.setGroupId(UUID.fromString(groupId));
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error", "Could not load Manage Admins scene.", Alert.AlertType.ERROR);
}
}
private void openManageMembersScene(JSONObject data) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/manage_members.fxml"));
Node overlay = loader.load();
ManageMembersController controller = loader.getController();
// Pass groupId and members list from the JSON data
controller.setGroupData(
data.optString("internal_uuid"),
data.optJSONArray("members")
);
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error", "Could not load Manage Members scene.", Alert.AlertType.ERROR);
}
}
public void setGroupData(JSONObject data) {
this.data = data;
this.groupId = data.optString("internal_uuid");
originalName = data.optString("group_name", "");
originalGroupId = data.optString("group_id", "");
originalImageUrl = data.optString("image_url", "");
groupNameField.setText(originalName);
groupIdField.setText(originalGroupId);
if (!originalImageUrl.isBlank()) {
groupImage.setImage(new Image(originalImageUrl, true));
}
adminCount.setText(String.valueOf(countRole(data, "admin")));
memberCount.setText(String.valueOf(countRole(data, "member")));
}
private int countRole(JSONObject groupData, String role) {
var arr = groupData.optJSONArray("members");
if (arr == null) return 0;
int count = 0;
for (int i = 0; i < arr.length(); i++) {
if (role.equalsIgnoreCase(arr.getJSONObject(i).optString("role"))) count++;
}
return count + 1;
}
private void choosePicture() {
FileChooser fc = new FileChooser();
fc.setTitle("Select group picture");
selectedImageFile = fc.showOpenDialog(manageGroupCard.getScene().getWindow());
if (selectedImageFile != null) {
groupImage.setImage(new Image(selectedImageFile.toURI().toString()));
}
}
private void saveChanges() {
String newName = groupNameField.getText().trim();
String newGroupId = groupIdField.getText().trim(); // make sure you added this field
String newImageUrl = (selectedImageFile != null)
? selectedImageFile.toURI().toString()
: originalImageUrl;
// Check if anything actually changed
boolean changed =
!Objects.equals(originalName, newName) ||
!Objects.equals(originalGroupId, newGroupId) ||
!Objects.equals(originalImageUrl, newImageUrl);
if (!changed) {
// Nothing changed just close overlay
MainController.getInstance().closeOverlay(manageGroupCard.getParent());
return;
}
// Build request with all fields (server expects them)
JSONObject req = new JSONObject()
.put("action", "edit_group_info")
.put("group_id", groupId) // internal_uuid
.put("new_group_id", newGroupId) // display id
.put("name", newName);
if (newImageUrl != null && !newImageUrl.isBlank()) {
req.put("image_url", newImageUrl);
} else {
req.put("image_url", JSONObject.NULL);
}
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
MainController.getInstance().closeOverlay(manageGroupCard.getParent());
} else {
String msg = (resp != null)
? resp.optString("message", "Failed to update group")
: "No response from server";
Alert a = new Alert(Alert.AlertType.ERROR, msg, ButtonType.OK);
a.show();
}
}
}
@@ -0,0 +1,188 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.json.JSONArray;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.AvatarLocalResolver;
import java.io.IOException;
import java.util.Comparator;
import java.util.UUID;
public class ManageMembersController {
@FXML private VBox membersCard;
@FXML private Pane overlayBackground;
@FXML private Button closeButton;
@FXML private Button closeFooterButton;
@FXML private Button addMembersButton;
@FXML private ScrollPane membersScroll;
@FXML private VBox membersList;
private String groupId;
@FXML
private void initialize() {
closeButton.setOnAction(e -> MainController.getInstance().closeOverlay(membersCard.getParent()));
closeFooterButton.setOnAction(e -> MainController.getInstance().closeOverlay(membersCard.getParent()));
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(membersCard.getParent()));
addMembersButton.setOnAction(e -> {
// open add members overlay
openAddMembersOverlay(groupId);
});
// Smooth scroll feel
membersScroll.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm());
membersScroll.setPannable(true);
membersScroll.setFitToWidth(true);
membersScroll.setFitToHeight(false);
membersScroll.getContent().setOnScroll(event -> {
double deltaY = event.getDeltaY() * 0.003;
membersScroll.setVvalue(membersScroll.getVvalue() - deltaY);
});
}
public void setGroupData(String groupId, JSONArray members) {
this.groupId = groupId;
membersList.getChildren().clear();
// Convert safely into JSONObject list
java.util.List<JSONObject> parsed = new java.util.ArrayList<>();
for (int i = 0; i < members.length(); i++) {
Object raw = members.get(i);
if (raw instanceof JSONObject obj) {
parsed.add(obj);
} else if (raw instanceof java.util.Map<?, ?> map) {
parsed.add(new JSONObject(map));
} else {
System.err.println("Skipping invalid member element at index " + i + ": " + raw);
}
}
// Sort: owners first, then others
parsed.sort((a, b) -> {
boolean aOwner = "owner".equalsIgnoreCase(a.optString("role"));
boolean bOwner = "owner".equalsIgnoreCase(b.optString("role"));
return Boolean.compare(!aOwner, !bOwner); // false < true owner first
});
// Add rows
for (JSONObject m : parsed) {
addMemberRow(m);
}
}
private void addMemberRow(JSONObject m) {
HBox row = new HBox(10);
row.getStyleClass().add("member-row");
row.setAlignment(Pos.CENTER_LEFT);
// Avatar
ImageView avatar = new ImageView();
avatar.setFitWidth(36);
avatar.setFitHeight(36);
avatar.setPreserveRatio(true);
String imgUrl = m.optString("image_url", "");
if (!imgUrl.isBlank()) {
Image img = AvatarLocalResolver.load(imgUrl);
if (img != null) avatar.setImage(img);
} else {
avatar.setImage(new Image(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"
)));
}
// Name + Status
VBox details = new VBox(2);
Label name = new Label(m.optString("profile_name", "Unknown"));
name.getStyleClass().add("member-name");
Label status = new Label(
m.optBoolean("is_online", false) ? "online"
: "last seen recently"
);
status.getStyleClass().add("member-status");
details.getChildren().addAll(name, status);
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
row.getChildren().addAll(avatar, details, spacer);
// Only non-owners can be removed
String role = m.optString("role", "member");
if (!"owner".equalsIgnoreCase(role)) {
Button removeBtn = new Button("Remove");
removeBtn.getStyleClass().add("link-btn");
removeBtn.setOnAction(e -> removeMember(m.optString("user_id"), row));
row.getChildren().add(removeBtn);
}
membersList.getChildren().add(row);
}
private void removeMember(String userId, HBox row) {
JSONObject req = new JSONObject()
.put("action", "remove_member_from_group")
.put("group_id", groupId) // must be the group's internal_uuid
.put("user_id", userId); // target user's internal_uuid
// Disable the button while request is in progress
row.setDisable(true);
new Thread(() -> {
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
Platform.runLater(() -> {
membersList.getChildren().remove(row);
});
} else {
Platform.runLater(() -> {
row.setDisable(false); // re-enable on error
Alert a = new Alert(Alert.AlertType.ERROR,
resp != null ? resp.optString("message", "Failed to remove member.")
: "No response from server.",
ButtonType.OK);
a.show();
});
}
}).start();
}
public void openAddMembersOverlay(String groupId) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/add_member.fxml"));
Node overlay = loader.load();
AddMembersController controller = loader.getController();
// Pass the groupId as UUID
controller.setGroupForAdd(UUID.fromString(groupId), "");
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error",
"Could not load Add Members scene.",
Alert.AlertType.ERROR
);
}
}
}
@@ -63,7 +63,7 @@ public class NewChannelController {
createButton.setOnAction(e -> onCreateChannel()); createButton.setOnAction(e -> onCreateChannel());
// محدودیت توضیح // Limit description
final int MAX_LENGTH = 255; final int MAX_LENGTH = 255;
channelDescField.addEventFilter(javafx.scene.input.KeyEvent.KEY_TYPED, e -> { channelDescField.addEventFilter(javafx.scene.input.KeyEvent.KEY_TYPED, e -> {
if (channelDescField.getText().length() >= MAX_LENGTH) e.consume(); if (channelDescField.getText().length() >= MAX_LENGTH) e.consume();
@@ -79,7 +79,7 @@ public class NewChannelController {
}); });
descCounter.setText("0 / 255"); descCounter.setText("0 / 255");
// پاک کردن استایل خطا هنگام تایپ // Reset error when user types again
channelNameField.textProperty().addListener((obs, ov, nv) -> { channelNameField.textProperty().addListener((obs, ov, nv) -> {
if (!nv.trim().isEmpty()) { if (!nv.trim().isEmpty()) {
channelNameField.getStyleClass().remove("error"); channelNameField.getStyleClass().remove("error");
@@ -135,7 +135,8 @@ public class NewChannelController {
if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) { if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) {
Platform.runLater(() -> { Platform.runLater(() -> {
createButton.setDisable(false); createButton.setDisable(false);
showToast("Create failed: " + (resp == null ? "no response" : resp.optString("message",""))); channelIdField.getStyleClass().add("error");
channelIdLabel.getStyleClass().add("error");
}); });
return; return;
} }
@@ -110,8 +110,10 @@ public class NewGroupController {
new Thread(() -> { new Thread(() -> {
JSONObject resp = ActionHandler.sendWithResponse(req); JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) { if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) {
Platform.runLater(() -> showToast("Create failed: " + Platform.runLater(() -> {
(resp == null ? "no response" : resp.optString("message","")))); groupIdField.getStyleClass().add("error");
groupIdLabel.getStyleClass().add("error");
});
return; return;
} }
@@ -182,7 +184,4 @@ public class NewGroupController {
a.initOwner(overlayRoot.getScene().getWindow()); a.initOwner(overlayRoot.getScene().getWindow());
a.show(); a.show();
} }
} }
@@ -364,10 +364,31 @@ public class SidebarMenuController {
} }
} }
private void openTelegramFeatures() { System.out.println("Opening Telegram Features..."); } private void openTelegramFeatures() {
private void openTelegramQnA() { System.out.println("Opening Telegram Q&A..."); } try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/telegram_features.fxml"));
Node featuresOverlay = loader.load();
MainController.getInstance().showOverlay(featuresOverlay);
} catch (IOException e) {
e.printStackTrace();
}
}
private void openTelegramQnA() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/telegram_qna.fxml"));
Node qnaOverlay = loader.load();
MainController.getInstance().showOverlay(qnaOverlay);
} catch (IOException e) {
e.printStackTrace();
}
}
public void setUserFromSession(JSONObject user) { public void setUserFromSession(JSONObject user) {
if (user == null) return; if (user == null) return;
@@ -0,0 +1,42 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
public class TelegramFeaturesController {
@FXML private Pane overlayBackground;
@FXML private ScrollPane featureScroll;
@FXML private StackPane rootOverlay;
@FXML private BorderPane contentCard;
@FXML
public void initialize() {
// Clicking background closes overlay
overlayBackground.setOnMouseClicked( e -> {
MainController.getInstance().closeOverlay(rootOverlay);
});
// Smooth scroll feel
featureScroll.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm());
featureScroll.setPannable(true);
featureScroll.setFitToWidth(true);
featureScroll.setFitToHeight(false);
featureScroll.getContent().setOnScroll(event -> {
double deltaY = event.getDeltaY() * 0.003;
featureScroll.setVvalue(featureScroll.getVvalue() - deltaY);
});
// Register scene for ThemeManager stylesheet swap will handle colors/icons
Platform.runLater(() -> {
if (contentCard.getScene() != null) {
ThemeManager.getInstance().registerScene(contentCard.getScene());
}
});
}
}
@@ -0,0 +1,41 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
public class TelegramQnAController {
@FXML private StackPane rootOverlay;
@FXML private Pane overlayBackground;
@FXML private ScrollPane qnaScroll;
@FXML private BorderPane contentCard;
@FXML
private void initialize() {
// Clicking outside closes overlay
overlayBackground.setOnMouseClicked(e ->
MainController.getInstance().closeOverlay(rootOverlay)
);
// Smooth scroll feel
qnaScroll.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm());
qnaScroll.setPannable(true);
qnaScroll.setFitToWidth(true);
qnaScroll.setFitToHeight(false);
qnaScroll.getContent().setOnScroll(event -> {
double deltaY = event.getDeltaY() * 0.003;
qnaScroll.setVvalue(qnaScroll.getVvalue() - deltaY);
});
// Register scene for ThemeManager stylesheet swap will handle colors/icons
Platform.runLater(() -> {
if (contentCard.getScene() != null) {
ThemeManager.getInstance().registerScene(contentCard.getScene());
}
});
}
}
@@ -1133,17 +1133,234 @@
-fx-border-width: 1; -fx-border-width: 1;
} }
/* ===== Dark theme overrides ===== */ .member-row {
.root.dark .tf-title { -fx-text-fill: #f3f4f6; } -fx-alignment: CENTER_LEFT;
.root.dark .tf-caption { -fx-text-fill: #b8bdc7; }
.root.dark .tf-item-desc { -fx-text-fill: #9ca3af; }
/* TitledPane header/content */
.root.dark .titled-pane > .title {
-fx-background-color: #2b2f33;
-fx-text-fill: #f3f4f6;
}
.root.dark .titled-pane > *.content {
-fx-background-color: #1f2327;
} }
.member-scroll {
-fx-background-color: transparent;
-fx-border-color: transparent;
}
.profile-card {
-fx-background-radius: 12;
-fx-background-color: #1f2a36; /* dark gray card */
-fx-padding: 16;
}
.profile-title {
-fx-font-size: 16px;
-fx-font-weight: bold;
-fx-text-fill: #e8f1f8; /* light text */
}
.member-row {
-fx-padding: 6 0;
}
.member-name {
-fx-font-size: 14px;
-fx-font-weight: bold;
-fx-text-fill: #e8f1f8; /* Telegram light text */
}
.member-status {
-fx-font-size: 12px;
-fx-text-fill: #8ea1b2; /* grayish */
}
.member-role {
-fx-font-size: 12px;
-fx-text-fill: #4fa8f0; /* blue */
-fx-font-weight: bold;
}
/* Group name (bold, light text) */
.info-title {
-fx-font-size: 16px;
-fx-font-weight: bold;
-fx-text-fill: #e8f1f8; /* light gray/white */
}
/* Members count (blue link style like Telegram dark mode) */
.info-subtitle {
-fx-font-size: 13px;
-fx-text-fill: #4fa8f0; /* same Telegram blue */
}
/* Inputs */
.input-field, .input-area {
-fx-background-color: transparent;
-fx-border-width: 0 0 1 0;
-fx-border-color: #444; /* subtle underline */
-fx-text-fill: #e6e6e6; /* light text */
-fx-padding: 4 0 4 0;
-fx-font-size: 14px;
}
.input-area {
-fx-font-size: 12px;
}
/* Info rows */
.info-row-label {
-fx-font-size: 14px;
-fx-text-fill: #e6e6e6;
}
.info-row-value {
-fx-font-size: 14px;
-fx-text-fill: #4fa8f0; /* Telegram blue */
-fx-font-weight: bold;
}
/* Buttons */
.primary-btn {
-fx-background-color: #4fa8f0;
-fx-text-fill: white;
-fx-font-weight: bold;
-fx-background-radius: 6;
-fx-padding: 6 14;
}
.secondary-btn {
-fx-background-color: transparent;
-fx-border-color: #8ea1b2;
-fx-border-radius: 6;
-fx-text-fill: #e6e6e6;
-fx-padding: 6 14;
}
.circle-pic-btn {
-fx-background-color: transparent;
-fx-padding: 0;
}
.manage-link-btn {
-fx-background-color: transparent;
-fx-text-fill: #4fa8f0; /* brighter blue for dark mode */
-fx-underline: false;
-fx-cursor: hand;
-fx-font-size: 13px;
-fx-padding: 0 4 0 4;
}
.manage-link-btn:hover {
-fx-underline: true;
}
.manage-link-btn {
-fx-background-color: transparent;
-fx-text-fill: #4fa8f0; /* brighter blue for dark mode */
-fx-underline: false;
-fx-cursor: hand;
-fx-font-size: 13px;
-fx-padding: 0 4 0 4;
}
.manage-link-btn:hover {
-fx-underline: true;
}
/* Shared */
.member-row {
-fx-alignment: CENTER_LEFT;
-fx-padding: 6 0;
}
.member-name {
-fx-font-size: 14px;
-fx-font-weight: bold;
}
.member-status {
-fx-font-size: 12px;
-fx-text-fill: #8ea1b2;
}
.link-btn {
-fx-background-color: transparent;
-fx-text-fill: -fx-accent;
-fx-cursor: hand;
-fx-padding: 0 6 0 6;
}
.link-btn:hover {
-fx-underline: true;
}
.overlay-background {
-fx-background-color: rgba(0,0,0,0.4);
}
.tf-root {
-fx-background-color: #1f2a36;
-fx-background-radius: 10;
-fx-padding: 16;
-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.5), 10, 0, 0, 4);
}
.tf-toolbar {
-fx-padding: 8 12 8 12;
}
.tf-title {
-fx-font-size: 18px;
-fx-font-weight: bold;
-fx-text-fill: #229ED9; /* Telegram blue */
}
.tf-caption {
-fx-font-size: 12px;
-fx-text-fill: #aaa;
}
.tf-section {
-fx-spacing: 8;
-fx-padding: 8 0 8 0;
}
/* ScrollPane backgrounds */
.scroll-pane {
-fx-background-color: transparent;
-fx-background: transparent;
}
.scroll-pane .viewport {
-fx-background-color: transparent;
}
.tf-sections {
-fx-background-color: #1f2a36; /* same as .tf-root */
}
/* Section + item text colors */
.tf-section-title {
-fx-font-size: 14px;
-fx-font-weight: bold;
-fx-text-fill: #ffffff;
-fx-padding: 4 0 2 0;
-fx-border-color: #333;
-fx-border-width: 0 0 1 0;
}
.tf-item-title {
-fx-font-weight: bold;
-fx-font-size: 13px;
-fx-text-fill: #f5f5f5;
}
.tf-item-desc {
-fx-font-size: 12px;
-fx-text-fill: #cccccc;
}
.tf-status {
-fx-font-size: 11px;
-fx-padding: 2 6 2 6;
-fx-background-radius: 6;
}
.tf-status-available {
-fx-text-fill: #a5d6a7;
-fx-background-color: #1b5e20;
}
.tf-status-progress {
-fx-text-fill: #ffb74d;
-fx-background-color: #e65100;
}
@@ -1124,38 +1124,220 @@
-fx-border-width: 1; -fx-border-width: 1;
} }
/* ===== Base (shared) ===== */ .member-row {
.tf-root { -fx-background-color: transparent; } -fx-alignment: CENTER_LEFT;
.tf-toolbar { -fx-padding: 10 12; -fx-alignment: CENTER_LEFT; }
.tf-title { -fx-font-size: 18px; -fx-font-weight: 700; }
.tf-caption { -fx-opacity: .75; }
.tf-section { -fx-padding: 10 12 14 12; }
.tf-item-desc { -fx-wrap-text: true; -fx-font-size: 13px; -fx-line-spacing: 1; }
/* TitledPane (Accordion items) */
.titled-pane > .title {
-fx-padding: 10 12;
-fx-background-radius: 10;
}
.titled-pane > *.content {
-fx-background-radius: 10;
-fx-background-insets: 0;
-fx-padding: 8 4 12 4;
} }
/* ===== Light theme overrides ===== */ .member-scroll {
.root.light .tf-title { -fx-text-fill: #111111; } -fx-background-color: transparent;
.root.light .tf-caption { -fx-text-fill: #555555; } -fx-border-color: transparent;
.root.light .tf-item-desc { -fx-text-fill: #444444; }
/* TitledPane header/content */
.root.light .titled-pane > .title {
-fx-background-color: #f1f1f1;
-fx-text-fill: #111111;
} }
.root.light .titled-pane > *.content {
.profile-card {
-fx-background-radius: 12;
-fx-background-color: #ffffff; /* pure white card */
-fx-padding: 16;
}
.profile-title {
-fx-font-size: 16px;
-fx-font-weight: bold;
-fx-text-fill: #0f141a; /* dark text */
}
.member-row {
-fx-padding: 6 0;
}
.member-name {
-fx-font-size: 14px;
-fx-font-weight: bold;
-fx-text-fill: #0f141a; /* Telegram dark gray for names */
}
.member-status {
-fx-font-size: 12px;
-fx-text-fill: #8ea1b2; /* grayish */
}
.member-role {
-fx-font-size: 12px;
-fx-text-fill: #4fa8f0; /* blue */
-fx-font-weight: bold;
}
/* Group name (bold, dark text) */
.info-title {
-fx-font-size: 16px;
-fx-font-weight: bold;
-fx-text-fill: #0f141a; /* deep gray */
}
/* Members count (blue link style like Telegram) */
.info-subtitle {
-fx-font-size: 13px;
-fx-text-fill: #4fa8f0; /* Telegram blue */
}
/* Inputs */
.input-field, .input-area {
-fx-background-color: transparent;
-fx-border-width: 0 0 1 0;
-fx-border-color: #d0d0d0; /* light underline */
-fx-text-fill: #2c2c2c; /* dark text */
-fx-padding: 4 0 4 0;
-fx-font-size: 14px;
}
.input-area {
-fx-font-size: 12px;
}
/* Info rows */
.info-row-label {
-fx-font-size: 14px;
-fx-text-fill: #2c2c2c;
}
.info-row-value {
-fx-font-size: 14px;
-fx-text-fill: #0078d7; /* Telegram blue */
-fx-font-weight: bold;
}
/* Buttons */
.primary-btn {
-fx-background-color: #0078d7;
-fx-text-fill: white;
-fx-font-weight: bold;
-fx-background-radius: 6;
-fx-padding: 6 14;
}
.secondary-btn {
-fx-background-color: transparent;
-fx-border-color: #8ea1b2;
-fx-border-radius: 6;
-fx-text-fill: #2c2c2c;
-fx-padding: 6 14;
}
.circle-pic-btn {
-fx-background-color: transparent;
-fx-padding: 0;
}
.manage-link-btn {
-fx-background-color: transparent;
-fx-text-fill: #0078d7; /* Telegram blue */
-fx-underline: false;
-fx-cursor: hand;
-fx-font-size: 13px;
-fx-padding: 0 4 0 4;
}
.manage-link-btn:hover {
-fx-underline: true;
}
/* Shared */
.member-row {
-fx-alignment: CENTER_LEFT;
-fx-padding: 6 0;
}
.member-name {
-fx-font-size: 14px;
-fx-font-weight: bold;
}
.member-status {
-fx-font-size: 12px;
-fx-text-fill: #8ea1b2;
}
.link-btn {
-fx-background-color: transparent;
-fx-text-fill: -fx-accent;
-fx-cursor: hand;
-fx-padding: 0 6 0 6;
}
.link-btn:hover {
-fx-underline: true;
}
.overlay-background {
-fx-background-color: rgba(0,0,0,0.4);
}
.tf-root {
-fx-background-color: #ffffff; -fx-background-color: #ffffff;
-fx-background-radius: 10;
-fx-padding: 16;
-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 10, 0, 0, 4);
} }
.tf-toolbar {
-fx-padding: 8 12 8 12;
}
.tf-title {
-fx-font-size: 18px;
-fx-font-weight: bold;
-fx-text-fill: #229ED9; /* Telegram blue */
}
.tf-caption {
-fx-font-size: 12px;
-fx-text-fill: #888;
}
/* ScrollPane backgrounds */
.scroll-pane {
-fx-background-color: transparent;
-fx-background: transparent;
}
.scroll-pane .viewport {
-fx-background-color: transparent;
}
.tf-sections {
-fx-background-color: transparent;
}
.tf-section {
-fx-spacing: 8;
-fx-padding: 8 0 8 0;
}
.tf-section-title {
-fx-font-size: 14px;
-fx-font-weight: bold;
-fx-text-fill: #2c2c2c;
-fx-padding: 4 0 2 0;
-fx-border-color: #ddd;
-fx-border-width: 0 0 1 0;
}
.tf-item-title {
-fx-font-weight: bold;
-fx-font-size: 13px;
}
.tf-item-desc {
-fx-font-size: 12px;
-fx-text-fill: #555;
}
.tf-status {
-fx-font-size: 11px;
-fx-padding: 2 6 2 6;
-fx-background-radius: 6;
}
.tf-status-available {
-fx-text-fill: #2e7d32;
-fx-background-color: #e8f5e9;
}
.tf-status-progress {
-fx-text-fill: #e65100;
-fx-background-color: #fff3e0;
}
@@ -19,7 +19,7 @@
<!-- ===== Header ===== --> <!-- ===== Header ===== -->
<HBox alignment="CENTER_LEFT" spacing="10" styleClass="contacts-header"> <HBox alignment="CENTER_LEFT" spacing="10" styleClass="contacts-header">
<Label text="Add Members" styleClass="contacts-title"/> <Label text="Add Subscribers" styleClass="contacts-title"/>
<!-- Member count --> <!-- Member count -->
<Label fx:id="memberCountLabel" text="0 / 200000" styleClass="member-count"/> <Label fx:id="memberCountLabel" text="0 / 200000" styleClass="member-count"/>
<Pane HBox.hgrow="ALWAYS"/> <!-- pushes elements to right --> <Pane HBox.hgrow="ALWAYS"/> <!-- pushes elements to right -->
@@ -50,28 +50,60 @@
<contextMenu> <contextMenu>
<ContextMenu fx:id="chatMoreMenu"> <ContextMenu fx:id="chatMoreMenu">
<items> <items>
<MenuItem fx:id="archiveItem" text="Archive chat"> <!-- Private -->
<MenuItem fx:id="archiveItem" text="Archive chat" visible="false">
<graphic> <graphic>
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true"> <ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
<image><Image url="@/org/to/telegramfinalproject/Icons/archived_chats_dark.png"/> <image><Image url="@/org/to/telegramfinalproject/Icons/archived_chats_dark.png"/></image>
</image>
</ImageView> </ImageView>
</graphic> </graphic>
</MenuItem> </MenuItem>
<MenuItem fx:id="viewProfileItem" text="View profile"> <MenuItem fx:id="viewProfileItem" text="View profile" visible="false">
<graphic> <graphic>
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true"> <ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
<image><Image url="@/org/to/telegramfinalproject/Icons/view_profile_dark.png"/></image> <image><Image url="@/org/to/telegramfinalproject/Icons/view_profile_dark.png"/></image>
</ImageView> </ImageView>
</graphic> </graphic>
</MenuItem> </MenuItem>
<MenuItem fx:id="deleteChatItem" text="Delete chat" style="-fx-text-fill: red;"> <MenuItem fx:id="deleteChatItem" text="Delete chat" style="-fx-text-fill: red;" visible="false">
<graphic> <graphic>
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true"> <ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
<image><Image url="@/org/to/telegramfinalproject/Icons/delete_red.png"/></image> <image><Image url="@/org/to/telegramfinalproject/Icons/delete_red.png"/></image>
</ImageView> </ImageView>
</graphic> </graphic>
</MenuItem> </MenuItem>
<!-- Group -->
<MenuItem fx:id="viewGroupItem" text="View group info" visible="false">
<graphic>
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
<image><Image url="@/org/to/telegramfinalproject/Icons/group_dark.png"/></image>
</ImageView>
</graphic>
</MenuItem>
<MenuItem fx:id="leaveGroupItem" text="Leave group" style="-fx-text-fill: red;" visible="false">
<graphic>
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
<image><Image url="@/org/to/telegramfinalproject/Icons/log_out.png"/></image>
</ImageView>
</graphic>
</MenuItem>
<!-- Channel -->
<MenuItem fx:id="viewChannelItem" text="View channel info" visible="false">
<graphic>
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
<image><Image url="@/org/to/telegramfinalproject/Icons/group_dark.png"/></image>
</ImageView>
</graphic>
</MenuItem>
<MenuItem fx:id="leaveChannelItem" text="Leave channel" style="-fx-text-fill: red;" visible="false">
<graphic>
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
<image><Image url="@/org/to/telegramfinalproject/Icons/log_out.png"/></image>
</ImageView>
</graphic>
</MenuItem>
</items> </items>
</ContextMenu> </ContextMenu>
</contextMenu> </contextMenu>
@@ -1,144 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<BorderPane xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml"
styleClass="tf-root">
<top>
<HBox spacing="10" styleClass="tf-toolbar">
<children>
<Label text="Telegram FAQ" styleClass="tf-title"/>
<Region HBox.hgrow="ALWAYS"/>
<Label text="(frequently asked questions)" styleClass="tf-caption"/>
</children>
</HBox>
</top>
<center>
<ScrollPane fitToWidth="true" hbarPolicy="NEVER" vbarPolicy="AS_NEEDED">
<content>
<Accordion>
<panes>
<TitledPane text="1) How do I create a new account?">
<content>
<VBox spacing="6" styleClass="tf-section">
<children>
<Label wrapText="true" styleClass="tf-item-desc"
text="Download the Telegram app, enter your phone number, and confirm it with the SMS code. Then you can set up your name and profile picture."/>
</children>
</VBox>
</content>
</TitledPane>
<TitledPane text="2) How can I add a new contact?">
<content>
<VBox spacing="6" styleClass="tf-section">
<children>
<Label wrapText="true" styleClass="tf-item-desc"
text="Tap the menu, choose 'Contacts', then 'Add Contact'. Enter the phone number and name. Telegram will link it to their account automatically."/>
</children>
</VBox>
</content>
</TitledPane>
<TitledPane text="3) How do I start a private chat?">
<content>
<VBox spacing="6" styleClass="tf-section">
<children>
<Label wrapText="true" styleClass="tf-item-desc"
text="Open the contacts profile and tap 'Message'. This will create a private chat window with them."/>
</children>
</VBox>
</content>
</TitledPane>
<TitledPane text="4) How can I create a group?">
<content>
<VBox spacing="6" styleClass="tf-section">
<children>
<Label wrapText="true" styleClass="tf-item-desc"
text="Go to the menu, select 'New Group', choose members, and set a group name and picture."/>
</children>
</VBox>
</content>
</TitledPane>
<TitledPane text="5) How do channels work?">
<content>
<VBox spacing="6" styleClass="tf-section">
<children>
<Label wrapText="true" styleClass="tf-item-desc"
text="Channels let you broadcast messages to large audiences. Only admins can post, but everyone who subscribes receives the updates."/>
</children>
</VBox>
</content>
</TitledPane>
<TitledPane text="6) How can I search for messages?">
<content>
<VBox spacing="6" styleClass="tf-section">
<children>
<Label wrapText="true" styleClass="tf-item-desc"
text="Use the search bar at the top of the chat list for global search, or use the search option inside a chat to find specific messages."/>
</children>
</VBox>
</content>
</TitledPane>
<TitledPane text="7) How do I delete or edit a message?">
<content>
<VBox spacing="6" styleClass="tf-section">
<children>
<Label wrapText="true" styleClass="tf-item-desc"
text="Long-press (or right-click) on your message. Choose 'Edit' to change it or 'Delete' to remove it. You can delete for yourself or for everyone if allowed."/>
</children>
</VBox>
</content>
</TitledPane>
<TitledPane text="8) How can I use reactions?">
<content>
<VBox spacing="6" styleClass="tf-section">
<children>
<Label wrapText="true" styleClass="tf-item-desc"
text="Tap and hold on a message, then choose an emoji reaction to express your feedback instantly."/>
</children>
</VBox>
</content>
</TitledPane>
<TitledPane text="9) How do I archive chats?">
<content>
<VBox spacing="6" styleClass="tf-section">
<children>
<Label wrapText="true" styleClass="tf-item-desc"
text="Swipe left on a chat (mobile) or right-click on it (desktop) and choose 'Archive'. Archived chats are hidden in a separate folder until a new message arrives."/>
</children>
</VBox>
</content>
</TitledPane>
<TitledPane text="10) How can I enable Dark Mode?">
<content>
<VBox spacing="6" styleClass="tf-section">
<children>
<Label wrapText="true" styleClass="tf-item-desc"
text="Open 'Settings' → 'Appearance' and switch between Light and Dark themes. You can also schedule it automatically."/>
</children>
</VBox>
</content>
</TitledPane>
</panes>
</Accordion>
</content>
</ScrollPane>
</center>
<padding>
<Insets top="8" right="8" bottom="8" left="8"/>
</padding>
</BorderPane>
@@ -1,314 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<BorderPane xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml"
styleClass="tf-root">
<top>
<HBox spacing="10" styleClass="tf-toolbar">
<children>
<Label text="Telegram Features" styleClass="tf-title"/>
<Region HBox.hgrow="ALWAYS"/>
<Label text="(static showcase)" styleClass="tf-caption"/>
</children>
</HBox>
</top>
<center>
<ScrollPane fitToWidth="true" hbarPolicy="NEVER" vbarPolicy="AS_NEEDED">
<content>
<Accordion>
<panes>
<!-- 1) Account & Authentication -->
<TitledPane text="1) Account &amp; Authentication">
<content>
<VBox spacing="8" styleClass="tf-section">
<children>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Register, Login &amp; Logout" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</children>
</HBox>
<Label text="Create account, sign in/out, and manage sessions (devices, multi-login prevention)." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children>
<Label text="GLOBAL" styleClass="tf-chip"/>
<Label text="SECURITY" styleClass="tf-chip"/>
</children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Profile &amp; Presence" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</children>
</HBox>
<Label text="Bio, photo, display name, user id, online / last seen." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children>
<Label text="GLOBAL" styleClass="tf-chip"/>
</children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
</children>
</VBox>
</content>
</TitledPane>
<!-- 2) Contacts -->
<TitledPane text="2) Contacts">
<content>
<VBox spacing="8" styleClass="tf-section">
<children>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Add / Remove Contacts" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</children>
</HBox>
<Label text="Add, remove, and browse contacts with full profile view." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children>
<Label text="CONTACT" styleClass="tf-chip"/>
</children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Block / Unblock" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</children>
</HBox>
<Label text="Manage private chat access and visibility." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children>
<Label text="CONTACT" styleClass="tf-chip"/>
<Label text="SECURITY" styleClass="tf-chip"/>
</children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
</children>
</VBox>
</content>
</TitledPane>
<!-- 3) Chats -->
<TitledPane text="3) Chats">
<content>
<VBox spacing="8" styleClass="tf-section">
<children>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Private Chats" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</children>
</HBox>
<Label text="Create or open private chats (get_or_create_private_chat)." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children>
<Label text="CHAT" styleClass="tf-chip"/>
</children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Groups" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</children>
</HBox>
<Label text="Create groups, add members, roles &amp; permissions, ownership transfer." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children>
<Label text="CHAT" styleClass="tf-chip"/>
<Label text="ADMIN" styleClass="tf-chip"/>
</children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Channels" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</children>
</HBox>
<Label text="Create channels, manage subscribers, admins, and ownership." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children>
<Label text="CHAT" styleClass="tf-chip"/>
<Label text="ADMIN" styleClass="tf-chip"/>
</children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Saved Messages" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</children>
</HBox>
<Label text="Personal cloud chat to keep your own notes and files." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children>
<Label text="GLOBAL" styleClass="tf-chip"/>
<Label text="MESSAGE" styleClass="tf-chip"/>
</children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Archive Chats" styleClass="tf-item-title"/>
<Label text="In Progress" styleClass="tf-status tf-status-progress"/>
</children>
</HBox>
<Label text="Move chats in/out of Archive for better organization." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children>
<Label text="CHAT" styleClass="tf-chip"/>
</children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
</children>
</VBox>
</content>
</TitledPane>
<!-- 4) Messaging -->
<TitledPane text="4) Messaging">
<content>
<VBox spacing="8" styleClass="tf-section">
<children>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Reply" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</children>
</HBox>
<Label text="Quote and reply to a specific message." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children><Label text="MESSAGE" styleClass="tf-chip"/></children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Forward" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</children>
</HBox>
<Label text="Send a message to another chat with source attribution." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children><Label text="MESSAGE" styleClass="tf-chip"/></children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
<!-- Add more items similarly: Edit, Delete, Reactions, Attachments, Read Receipts -->
</children>
</VBox>
</content>
</TitledPane>
<!-- You can add more TitledPane sections similarly (Search, UI/UX, Real-Time Events, Security). -->
</panes>
</Accordion>
</content>
</ScrollPane>
</center>
<padding>
<Insets top="8" right="8" bottom="8" left="8"/>
</padding>
</BorderPane>
@@ -1,14 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.control.*?>
<?import javafx.geometry.Insets?>
<AnchorPane xmlns="http://javafx.com/javafx" <?import javafx.scene.image.Image?>
xmlns:fx="http://javafx.com/fxml" <StackPane xmlns:fx="http://javafx.com/fxml"
fx:controller="org.to.telegramfinalproject.UI.GroupInfoController" fx:controller="org.to.telegramfinalproject.UI.GroupInfoController"
prefHeight="400.0" prefWidth="600.0"> styleClass="overlay-root">
</AnchorPane> <!-- Background -->
<Pane fx:id="overlayBackground" styleClass="overlay-background"/>
<!-- Group card -->
<VBox fx:id="groupCard" styleClass="profile-card" spacing="16"
prefWidth="360" maxWidth="360"
prefHeight="500" maxHeight="500">
<!-- ===== Header: Title + More + Close ===== -->
<HBox alignment="CENTER_LEFT" spacing="10">
<Label text="Group Info" styleClass="profile-title" HBox.hgrow="ALWAYS"/>
<Pane HBox.hgrow="ALWAYS"/>
<!-- More -->
<Button fx:id="infoMoreButton" styleClass="icon-button">
<graphic>
<ImageView fx:id="moreIcon" fitWidth="18" fitHeight="18" preserveRatio="true"/>
</graphic>
<contextMenu>
<ContextMenu fx:id="infoMoreMenu">
<items>
<!-- Add Member -->
<MenuItem fx:id="addMemberItem" text="Add member">
<graphic>
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
<image>
<Image url="@/org/to/telegramfinalproject/Icons/add_member_dark.png"/>
</image>
</ImageView>
</graphic>
</MenuItem>
<!-- Manage Group -->
<MenuItem fx:id="manageGroupItem" text="Manage group">
<graphic>
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
<image>
<Image url="@/org/to/telegramfinalproject/Icons/manage_dark.png"/>
</image>
</ImageView>
</graphic>
</MenuItem>
<!-- Delete Chat (owner only) -->
<MenuItem fx:id="deleteGroupItem" text="Delete chat" style="-fx-text-fill: red;" visible="false">
<graphic>
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
<image>
<Image url="@/org/to/telegramfinalproject/Icons/delete_red.png"/>
</image>
</ImageView>
</graphic>
</MenuItem>
</items>
</ContextMenu>
</contextMenu>
</Button>
<!-- Close -->
<Button fx:id="closeButton" styleClass="icon-button" text="✕"/>
</HBox>
<!-- ===== Group picture + name + members count ===== -->
<HBox alignment="CENTER_LEFT" spacing="12">
<!-- Group picture -->
<ImageView fx:id="groupImage" fitWidth="60" fitHeight="60" preserveRatio="true"
styleClass="profile-picture"/>
<!-- Name + members stacked -->
<VBox alignment="CENTER_LEFT" spacing="4">
<Label fx:id="groupName" text="Group name" styleClass="info-title"/>
<Label fx:id="memberCount" text="0 members" styleClass="info-subtitle"/>
</VBox>
</HBox>
<Separator styleClass="section-separator"/>
<!-- ===== Members list ===== -->
<VBox spacing="8" styleClass="info-blocks" VBox.vgrow="ALWAYS">
<HBox spacing="8" alignment="CENTER_LEFT">
<ImageView fx:id="membersIcon" fitWidth="26" fitHeight="26" preserveRatio="true">
<image>
<Image url="@/org/to/telegramfinalproject/Icons/group_member_dark.png"/>
</image>
</ImageView>
<Label fx:id="membersHeader" text="0 MEMBERS" styleClass="profile-info-sub"/>
<Pane HBox.hgrow="ALWAYS"/>
<Button fx:id="addMemberButton" styleClass="icon-button">
<graphic>
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
<image>
<Image url="@/org/to/telegramfinalproject/Icons/add_member_dark.png"/>
</image>
</ImageView>
</graphic>
</Button>
</HBox>
<ScrollPane fx:id="membersScroll" fitToWidth="true" styleClass="member-scroll" VBox.vgrow="ALWAYS">
<content>
<VBox fx:id="membersList" spacing="10"/>
</content>
</ScrollPane>
</VBox>
</VBox>
</StackPane>
@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Button?>
<VBox alignment="CENTER" spacing="20.0" xmlns:fx="http://javafx.com/fxml"
fx:controller="org.to.telegramfinalproject.HelloController">
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
</padding>
<Label fx:id="welcomeText"/>
<Button text="Hello!" onAction="#onHelloButtonClick"/>
</VBox>
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.control.*?>
<?import javafx.geometry.Insets?>
<StackPane xmlns:fx="http://javafx.com/fxml"
fx:controller="org.to.telegramfinalproject.UI.ManageGroupController"
styleClass="overlay-root">
<!-- Background -->
<Pane fx:id="overlayBackground" styleClass="overlay-background"/>
<!-- Card -->
<VBox fx:id="manageGroupCard" styleClass="profile-card" spacing="16"
prefWidth="360" maxWidth="360"
prefHeight="520" maxHeight="520">
<!-- ===== Header ===== -->
<HBox alignment="CENTER_LEFT" spacing="10">
<Label text="Edit group" styleClass="profile-title" HBox.hgrow="ALWAYS"/>
<Pane HBox.hgrow="ALWAYS"/>
<Button fx:id="closeButton" styleClass="icon-button" text="✕"/>
</HBox>
<!-- ===== Group picture + name + description ===== -->
<VBox alignment="CENTER" spacing="12">
<!-- Group picture -->
<Button fx:id="changePicButton" styleClass="circle-pic-btn">
<graphic>
<ImageView fx:id="groupImage" fitWidth="80" fitHeight="80" preserveRatio="true">
<image>
<Image url="@/org/to/telegramfinalproject/Avatars/default_group_profile.png"/>
</image>
</ImageView>
</graphic>
</Button>
<!-- Group name -->
<TextField fx:id="groupNameField" promptText="Group name" styleClass="input-field"/>
<!-- Group ID -->
<TextField fx:id="groupIdField" promptText="Group ID (unique)" styleClass="input-field"/>
</VBox>
<!-- ===== Options ===== -->
<VBox spacing="10" styleClass="info-blocks" VBox.vgrow="ALWAYS">
<!-- Administrators -->
<HBox alignment="CENTER_LEFT" spacing="10">
<Label text="Administrators" styleClass="info-row-label"/>
<Pane HBox.hgrow="ALWAYS"/>
<Label fx:id="adminCount" text="0" styleClass="info-row-value"/>
<Button fx:id="manageAdminsButton" text="Manage" styleClass="manage-link-btn"/>
</HBox>
<!-- Members -->
<HBox alignment="CENTER_LEFT" spacing="10">
<Label text="Members" styleClass="info-row-label"/>
<Pane HBox.hgrow="ALWAYS"/>
<Label fx:id="memberCount" text="0" styleClass="info-row-value"/>
<Button fx:id="manageMembersButton" text="Manage" styleClass="manage-link-btn"/>
</HBox>
</VBox>
<!-- ===== Footer ===== -->
<HBox spacing="10" alignment="CENTER_RIGHT">
<Button fx:id="cancelButton" text="Cancel" styleClass="secondary-btn"/>
<Button fx:id="saveButton" text="Save" styleClass="primary-btn"/>
</HBox>
</VBox>
</StackPane>
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.control.*?>
<?import javafx.geometry.Insets?>
<StackPane xmlns:fx="http://javafx.com/fxml"
fx:controller="org.to.telegramfinalproject.UI.ManageMembersController"
styleClass="overlay-root">
<!-- Background -->
<Pane fx:id="overlayBackground" styleClass="overlay-background"/>
<!-- Card -->
<VBox fx:id="membersCard" styleClass="profile-card" spacing="12"
prefWidth="360" maxWidth="360"
prefHeight="520" maxHeight="520">
<!-- ===== Header ===== -->
<HBox alignment="CENTER_LEFT" spacing="10">
<Label text="Members" styleClass="profile-title" HBox.hgrow="ALWAYS"/>
<Pane HBox.hgrow="ALWAYS"/>
<Button fx:id="closeButton" styleClass="icon-button" text="✕"/>
</HBox>
<Separator styleClass="section-separator"/>
<!-- ===== Members List ===== -->
<ScrollPane fx:id="membersScroll" fitToWidth="true" styleClass="member-scroll" VBox.vgrow="ALWAYS">
<content>
<VBox fx:id="membersList" spacing="10"/>
</content>
</ScrollPane>
<Separator styleClass="section-separator"/>
<!-- ===== Footer ===== -->
<HBox spacing="12" alignment="CENTER_RIGHT">
<Button fx:id="addMembersButton" text="Add members" styleClass="secondary-btn"/>
<Button fx:id="closeFooterButton" text="Close" styleClass="primary-btn"/>
</HBox>
</VBox>
</StackPane>
@@ -0,0 +1,165 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.control.*?>
<?import javafx.geometry.Insets?>
<StackPane xmlns:fx="http://javafx.com/fxml"
fx:id="rootOverlay"
fx:controller="org.to.telegramfinalproject.UI.TelegramFeaturesController"
styleClass="overlay-root">
<!-- Background (click to close) -->
<Pane fx:id="overlayBackground" styleClass="overlay-background"/>
<!-- Content card -->
<BorderPane fx:id="contentCard"
styleClass="tf-root"
maxWidth="700" maxHeight="520">
<!-- Header -->
<top>
<HBox spacing="10" styleClass="tf-toolbar">
<children>
<Label text="Telegram Features" styleClass="tf-title"/>
<Region HBox.hgrow="ALWAYS"/>
</children>
</HBox>
</top>
<!-- Scrollable content -->
<center>
<ScrollPane fx:id="featureScroll" fitToWidth="true" hbarPolicy="NEVER" vbarPolicy="AS_NEEDED">
<content>
<VBox spacing="24" styleClass="tf-sections">
<padding>
<Insets top="8" right="16" bottom="8" left="16"/>
</padding>
<!-- Section 1 -->
<VBox spacing="8" styleClass="tf-section">
<Label text="1) Account &amp; Authentication" styleClass="tf-section-title"/>
<VBox spacing="12">
<VBox>
<HBox spacing="8" alignment="CENTER_LEFT">
<Label text="Register, Login &amp; Logout" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</HBox>
<Label text="Create account, sign in/out, and manage sessions (devices, multi-login prevention)." wrapText="true" styleClass="tf-item-desc"/>
</VBox>
<VBox>
<HBox spacing="8" alignment="CENTER_LEFT">
<Label text="Profile &amp; Presence" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</HBox>
<Label text="Bio, photo, display name, user id, online / last seen." wrapText="true" styleClass="tf-item-desc"/>
</VBox>
</VBox>
</VBox>
<Separator/>
<!-- Section 2 -->
<VBox spacing="8" styleClass="tf-section">
<Label text="2) Contacts" styleClass="tf-section-title"/>
<VBox spacing="12">
<VBox>
<HBox spacing="8" alignment="CENTER_LEFT">
<Label text="Add / Remove Contacts" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</HBox>
<Label text="Add, remove, and browse contacts with full profile view." wrapText="true" styleClass="tf-item-desc"/>
</VBox>
<VBox>
<HBox spacing="8" alignment="CENTER_LEFT">
<Label text="Block / Unblock" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</HBox>
<Label text="Manage private chat access and visibility." wrapText="true" styleClass="tf-item-desc"/>
</VBox>
</VBox>
</VBox>
<Separator/>
<!-- Section 3 -->
<VBox spacing="8" styleClass="tf-section">
<Label text="3) Chats" styleClass="tf-section-title"/>
<VBox spacing="12">
<VBox>
<HBox spacing="8" alignment="CENTER_LEFT">
<Label text="Private Chats" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</HBox>
<Label text="Create or open private chats (get_or_create_private_chat)." wrapText="true" styleClass="tf-item-desc"/>
</VBox>
<VBox>
<HBox spacing="8" alignment="CENTER_LEFT">
<Label text="Groups" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</HBox>
<Label text="Create groups, add members, roles &amp; permissions, ownership transfer." wrapText="true" styleClass="tf-item-desc"/>
</VBox>
<VBox>
<HBox spacing="8" alignment="CENTER_LEFT">
<Label text="Channels" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</HBox>
<Label text="Create channels, manage subscribers, admins, and ownership." wrapText="true" styleClass="tf-item-desc"/>
</VBox>
<VBox>
<HBox spacing="8" alignment="CENTER_LEFT">
<Label text="Saved Messages" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</HBox>
<Label text="Personal cloud chat to keep your own notes and files." wrapText="true" styleClass="tf-item-desc"/>
</VBox>
<VBox>
<HBox spacing="8" alignment="CENTER_LEFT">
<Label text="Archive Chats" styleClass="tf-item-title"/>
<Label text="In Progress" styleClass="tf-status tf-status-progress"/>
</HBox>
<Label text="Move chats in/out of Archive for better organization." wrapText="true" styleClass="tf-item-desc"/>
</VBox>
</VBox>
</VBox>
<Separator/>
<!-- Section 4 -->
<VBox spacing="8" styleClass="tf-section">
<Label text="4) Messaging" styleClass="tf-section-title"/>
<VBox spacing="12">
<VBox>
<HBox spacing="8" alignment="CENTER_LEFT">
<Label text="Reply" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</HBox>
<Label text="Quote and reply to a specific message." wrapText="true" styleClass="tf-item-desc"/>
</VBox>
<VBox>
<HBox spacing="8" alignment="CENTER_LEFT">
<Label text="Forward" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</HBox>
<Label text="Send a message to another chat with source attribution." wrapText="true" styleClass="tf-item-desc"/>
</VBox>
</VBox>
</VBox>
</VBox>
</content>
</ScrollPane>
</center>
<padding>
<Insets top="8" right="8" bottom="8" left="8"/>
</padding>
</BorderPane>
</StackPane>
@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.control.*?>
<?import javafx.geometry.Insets?>
<StackPane xmlns:fx="http://javafx.com/fxml"
fx:id="rootOverlay"
fx:controller="org.to.telegramfinalproject.UI.TelegramQnAController"
styleClass="overlay-root">
<!-- Background (click to close) -->
<Pane fx:id="overlayBackground" styleClass="overlay-background"/>
<!-- Content card -->
<BorderPane fx:id="contentCard"
styleClass="tf-root"
maxWidth="700" maxHeight="520">
<!-- Header -->
<top>
<HBox spacing="10" styleClass="tf-toolbar">
<children>
<Label text="Telegram FAQ" styleClass="tf-title"/>
<Region HBox.hgrow="ALWAYS"/>
<Label text="(frequently asked questions)" styleClass="tf-caption"/>
</children>
</HBox>
</top>
<!-- Scrollable content -->
<center>
<ScrollPane fx:id="qnaScroll" fitToWidth="true" hbarPolicy="NEVER" vbarPolicy="AS_NEEDED">
<content>
<VBox spacing="20" styleClass="tf-sections">
<padding>
<Insets top="8" right="16" bottom="8" left="16"/>
</padding>
<!-- FAQ Item -->
<VBox spacing="6" styleClass="tf-section">
<Label text="1) How do I create a new account?" styleClass="tf-section-title"/>
<Label wrapText="true" styleClass="tf-item-desc"
text="Download the Telegram app, enter your phone number, and confirm it with the SMS code. Then you can set up your name and profile picture."/>
</VBox>
<VBox spacing="6" styleClass="tf-section">
<Label text="2) How can I add a new contact?" styleClass="tf-section-title"/>
<Label wrapText="true" styleClass="tf-item-desc"
text="Tap the menu, choose 'Contacts', then 'Add Contact'. Enter the phone number and name. Telegram will link it to their account automatically."/>
</VBox>
<VBox spacing="6" styleClass="tf-section">
<Label text="3) How do I start a private chat?" styleClass="tf-section-title"/>
<Label wrapText="true" styleClass="tf-item-desc"
text="Open the contacts profile and tap 'Message'. This will create a private chat window with them."/>
</VBox>
<VBox spacing="6" styleClass="tf-section">
<Label text="4) How can I create a group?" styleClass="tf-section-title"/>
<Label wrapText="true" styleClass="tf-item-desc"
text="Go to the menu, select 'New Group', choose members, and set a group name and picture."/>
</VBox>
<VBox spacing="6" styleClass="tf-section">
<Label text="5) How do channels work?" styleClass="tf-section-title"/>
<Label wrapText="true" styleClass="tf-item-desc"
text="Channels let you broadcast messages to large audiences. Only admins can post, but everyone who subscribes receives the updates."/>
</VBox>
<VBox spacing="6" styleClass="tf-section">
<Label text="6) How can I search for messages?" styleClass="tf-section-title"/>
<Label wrapText="true" styleClass="tf-item-desc"
text="Use the search bar at the top of the chat list for global search, or use the search option inside a chat to find specific messages."/>
</VBox>
<VBox spacing="6" styleClass="tf-section">
<Label text="7) How do I delete or edit a message?" styleClass="tf-section-title"/>
<Label wrapText="true" styleClass="tf-item-desc"
text="Long-press (or right-click) on your message. Choose 'Edit' to change it or 'Delete' to remove it. You can delete for yourself or for everyone if allowed."/>
</VBox>
<VBox spacing="6" styleClass="tf-section">
<Label text="8) How can I use reactions?" styleClass="tf-section-title"/>
<Label wrapText="true" styleClass="tf-item-desc"
text="Tap and hold on a message, then choose an emoji reaction to express your feedback instantly."/>
</VBox>
<VBox spacing="6" styleClass="tf-section">
<Label text="9) How do I archive chats?" styleClass="tf-section-title"/>
<Label wrapText="true" styleClass="tf-item-desc"
text="Swipe left on a chat (mobile) or right-click on it (desktop) and choose 'Archive'. Archived chats are hidden in a separate folder until a new message arrives."/>
</VBox>
<VBox spacing="6" styleClass="tf-section">
<Label text="10) How can I enable Dark Mode?" styleClass="tf-section-title"/>
<Label wrapText="true" styleClass="tf-item-desc"
text="Open 'Settings' → 'Appearance' and switch between Light and Dark themes. You can also schedule it automatically."/>
</VBox>
</VBox>
</content>
</ScrollPane>
</center>
<padding>
<Insets top="8" right="8" bottom="8" left="8"/>
</padding>
</BorderPane>
</StackPane>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1021 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B