Skip to content
Merged
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
79 changes: 77 additions & 2 deletions src/main/java/com/studysync/presentation/ui/StudySyncUI.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
Expand Down Expand Up @@ -65,6 +67,9 @@ public class StudySyncUI {
private final Map<Tab, RefreshablePanel> panelMap;
private TabPane tabPane;
private StackPane overlayLayer;
private Button syncStatusBadge;
/** Bumped on every badge update; stale async checks compare against it. FX thread only. */
private int syncBadgeGeneration;

@Autowired
public StudySyncUI(TaskService taskService,
Expand Down Expand Up @@ -206,7 +211,7 @@ private HBox createHeader() {
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);

javafx.scene.control.Button profileButton = new javafx.scene.control.Button("Profile");
Button profileButton = new Button("Profile");
profileButton.setGraphic(TaskStyleUtils.iconLabel("\u263B", 14));
profileButton.setStyle("-fx-background-color: rgba(255,255,255,0.2); -fx-text-fill: white; "
+ "-fx-border-color: rgba(255,255,255,0.5); -fx-border-radius: 5; -fx-background-radius: 5; "
Expand All @@ -219,19 +224,85 @@ private HBox createHeader() {
+ "-fx-background-radius: 5; -fx-font-weight: bold;"));
profileButton.setOnAction(e -> showProfileWindow());

header.getChildren().addAll(appTitle, spacer, profileButton);
syncStatusBadge = new Button();
syncStatusBadge.setStyle(syncBadgeStyle("white"));
syncStatusBadge.setTooltip(new Tooltip(
"Shows whether this device or Google Drive holds the most up-to-date database. Click to re-check."));
syncStatusBadge.setOnAction(e -> refreshSyncStatusBadge());
setSyncBadgeVisible(false);

header.setSpacing(12);
header.getChildren().addAll(appTitle, spacer, syncStatusBadge, profileButton);
return header;
}

private static String syncBadgeStyle(String textColor) {
return "-fx-background-color: rgba(255,255,255,0.15); -fx-background-radius: 12; "
+ "-fx-padding: 4 10 4 10; -fx-font-weight: bold; -fx-font-size: 12px; "
+ "-fx-text-fill: " + textColor + ";";
}

/** Re-checks Drive sync state off the FX thread and updates the header badge. */
private void refreshSyncStatusBadge() {
// Stale-result fence: only the newest in-flight check may update the
// badge, so a check started before sign-out can't resurface it.
int generation = ++syncBadgeGeneration;
syncStatusBadge.setText("Checking sync…");
syncStatusBadge.setGraphic(null);
setSyncBadgeVisible(true);
CompletableFuture.supplyAsync(googleDriveService::checkSyncStatus)
.whenComplete((status, error) -> Platform.runLater(() -> {
if (generation != syncBadgeGeneration) {
return;
}
applySyncStatusBadge(error != null
? GoogleDriveService.SyncStatus.UNKNOWN : status);
}));
}

private void applySyncStatusBadge(GoogleDriveService.SyncStatus status) {
// Direct applies are authoritative: invalidate any in-flight check.
syncBadgeGeneration++;
if (status == GoogleDriveService.SyncStatus.DISABLED
|| !googleDriveService.isIntegrationEnabled()
|| !googleDriveService.isSignedIn()) {
setSyncBadgeVisible(false);
return;
}
String icon;
String text;
String color;
switch (status) {
case UP_TO_DATE -> { icon = "\u2713"; text = "In sync"; color = "#b9f6ca"; }
case LOCAL_NEWER -> { icon = "\u2191"; text = "Local is newer"; color = "white"; }
case DRIVE_NEWER -> { icon = "\u2193"; text = "Drive is newer"; color = "white"; }
case CONFLICT -> { icon = "\u26A0"; text = "Sync conflict"; color = "#ffe082"; }
default -> { icon = "?"; text = "Sync unknown"; color = "white"; }
}
Label iconLabel = TaskStyleUtils.iconLabel(icon, 12);
iconLabel.setTextFill(Color.web(color.equals("white") ? "#ffffff" : color));
syncStatusBadge.setGraphic(iconLabel);
syncStatusBadge.setText(text);
syncStatusBadge.setStyle(syncBadgeStyle(color));
setSyncBadgeVisible(true);
}

private void setSyncBadgeVisible(boolean visible) {
syncStatusBadge.setVisible(visible);
syncStatusBadge.setManaged(visible);
}

private void resolveDriveSyncOnStartup() {
if (googleDriveService.hasPendingDownload() || googleDriveService.hasFailedPendingDownload()) {
refreshSyncStatusBadge();
showStartupAlert("Google Drive Sync",
"A staged Drive download could not be fully applied. "
+ "Resolve it from the Profile window before StudySync mutates delayed tasks or goals.");
return;
}

if (!googleDriveService.isIntegrationEnabled() || !googleDriveService.isSignedIn()) {
applySyncStatusBadge(GoogleDriveService.SyncStatus.DISABLED);
runStartupMaintenance();
return;
}
Expand All @@ -248,6 +319,7 @@ private void resolveDriveSyncOnStartup() {
}

private void handleStartupSyncStatus(GoogleDriveService.SyncStatus status) {
applySyncStatusBadge(status);
switch (status) {
case DRIVE_NEWER -> showStartupAlert("Google Drive Sync",
"A newer database is available on Google Drive. Download it from the Profile window before "
Expand Down Expand Up @@ -321,6 +393,9 @@ private void showProfileWindow() {
profileScene.getStylesheets().add(getClass().getResource("/styles.css").toExternalForm());

profileStage.setScene(profileScene);
// Drive actions (upload/download/sign-in) happen in the Profile window,
// so the header badge re-checks once it closes.
profileStage.setOnHidden(e -> refreshSyncStatusBadge());
profileStage.setMinWidth(900);
profileStage.setMinHeight(600);
profileStage.setWidth(1000);
Expand Down
Loading