Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions exports/my_auctions_export.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
AuctionID,Title,Category,StartingPrice,FinalPrice,Winner,Status,StartTime,EndTime
Original file line number Diff line number Diff line change
@@ -1,51 +1,281 @@
package com.auction.client.controllers;

import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import com.auction.client.core.ClientContext;
import com.auction.client.service.PollingService;
import com.auction.shared.models.AuctionItem;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TextField;
import javafx.animation.PauseTransition;
import javafx.animation.ScaleTransition;
import javafx.util.Duration;
import javafx.stage.Popup;
import com.auction.client.service.ThumbnailExecutor;

import java.io.IOException;
import java.io.InputStream;

public class AuctionDetailController {

@FXML private VBox bidderControls;
@FXML private VBox sellerControls;
@FXML private Label controlsTitleLabel;
@FXML private Label controlsDescLabel;
@FXML private Label auctionTitleLabel;
@FXML private Label auctionDescriptionLabel;
@FXML private Label currentBidLabel;
@FXML private Label timeLeftLabel;
@FXML private Label highestBidderLabel;
@FXML private Button placeBidButton;

private PollingService pollingService;
private int currentAuctionId = -1;
private com.auction.shared.models.AuctionItem currentItem;
@FXML private TextField bidAmountField;
@FXML private Label bidStatusLabel;
@FXML private ProgressIndicator bidSpinner;
@FXML private javafx.scene.image.ImageView heroImageView;
@FXML private javafx.scene.image.ImageView thumb1View;
@FXML private javafx.scene.image.ImageView thumb2View;
@FXML private javafx.scene.image.ImageView thumb3View;
private final java.util.concurrent.Executor executor = java.util.concurrent.Executors.newCachedThreadPool();
private static final javafx.scene.image.Image PLACEHOLDER_IMAGE = loadPlaceholderImage();

@FXML
public void initialize() {
int auctionId = ClientContext.getInstance().getCurrentAuctionId();
if (auctionId != -1) {
// nothing; detail view will be initialized via loadAuction(int)
}

public void loadAuction(int auctionId) {
this.currentAuctionId = auctionId;
try {
var service = ClientContext.getInstance().getRmiProvider().getService();
this.pollingService = new PollingService(service);
// load initial item state
AuctionItem initial = service.getAuctionById(auctionId);
this.currentItem = initial;
Platform.runLater(() -> updateUi(initial));
// load thumbnails for detail view
loadDetailThumbnail(auctionId, 0, heroImageView);
loadDetailThumbnail(auctionId, 1, thumb1View);
loadDetailThumbnail(auctionId, 2, thumb2View);
loadDetailThumbnail(auctionId, 3, thumb3View);
pollingService.startPolling(auctionId, item -> {
this.currentItem = item;
Platform.runLater(() -> updateUi(item));
});
Comment on lines +58 to +66
} catch (Exception e) {
e.printStackTrace();
}
}
Comment on lines +49 to +70

private void updateUi(AuctionItem item) {
if (item == null) return;
if (auctionTitleLabel != null) {
auctionTitleLabel.setText(item.getTitle() == null || item.getTitle().isBlank() ? "Auction Detail" : item.getTitle());
}
if (auctionDescriptionLabel != null) {
String desc = (item.getDescription() == null || item.getDescription().isBlank())
? "No description provided"
: item.getDescription();
auctionDescriptionLabel.setText(desc);
}
currentBidLabel.setText(com.auction.shared.Constants.formatCents(item.getCurrentBidCents()));
highestBidderLabel.setText(item.getHighestBidderUsername() == null ? "N/A" : item.getHighestBidderUsername());
if (timeLeftLabel != null) {
timeLeftLabel.setText(formatTimeLeft(item.getEndTime()));
}
}

private String formatTimeLeft(String endTimeIso) {
if (endTimeIso == null || endTimeIso.isBlank()) return "--:--";
try {
java.time.Instant end = java.time.Instant.parse(endTimeIso);
java.time.Duration d = java.time.Duration.between(java.time.Instant.now(), end);
if (d.isNegative() || d.isZero()) return "Ended";
long hours = d.toHours();
long minutes = d.minusHours(hours).toMinutes();
long seconds = d.minusHours(hours).minusMinutes(minutes).toSeconds();
if (hours > 0) {
return String.format("%02dh %02dm", hours, minutes);
}
return String.format("%02dm %02ds", minutes, seconds);
} catch (Exception ignored) {
return "--:--";
}
}

public void shutdown() {
if (pollingService != null) pollingService.shutdown();
}

@FXML
private void handlePlaceBid() {
if (currentAuctionId < 0) return;
String input = bidAmountField.getText();
if (input == null || input.trim().isEmpty()) {
bidStatusLabel.setText("Enter bid amount");
return;
}

double amount;
try {
amount = Double.parseDouble(input.trim());
} catch (NumberFormatException nfe) {
bidStatusLabel.setText("Invalid amount format");
return;
}

long amountCents = Math.round(amount * 100);
long expected = currentItem == null ? 0L : currentItem.getCurrentBidCents();

// optimistic UI update
long prevBid = currentItem == null ? 0L : currentItem.getCurrentBidCents();
String prevHighest = currentItem == null ? null : currentItem.getHighestBidderUsername();

var context = ClientContext.getInstance();
String you = context.getUsername() == null ? "You" : context.getUsername();

// apply optimistic change
if (currentItem != null) {
currentItem.setCurrentBidCents(amountCents);
currentItem.setHighestBidderUsername(you);
}
Platform.runLater(() -> {
updateUi(currentItem);
placeBidButton.setDisable(true);
bidAmountField.setDisable(true);
bidSpinner.setVisible(true);
bidStatusLabel.setText("Submitting...");
});

java.util.concurrent.CompletableFuture.runAsync(() -> {
try {
AuctionItem auction = ClientContext.getInstance().getRmiProvider().getService().getAuctionById(auctionId);
if (auction != null) {
String currentUsername = ClientContext.getInstance().getUsername();
if (currentUsername.equals(auction.getSellerUsername())) {
bidderControls.setVisible(false);
bidderControls.setManaged(false);
sellerControls.setVisible(true);
sellerControls.setManaged(true);
if (controlsTitleLabel != null) controlsTitleLabel.setText("Management Settings");
if (controlsDescLabel != null) controlsDescLabel.setText("As the seller, you can manage this auction here.");
} else {
sellerControls.setVisible(false);
sellerControls.setManaged(false);
bidderControls.setVisible(true);
bidderControls.setManaged(true);
if (controlsTitleLabel != null) controlsTitleLabel.setText("Bid Controls");
if (controlsDescLabel != null) controlsDescLabel.setText("Place your bids and monitor status here.");
var service = context.getRmiProvider().getService();
service.placeBid(currentAuctionId, amountCents, expected, context.getSessionToken());
Platform.runLater(() -> {
bidStatusLabel.setText("Bid submitted");
bidAmountField.clear();
// show success toast and highlight hero image
showToast("Bid placed");
animateSuccess();
});
} catch (Exception e) {
// parse server-side AuctionException if present
String userMsg = "Failed to place bid";
Throwable cause = e;
while (cause != null) {
if (cause instanceof com.auction.shared.exceptions.AuctionException) {
userMsg = cause.getMessage();
break;
}
cause = cause.getCause();
}
final String finalMsg = userMsg;
// rollback optimistic update
if (currentItem != null) {
currentItem.setCurrentBidCents(prevBid);
currentItem.setHighestBidderUsername(prevHighest);
}
Platform.runLater(() -> {
updateUi(currentItem);
bidStatusLabel.setText("Failed: " + finalMsg);
});
} finally {
Platform.runLater(() -> {
placeBidButton.setDisable(false);
bidAmountField.setDisable(false);
bidSpinner.setVisible(false);
});
}
}, executor);
}

private void loadDetailThumbnail(int auctionId, int index, javafx.scene.image.ImageView target) {
java.util.concurrent.CompletableFuture.supplyAsync(() -> {
try {
var service = ClientContext.getInstance().getRmiProvider().getService();
byte[] bytes = service.getThumbnail(auctionId, index);
if (bytes == null || bytes.length == 0) return null;
return new javafx.scene.image.Image(new java.io.ByteArrayInputStream(bytes));
} catch (Exception e) {
e.printStackTrace();
return null;
}
}, ThumbnailExecutor.getExecutor()).thenAccept(image -> {
if (image != null) {
Platform.runLater(() -> {
target.setImage(image);
target.setStyle(null);
});
} else {
Platform.runLater(() -> {
target.setImage(PLACEHOLDER_IMAGE);
target.setStyle(null);
});
}
});
}

@FXML
private void handleThumb1Click(javafx.scene.input.MouseEvent e) {

Check notice

Code scanning / CodeQL

Useless parameter Note

The parameter 'e' is never used.
if (thumb1View.getImage() != null) heroImageView.setImage(thumb1View.getImage());
}

@FXML
private void handleThumb2Click(javafx.scene.input.MouseEvent e) {

Check notice

Code scanning / CodeQL

Useless parameter Note

The parameter 'e' is never used.
if (thumb2View.getImage() != null) heroImageView.setImage(thumb2View.getImage());
}

@FXML
private void handleThumb3Click(javafx.scene.input.MouseEvent e) {

Check notice

Code scanning / CodeQL

Useless parameter Note

The parameter 'e' is never used.
if (thumb3View.getImage() != null) heroImageView.setImage(thumb3View.getImage());
}

// allow gallery to request showing a particular hero index after loading
public void showHeroImageIndex(int index) {
switch (index) {
case 0: if (heroImageView.getImage() != null) heroImageView.setImage(heroImageView.getImage()); break;

Check warning

Code scanning / CodeQL

Misleading indentation Warning

Indentation suggests that
the next statement
belongs to
the control structure
, but this is not the case; consider adding braces or adjusting indentation.
case 1: if (thumb1View.getImage() != null) heroImageView.setImage(thumb1View.getImage()); break;

Check warning

Code scanning / CodeQL

Misleading indentation Warning

Indentation suggests that
the next statement
belongs to
the control structure
, but this is not the case; consider adding braces or adjusting indentation.
case 2: if (thumb2View.getImage() != null) heroImageView.setImage(thumb2View.getImage()); break;

Check warning

Code scanning / CodeQL

Misleading indentation Warning

Indentation suggests that
the next statement
belongs to
the control structure
, but this is not the case; consider adding braces or adjusting indentation.
default: break;
}
Comment on lines +235 to 242
// TODO: setup polling for updates, handle bid placement
}

public void shutdown() {
// TODO: stop polling thread

private void animateSuccess() {
if (heroImageView == null) return;
ScaleTransition st = new ScaleTransition(Duration.millis(300), heroImageView);
st.setFromX(1.0);
st.setFromY(1.0);
st.setToX(1.06);
st.setToY(1.06);
st.setAutoReverse(true);
st.setCycleCount(2);
st.play();
}

private void showToast(String message) {
try {
Label lbl = new Label(message);
lbl.setStyle("-fx-background-color: rgba(40,160,67,0.95); -fx-text-fill: white; -fx-padding: 8px 12px; -fx-background-radius: 6px;");
Popup popup = new Popup();
popup.getContent().add(lbl);
javafx.geometry.Bounds b = heroImageView.localToScreen(heroImageView.getBoundsInLocal());
double x = b.getMinX() + b.getWidth() - 10;
double y = b.getMinY() + 10;
popup.show(heroImageView.getScene().getWindow(), x, y);
PauseTransition pt = new PauseTransition(Duration.seconds(1.6));
pt.setOnFinished(evt -> popup.hide());
pt.play();
} catch (Exception ignored) {}
}

private static javafx.scene.image.Image loadPlaceholderImage() {
InputStream stream = AuctionDetailController.class.getResourceAsStream("/images/placeholder.png");
if (stream == null) {
throw new IllegalStateException("Missing resource: /images/placeholder.png");
}
return new javafx.scene.image.Image(stream);
}

@FXML
Expand All @@ -61,8 +291,14 @@
@FXML
private void handleBackToGallery() {
try {
com.auction.client.core.ClientContext.getInstance().getViewLoader().loadView("gallery.fxml");
} catch (java.io.IOException e) {
shutdown();
ClientContext context = ClientContext.getInstance();
String targetView = context.getPreviousViewName();
if (targetView == null || targetView.isBlank()) {
targetView = "gallery.fxml";
}
context.getViewLoader().loadView(targetView);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,28 @@ public void initialize() {
com.auction.client.core.ClientContext context = com.auction.client.core.ClientContext.getInstance();
context.getUdpClient().startListening();

javafx.application.Platform.runLater(() -> statusLabel.setText("Waiting for discovered servers. You can edit IP and port manually."));

// Start a background thread to update the list view
Thread updateThread = new Thread(() -> {
while (true) {
try {
Thread.sleep(1000);
java.util.List<com.auction.client.network.UdpDiscoveryClient.ServerInfo> servers = context.getUdpClient().getDiscoveredServers();
javafx.application.Platform.runLater(() -> {
serverListView.getItems().setAll(servers);
if (servers == null || servers.isEmpty()) {
serverListView.getItems().clear();
if (ipField.getText() == null || ipField.getText().isBlank()) {
ipField.setText("localhost");
}
if (portField.getText() == null || portField.getText().isBlank()) {
portField.setText("1099");
}
statusLabel.setText("No discovered server yet. Localhost is prefilled and can be edited.");
} else {
serverListView.getItems().setAll(servers);
statusLabel.setText("Discovered " + servers.size() + " server(s).");
}
});
} catch (InterruptedException e) {
break;
Expand Down
Loading
Loading