Add avatar controls
This commit is contained in:
@@ -8,7 +8,11 @@ import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ApiClient {
|
||||
private static final ApiClient INSTANCE = new ApiClient();
|
||||
@@ -83,6 +87,36 @@ public class ApiClient {
|
||||
return handleResponse(response, responseClass);
|
||||
}
|
||||
|
||||
public <T> T postMultipart(String path, String partName, Path filePath, Class<T> responseClass) throws Exception {
|
||||
String boundary = "----PetShopDesktop" + UUID.randomUUID();
|
||||
String mimeType = Files.probeContentType(filePath);
|
||||
if (mimeType == null || mimeType.isBlank()) {
|
||||
mimeType = "application/octet-stream";
|
||||
}
|
||||
|
||||
byte[] fileBytes = Files.readAllBytes(filePath);
|
||||
String fileName = filePath.getFileName().toString();
|
||||
byte[] prefix = ("--" + boundary + "\r\n"
|
||||
+ "Content-Disposition: form-data; name=\"" + partName + "\"; filename=\"" + fileName + "\"\r\n"
|
||||
+ "Content-Type: " + mimeType + "\r\n\r\n").getBytes(StandardCharsets.UTF_8);
|
||||
byte[] suffix = ("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8);
|
||||
byte[] body = new byte[prefix.length + fileBytes.length + suffix.length];
|
||||
System.arraycopy(prefix, 0, body, 0, prefix.length);
|
||||
System.arraycopy(fileBytes, 0, body, prefix.length, fileBytes.length);
|
||||
System.arraycopy(suffix, 0, body, prefix.length + fileBytes.length, suffix.length);
|
||||
|
||||
HttpRequest.Builder builder = HttpRequest.newBuilder()
|
||||
.uri(URI.create(baseUrl + path))
|
||||
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
|
||||
.POST(HttpRequest.BodyPublishers.ofByteArray(body))
|
||||
.timeout(Duration.ofSeconds(30));
|
||||
|
||||
addAuthHeader(builder);
|
||||
|
||||
HttpResponse<String> response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
|
||||
return handleResponse(response, responseClass);
|
||||
}
|
||||
|
||||
public <T> T put(String path, Object requestBody, Class<T> responseClass) throws Exception {
|
||||
String jsonBody = objectMapper.writeValueAsString(requestBody);
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.example.petshopdesktop.api.dto.auth;
|
||||
|
||||
public class AvatarUploadResponse {
|
||||
private String avatarUrl;
|
||||
private String message;
|
||||
|
||||
public AvatarUploadResponse() {
|
||||
}
|
||||
|
||||
public String getAvatarUrl() {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
public void setAvatarUrl(String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.example.petshopdesktop.api.endpoints;
|
||||
|
||||
import org.example.petshopdesktop.api.ApiClient;
|
||||
import org.example.petshopdesktop.api.dto.auth.AvatarUploadResponse;
|
||||
import org.example.petshopdesktop.api.dto.auth.UserInfoResponse;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class AuthApi {
|
||||
private static final AuthApi INSTANCE = new AuthApi();
|
||||
private final ApiClient apiClient;
|
||||
|
||||
private AuthApi() {
|
||||
this.apiClient = ApiClient.getInstance();
|
||||
}
|
||||
|
||||
public static AuthApi getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public UserInfoResponse getCurrentUser() throws Exception {
|
||||
return apiClient.get("/api/v1/auth/me", UserInfoResponse.class);
|
||||
}
|
||||
|
||||
public AvatarUploadResponse uploadAvatar(Path filePath) throws Exception {
|
||||
return apiClient.postMultipart("/api/v1/auth/me/avatar", "avatar", filePath, AvatarUploadResponse.class);
|
||||
}
|
||||
|
||||
public void deleteAvatar() throws Exception {
|
||||
apiClient.delete("/api/v1/auth/me/avatar");
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ public class UserSession {
|
||||
private Role role;
|
||||
private String jwtToken;
|
||||
private Long storeId;
|
||||
private String avatarUrl;
|
||||
|
||||
private UserSession() {}
|
||||
|
||||
@@ -37,6 +38,7 @@ public class UserSession {
|
||||
this.role = null;
|
||||
this.jwtToken = null;
|
||||
this.storeId = null;
|
||||
this.avatarUrl = null;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
@@ -71,6 +73,18 @@ public class UserSession {
|
||||
this.storeId = storeId;
|
||||
}
|
||||
|
||||
public String getAvatarUrl() {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
public void setAvatarUrl(String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
public void setEmployeeName(String employeeName) {
|
||||
this.employeeName = employeeName;
|
||||
}
|
||||
|
||||
public boolean isLoggedIn() {
|
||||
return username != null && role != null;
|
||||
}
|
||||
|
||||
@@ -81,6 +81,8 @@ public class LoginController {
|
||||
}
|
||||
UserSession.getInstance().login(userInfo.getId(), username, role, token);
|
||||
UserSession.getInstance().setStoreId(userInfo.getStoreId());
|
||||
UserSession.getInstance().setEmployeeName(userInfo.getFullName() == null || userInfo.getFullName().isBlank() ? username : userInfo.getFullName());
|
||||
UserSession.getInstance().setAvatarUrl(userInfo.getAvatarUrl());
|
||||
|
||||
openMainLayout();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.example.petshopdesktop.controllers;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
@@ -9,10 +10,19 @@ import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Separator;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.ImagePattern;
|
||||
import javafx.scene.shape.Circle;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.example.petshopdesktop.api.ApiConfig;
|
||||
import org.example.petshopdesktop.api.ChatRealtimeClient;
|
||||
import org.example.petshopdesktop.api.dto.auth.AvatarUploadResponse;
|
||||
import org.example.petshopdesktop.api.dto.auth.UserInfoResponse;
|
||||
import org.example.petshopdesktop.api.endpoints.AuthApi;
|
||||
import org.example.petshopdesktop.auth.UserSession;
|
||||
import org.example.petshopdesktop.ui.SvgWebViewFactory;
|
||||
import org.example.petshopdesktop.util.ActivityLogger;
|
||||
@@ -79,6 +89,15 @@ public class MainLayoutController {
|
||||
@FXML
|
||||
private StackPane logoContainer;
|
||||
|
||||
@FXML
|
||||
private StackPane avatarPreview;
|
||||
|
||||
@FXML
|
||||
private Button btnChangeAvatar;
|
||||
|
||||
@FXML
|
||||
private Button btnRemoveAvatar;
|
||||
|
||||
@FXML
|
||||
private Label lblUsername;
|
||||
|
||||
@@ -184,6 +203,42 @@ public class MainLayoutController {
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
void btnChangeAvatarClicked(ActionEvent event) {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle("Choose Profile Picture");
|
||||
chooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("Image Files", "*.png", "*.jpg", "*.jpeg", "*.gif")
|
||||
);
|
||||
java.io.File file = chooser.showOpenDialog(btnChangeAvatar.getScene().getWindow());
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
AvatarUploadResponse response = AuthApi.getInstance().uploadAvatar(file.toPath());
|
||||
UserSession.getInstance().setAvatarUrl(response.getAvatarUrl());
|
||||
renderAvatar(UserSession.getInstance().getEmployeeName(), response.getAvatarUrl());
|
||||
btnRemoveAvatar.setDisable(response.getAvatarUrl() == null || response.getAvatarUrl().isBlank());
|
||||
} catch (Exception e) {
|
||||
ActivityLogger.getInstance().logException("MainLayoutController.btnChangeAvatarClicked", e, "Uploading avatar");
|
||||
showAvatarError(e.getMessage() != null ? e.getMessage() : "Could not upload profile picture.");
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
void btnRemoveAvatarClicked(ActionEvent event) {
|
||||
try {
|
||||
AuthApi.getInstance().deleteAvatar();
|
||||
UserSession.getInstance().setAvatarUrl(null);
|
||||
renderAvatar(UserSession.getInstance().getEmployeeName(), null);
|
||||
btnRemoveAvatar.setDisable(true);
|
||||
} catch (Exception e) {
|
||||
ActivityLogger.getInstance().logException("MainLayoutController.btnRemoveAvatarClicked", e, "Deleting avatar");
|
||||
showAvatarError(e.getMessage() != null ? e.getMessage() : "Could not remove profile picture.");
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
void btnLogoutClicked(ActionEvent event) {
|
||||
ChatRealtimeClient.getInstance().disconnect();
|
||||
@@ -208,6 +263,9 @@ public class MainLayoutController {
|
||||
@FXML
|
||||
public void initialize() {
|
||||
logoContainer.getChildren().setAll(SvgWebViewFactory.build("/org/example/petshopdesktop/images/leons-pet-store-badge-light.svg", 94));
|
||||
renderAvatar(UserSession.getInstance().getEmployeeName(), UserSession.getInstance().getAvatarUrl());
|
||||
btnRemoveAvatar.setDisable(UserSession.getInstance().getAvatarUrl() == null || UserSession.getInstance().getAvatarUrl().isBlank());
|
||||
refreshProfileHeader();
|
||||
applyRBAC();
|
||||
|
||||
UserSession session = UserSession.getInstance();
|
||||
@@ -220,6 +278,77 @@ public class MainLayoutController {
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshProfileHeader() {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
UserInfoResponse userInfo = AuthApi.getInstance().getCurrentUser();
|
||||
String displayName = userInfo.getFullName() == null || userInfo.getFullName().isBlank()
|
||||
? UserSession.getInstance().getUsername()
|
||||
: userInfo.getFullName();
|
||||
Platform.runLater(() -> {
|
||||
UserSession.getInstance().setEmployeeName(displayName);
|
||||
UserSession.getInstance().setAvatarUrl(userInfo.getAvatarUrl());
|
||||
lblUsername.setText(displayName);
|
||||
renderAvatar(displayName, userInfo.getAvatarUrl());
|
||||
btnRemoveAvatar.setDisable(userInfo.getAvatarUrl() == null || userInfo.getAvatarUrl().isBlank());
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Platform.runLater(() -> renderAvatar(UserSession.getInstance().getEmployeeName(), UserSession.getInstance().getAvatarUrl()));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void renderAvatar(String displayName, String avatarUrl) {
|
||||
Circle border = new Circle(29);
|
||||
border.setFill(Color.web("#dbe4ee"));
|
||||
|
||||
Circle circle = new Circle(26);
|
||||
Label initials = new Label(initials(displayName));
|
||||
initials.setStyle("-fx-text-fill: white; -fx-font-weight: bold; -fx-font-size: 16px;");
|
||||
|
||||
if (avatarUrl != null && !avatarUrl.isBlank()) {
|
||||
try {
|
||||
String resolvedUrl = avatarUrl.startsWith("http") ? avatarUrl : ApiConfig.getInstance().getBaseUrl() + avatarUrl;
|
||||
Image image = new Image(resolvedUrl, 52, 52, true, true, true);
|
||||
if (!image.isError()) {
|
||||
circle.setFill(new ImagePattern(image));
|
||||
initials.setVisible(false);
|
||||
} else {
|
||||
circle.setFill(Color.web("#4ECDC4"));
|
||||
initials.setVisible(true);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
circle.setFill(Color.web("#4ECDC4"));
|
||||
initials.setVisible(true);
|
||||
}
|
||||
} else {
|
||||
circle.setFill(Color.web("#4ECDC4"));
|
||||
initials.setVisible(true);
|
||||
}
|
||||
|
||||
avatarPreview.getChildren().setAll(border, circle, initials);
|
||||
}
|
||||
|
||||
private String initials(String displayName) {
|
||||
if (displayName == null || displayName.isBlank()) {
|
||||
return "?";
|
||||
}
|
||||
|
||||
String[] parts = displayName.trim().split("\\s+");
|
||||
if (parts.length == 1) {
|
||||
return parts[0].substring(0, 1).toUpperCase();
|
||||
}
|
||||
return (parts[0].substring(0, 1) + parts[parts.length - 1].substring(0, 1)).toUpperCase();
|
||||
}
|
||||
|
||||
private void showAvatarError(String message) {
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||
alert.setTitle("Profile Picture");
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
private void applyRBAC() {
|
||||
UserSession session = UserSession.getInstance();
|
||||
|
||||
|
||||
@@ -44,6 +44,34 @@
|
||||
<Insets bottom="4.0" />
|
||||
</padding>
|
||||
</Label>
|
||||
<HBox alignment="CENTER_LEFT" spacing="10.0">
|
||||
<children>
|
||||
<StackPane fx:id="avatarPreview" maxHeight="58.0" maxWidth="58.0" minHeight="58.0" minWidth="58.0" prefHeight="58.0" prefWidth="58.0" />
|
||||
<VBox spacing="6.0">
|
||||
<children>
|
||||
<Button fx:id="btnChangeAvatar" mnemonicParsing="false" onAction="#btnChangeAvatarClicked" style="-fx-background-color: rgba(255,255,255,0.12); -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Change Photo" textFill="#e2e8f0">
|
||||
<font>
|
||||
<Font name="System" size="11.0" />
|
||||
</font>
|
||||
<padding>
|
||||
<Insets bottom="6.0" left="10.0" right="10.0" top="6.0" />
|
||||
</padding>
|
||||
</Button>
|
||||
<Button fx:id="btnRemoveAvatar" mnemonicParsing="false" onAction="#btnRemoveAvatarClicked" style="-fx-background-color: transparent; -fx-border-color: rgba(226,232,240,0.35); -fx-border-radius: 8; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Remove" textFill="#cbd5e1">
|
||||
<font>
|
||||
<Font name="System" size="11.0" />
|
||||
</font>
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="10.0" right="10.0" top="5.0" />
|
||||
</padding>
|
||||
</Button>
|
||||
</children>
|
||||
</VBox>
|
||||
</children>
|
||||
<VBox.margin>
|
||||
<Insets bottom="8.0" top="4.0" />
|
||||
</VBox.margin>
|
||||
</HBox>
|
||||
<Separator prefWidth="200.0" style="-fx-background-color: #444444; -fx-opacity: 0.35;" />
|
||||
</children>
|
||||
</VBox>
|
||||
|
||||
Reference in New Issue
Block a user