nearly done, still needs a bit of work
This commit is contained in:
parent
a659e1217c
commit
be2d918620
@ -165,6 +165,7 @@ public class UserRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private static <T extends User> List<T> findAll(
|
private static <T extends User> List<T> findAll(
|
||||||
Path dir, Class<T> type, String lockPrefix) throws Exception {
|
Path dir, Class<T> type, String lockPrefix) throws Exception {
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import javafx.application.Platform;
|
|||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
import javafx.scene.Parent;
|
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.input.Dragboard;
|
import javafx.scene.input.Dragboard;
|
||||||
@ -261,27 +260,42 @@ public class LoginController {
|
|||||||
private void navigateToUserUI(User user) {
|
private void navigateToUserUI(User user) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (user instanceof Organizer organizer) {
|
|
||||||
|
if (user instanceof Organizer) {
|
||||||
FXMLLoader loader = new FXMLLoader(
|
FXMLLoader loader = new FXMLLoader(
|
||||||
getClass().getResource("/fxml/OrganizerView.fxml"));
|
getClass().getResource("/fxml/OrganizerView.fxml"));
|
||||||
Parent root = loader.load();
|
Scene oldScene = stage.getScene();
|
||||||
|
Scene scene = new Scene(loader.load(),
|
||||||
|
oldScene.getWidth(),
|
||||||
|
oldScene.getHeight()
|
||||||
|
);
|
||||||
OrganizerController controller = loader.getController();
|
OrganizerController controller = loader.getController();
|
||||||
controller.setOrganizer(organizer);
|
controller.setOrganizer((Organizer) user);
|
||||||
controller.setStage(stage);
|
controller.setStage(stage);
|
||||||
stage.setScene(new Scene(root));
|
|
||||||
|
|
||||||
} else if (user instanceof Voter voter) {
|
stage.setScene(scene);
|
||||||
|
stage.setTitle(((Organizer) user).getName());
|
||||||
|
|
||||||
|
|
||||||
|
} else if (user instanceof Voter) {
|
||||||
FXMLLoader loader = new FXMLLoader(
|
FXMLLoader loader = new FXMLLoader(
|
||||||
getClass().getResource("/fxml/VoterView.fxml"));
|
getClass().getResource("/fxml/VoterView.fxml"));
|
||||||
Parent root = loader.load();
|
Scene oldScene = stage.getScene();
|
||||||
|
Scene scene = new Scene(loader.load(),
|
||||||
|
oldScene.getWidth(),
|
||||||
|
oldScene.getHeight()
|
||||||
|
);
|
||||||
VoterController controller = loader.getController();
|
VoterController controller = loader.getController();
|
||||||
controller.setVoter(voter);
|
controller.setVoter((Voter) user);
|
||||||
controller.setStage(stage);
|
controller.setStage(stage);
|
||||||
stage.setScene(new Scene(root));
|
|
||||||
|
stage.setScene(scene);
|
||||||
|
stage.setTitle(((Voter) user).getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
showError("Failed to load UI: " + e.getMessage());
|
showError("Failed to load UI: " + e.getMessage());
|
||||||
|
System.out.println(e);
|
||||||
loginButton.setDisable(false);
|
loginButton.setDisable(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,432 @@
|
|||||||
package dev.ksan.ui;
|
package dev.ksan.ui;
|
||||||
|
|
||||||
|
import dev.ksan.CASystem.KeystoreService;
|
||||||
import dev.ksan.Users.Organizer;
|
import dev.ksan.Users.Organizer;
|
||||||
|
import dev.ksan.voteSystem.*;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.scene.text.Text;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class OrganizerController {
|
public class OrganizerController {
|
||||||
private Organizer organizer;
|
private Organizer organizer;
|
||||||
|
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label usernameLabel;
|
||||||
|
|
||||||
|
@FXML private Label errorLabel;
|
||||||
|
|
||||||
|
@FXML private VBox panesBox;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TitledPane activeList;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TitledPane inactiveList;
|
||||||
|
@FXML
|
||||||
|
private ListView<Poll> activePollsListView;
|
||||||
|
@FXML
|
||||||
|
private ListView<Poll> inactivePollsListView;
|
||||||
|
@FXML
|
||||||
|
private Button newButton;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private StackPane centerPane;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TextField descriptionField;
|
||||||
|
@FXML
|
||||||
|
private TextField titleField;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private DatePicker startDatePicker;
|
||||||
|
@FXML
|
||||||
|
private DatePicker endDatePicker;
|
||||||
|
@FXML
|
||||||
|
private Button confirmButton;
|
||||||
|
@FXML
|
||||||
|
private Button newOptionButton;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TextField startTimeField;
|
||||||
|
@FXML
|
||||||
|
private TextField endTimeField;
|
||||||
|
@FXML
|
||||||
|
private VBox optionsContainer;
|
||||||
|
|
||||||
private Stage stage;
|
private Stage stage;
|
||||||
|
|
||||||
|
private final List<TextField> optionFields = new ArrayList<>();
|
||||||
|
|
||||||
public void setOrganizer(Organizer organizer) {
|
public void setOrganizer(Organizer organizer) {
|
||||||
this.organizer = organizer;
|
this.organizer = organizer;
|
||||||
|
usernameLabel.setText(organizer.getName());
|
||||||
|
|
||||||
|
loadPolls();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setStage(Stage stage) {
|
public void setStage(Stage stage) {
|
||||||
this.stage = stage;
|
this.stage = stage;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void initialize() {
|
||||||
|
errorLabel.setVisible(false);
|
||||||
|
confirmButton.setOnAction(e -> createNewPoll());
|
||||||
|
newOptionButton.setOnAction(e -> addOptionField());
|
||||||
|
newButton.setOnAction(e -> showCreatePollForm());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private void addOptionField() {
|
||||||
|
if (optionFields.size() >= 5) return; // max 5 options
|
||||||
|
|
||||||
|
HBox optionBox = new HBox(10);
|
||||||
|
Text label = new Text("option " + (optionFields.size() + 1) + ":");
|
||||||
|
TextField textField = new TextField();
|
||||||
|
|
||||||
|
optionBox.getChildren().addAll(label, textField);
|
||||||
|
optionsContainer.getChildren().add(optionBox);
|
||||||
|
|
||||||
|
optionFields.add(textField);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createNewPoll() {
|
||||||
|
clearError();
|
||||||
|
|
||||||
|
if (titleField.getText() == null || titleField.getText().isBlank()) {
|
||||||
|
showError("Title is required."); return;
|
||||||
|
}
|
||||||
|
if (descriptionField.getText() == null || descriptionField.getText().isBlank()) {
|
||||||
|
showError("Description is required."); return;
|
||||||
|
}
|
||||||
|
if (startDatePicker.getValue() == null) {
|
||||||
|
showError("Start date is required."); return;
|
||||||
|
}
|
||||||
|
if (endDatePicker.getValue() == null) {
|
||||||
|
showError("End date is required."); return;
|
||||||
|
}
|
||||||
|
if (startTimeField.getText() == null || startTimeField.getText().isBlank()) {
|
||||||
|
showError("Start time is required."); return;
|
||||||
|
}
|
||||||
|
if (endTimeField.getText() == null || endTimeField.getText().isBlank()) {
|
||||||
|
showError("End time is required."); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalTime startTime = parseTime(startTimeField.getText());
|
||||||
|
LocalTime endTime = parseTime(endTimeField.getText());
|
||||||
|
|
||||||
|
if (startTime == null) {
|
||||||
|
showError("Invalid start time. Use HH:MM format."); return;
|
||||||
|
}
|
||||||
|
if (endTime == null) {
|
||||||
|
showError("Invalid end time. Use HH:MM format."); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime startDateTime = LocalDateTime.of(startDatePicker.getValue(), startTime);
|
||||||
|
LocalDateTime endDateTime = LocalDateTime.of(endDatePicker.getValue(), endTime);
|
||||||
|
|
||||||
|
if (startDateTime.isBefore(LocalDateTime.now())) {
|
||||||
|
showError("Start time cannot be in the past."); return;
|
||||||
|
}
|
||||||
|
if (!endDateTime.isAfter(startDateTime)) {
|
||||||
|
showError("End time must be after start time."); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> optionTexts = optionFields.stream()
|
||||||
|
.map(tf -> tf.getText().trim())
|
||||||
|
.filter(t -> !t.isEmpty())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (optionTexts.size() < 2) {
|
||||||
|
showError("At least 2 options are required."); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long distinctCount = optionTexts.stream().distinct().count();
|
||||||
|
if (distinctCount != optionTexts.size()) {
|
||||||
|
showError("Options must be unique."); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmButton.setDisable(true);
|
||||||
|
|
||||||
|
Task<Poll> task = new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected Poll call() throws Exception {
|
||||||
|
Poll poll = new Poll(
|
||||||
|
organizer.getId(),
|
||||||
|
titleField.getText().trim(),
|
||||||
|
descriptionField.getText().trim(),
|
||||||
|
startDateTime,
|
||||||
|
endDateTime
|
||||||
|
);
|
||||||
|
|
||||||
|
for (String text : optionTexts) {
|
||||||
|
poll.addOption(new PollOption(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
PollRepository.save(poll);
|
||||||
|
return poll;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
task.setOnSucceeded(e -> {
|
||||||
|
Poll created = task.getValue();
|
||||||
|
System.out.println("Poll created: " + created.getTitle());
|
||||||
|
confirmButton.setDisable(false);
|
||||||
|
clearForm();
|
||||||
|
loadPolls(); // refresh the poll lists
|
||||||
|
});
|
||||||
|
|
||||||
|
task.setOnFailed(e -> {
|
||||||
|
showError("Failed to create poll: " + task.getException().getMessage());
|
||||||
|
confirmButton.setDisable(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
new Thread(task).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearForm() {
|
||||||
|
titleField.clear();
|
||||||
|
descriptionField.clear();
|
||||||
|
startDatePicker.setValue(null);
|
||||||
|
endDatePicker.setValue(null);
|
||||||
|
startTimeField.clear();
|
||||||
|
endTimeField.clear();
|
||||||
|
optionsContainer.getChildren().clear();
|
||||||
|
optionFields.clear();
|
||||||
|
clearError();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void loadPolls() {
|
||||||
|
Task<List<Poll>> task = new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected List<Poll> call() throws Exception {
|
||||||
|
return PollRepository.findByOrganizer(organizer.getId());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
task.setOnSucceeded(e -> {
|
||||||
|
|
||||||
|
List<Poll> polls = task.getValue();
|
||||||
|
|
||||||
|
List<Poll> active = polls.stream()
|
||||||
|
.filter(p -> p.getStatus() == PollStatus.ACTIVE)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
List<Poll> inactive = polls.stream()
|
||||||
|
.filter(p -> p.getStatus() != PollStatus.ACTIVE)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
activePollsListView.setItems(FXCollections.observableArrayList(active));
|
||||||
|
inactivePollsListView.setItems(FXCollections.observableArrayList(inactive));
|
||||||
|
activePollsListView.setCellFactory(lv -> new PollListCell());
|
||||||
|
inactivePollsListView.setCellFactory(lv -> new PollListCell());
|
||||||
|
});
|
||||||
|
|
||||||
|
task.setOnFailed(e ->
|
||||||
|
showError("Failed to load polls: " + task.getException().getMessage()));
|
||||||
|
|
||||||
|
new Thread(task).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PollListCell extends ListCell<Poll> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateItem(Poll poll, boolean empty) {
|
||||||
|
super.updateItem(poll, empty);
|
||||||
|
|
||||||
|
if (empty || poll == null) {
|
||||||
|
setGraphic(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
VBox card = new VBox(4);
|
||||||
|
card.setPadding(new Insets(8));
|
||||||
|
|
||||||
|
Label title = new Label(poll.getTitle());
|
||||||
|
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
||||||
|
|
||||||
|
Label period = new Label(
|
||||||
|
poll.getStartTime().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))
|
||||||
|
+ " → " +
|
||||||
|
poll.getEndTime().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))
|
||||||
|
);
|
||||||
|
period.setStyle("-fx-text-fill: #666; -fx-font-size: 11;");
|
||||||
|
|
||||||
|
Label status = new Label(formatStatus(poll));
|
||||||
|
status.setStyle("-fx-font-size: 11; -fx-text-fill: " + statusColor(poll) + ";");
|
||||||
|
|
||||||
|
card.getChildren().addAll(title, period, status);
|
||||||
|
|
||||||
|
HBox buttons = new HBox(8);
|
||||||
|
|
||||||
|
if (poll.isReadyForTally()) {
|
||||||
|
Button tallyBtn = new Button("Count Votes");
|
||||||
|
tallyBtn.setStyle("-fx-background-color: #4A90D9; -fx-text-fill: white;");
|
||||||
|
tallyBtn.setOnAction(e -> handleTally(poll));
|
||||||
|
buttons.getChildren().add(tallyBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (poll.isTallied()) {
|
||||||
|
Button resultsBtn = new Button("View Results");
|
||||||
|
resultsBtn.setOnAction(e -> handleViewResults(poll));
|
||||||
|
buttons.getChildren().add(resultsBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!buttons.getChildren().isEmpty()) {
|
||||||
|
card.getChildren().add(buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
setGraphic(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleViewResults(Poll poll) {
|
||||||
|
Task<TallyResult> task = new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected TallyResult call() throws Exception {
|
||||||
|
return TallyResultRepository.findByPoll(poll.getPollId());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
task.setOnSucceeded(e -> {
|
||||||
|
TallyResult result = task.getValue();
|
||||||
|
if (result == null) { showError("Results not found."); return; }
|
||||||
|
|
||||||
|
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
||||||
|
alert.setTitle("Results — " + poll.getTitle());
|
||||||
|
alert.setHeaderText(null);
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
result.getResultsByOption().forEach((option, count) ->
|
||||||
|
sb.append(option).append(": ").append(count).append(" votes\n"));
|
||||||
|
sb.append("\nValid: ").append(result.getValidVotes());
|
||||||
|
sb.append("\nInvalid: ").append(result.getInvalidVotes());
|
||||||
|
|
||||||
|
alert.setContentText(sb.toString());
|
||||||
|
alert.showAndWait();
|
||||||
|
});
|
||||||
|
|
||||||
|
task.setOnFailed(e ->
|
||||||
|
showError("Could not load results: " + task.getException().getMessage()));
|
||||||
|
|
||||||
|
new Thread(task).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private String formatStatus(Poll poll) {
|
||||||
|
return switch (poll.getStatus()) {
|
||||||
|
case PENDING -> "⏳ Pending";
|
||||||
|
case ACTIVE -> "✅ Active";
|
||||||
|
case FINISHED -> poll.isTallied() ? "📊 Tallied" : "🔒 Finished";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String statusColor(Poll poll) {
|
||||||
|
return switch (poll.getStatus()) {
|
||||||
|
case PENDING -> "#F5A623";
|
||||||
|
case ACTIVE -> "#7ED321";
|
||||||
|
case FINISHED -> "#9B9B9B";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleTally(Poll poll) {
|
||||||
|
|
||||||
|
TextInputDialog dialog = new TextInputDialog();
|
||||||
|
dialog.setTitle("Tally Votes");
|
||||||
|
dialog.setHeaderText("Enter your password to decrypt and count votes.");
|
||||||
|
dialog.setContentText("Password:");
|
||||||
|
|
||||||
|
PasswordField pf = new PasswordField();
|
||||||
|
dialog.getDialogPane().setContent(pf);
|
||||||
|
|
||||||
|
dialog.showAndWait().ifPresent(ignored -> {
|
||||||
|
String password = pf.getText();
|
||||||
|
if (password.isBlank()) return;
|
||||||
|
|
||||||
|
Task<TallyResult> task = new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected TallyResult call() throws Exception {
|
||||||
|
char[] ksPassword = KeystoreService.deriveKeystorePassword(
|
||||||
|
password, organizer.getId());
|
||||||
|
PrivateKey privateKey = KeystoreService.getPrivateKey(
|
||||||
|
organizer.getId(), ksPassword);
|
||||||
|
return TallyService.tallyPoll(poll, organizer, privateKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
task.setOnSucceeded(e -> {
|
||||||
|
TallyResult result = task.getValue();
|
||||||
|
result.printReport();
|
||||||
|
loadPolls();
|
||||||
|
});
|
||||||
|
|
||||||
|
task.setOnFailed(e ->
|
||||||
|
showError("Tally failed: " + task.getException().getMessage()));
|
||||||
|
|
||||||
|
new Thread(task).start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static LocalTime parseTime(String input) {
|
||||||
|
if (input == null || input.trim().isEmpty()) return null;
|
||||||
|
|
||||||
|
input = input.trim().replaceAll("\\s+", ":"); // "7 30" to "7:30"
|
||||||
|
|
||||||
|
String[] parts = input.split(":");
|
||||||
|
|
||||||
|
try {
|
||||||
|
int hours = Integer.parseInt(parts[0]);
|
||||||
|
int minutes = (parts.length > 1) ? Integer.parseInt(parts[1]) : 0;
|
||||||
|
|
||||||
|
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return LocalTime.of(hours, minutes);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showError(String msg) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
errorLabel.setText(msg);
|
||||||
|
errorLabel.setVisible(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearError() {
|
||||||
|
errorLabel.setText("");
|
||||||
|
errorLabel.setVisible(false);
|
||||||
|
}
|
||||||
|
private void showCreatePollForm() {
|
||||||
|
centerPane.setVisible(true);
|
||||||
|
clearForm();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
5
src/main/java/dev/ksan/ui/PollListCell.java
Normal file
5
src/main/java/dev/ksan/ui/PollListCell.java
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package dev.ksan.ui;
|
||||||
|
|
||||||
|
|
||||||
|
public class PollListCell {
|
||||||
|
}
|
||||||
@ -1,17 +1,440 @@
|
|||||||
package dev.ksan.ui;
|
package dev.ksan.ui;
|
||||||
|
|
||||||
|
import dev.ksan.CASystem.KeystoreService;
|
||||||
|
import dev.ksan.Users.Organizer;
|
||||||
|
import dev.ksan.Users.UserRepository;
|
||||||
import dev.ksan.Users.Voter;
|
import dev.ksan.Users.Voter;
|
||||||
|
import dev.ksan.voteSystem.*;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.scene.text.Text;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class VoterController {
|
public class VoterController {
|
||||||
private Voter voter;
|
private Voter voter;
|
||||||
private Stage stage;
|
private Stage stage;
|
||||||
|
private Poll selectedPoll;
|
||||||
|
@FXML
|
||||||
|
private Label usernameLabel;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label alreadyVotedLabel;
|
||||||
|
@FXML
|
||||||
|
private TitledPane activeList;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TitledPane inactiveList;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private StackPane centerPane;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Text titleText;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button refreshButton;
|
||||||
|
@FXML
|
||||||
|
private Text descriptionText;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Text startDateText;
|
||||||
|
@FXML
|
||||||
|
private Text endDateText;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button confirmButton;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label errorLabel;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private VBox optionsContainer;
|
||||||
|
|
||||||
|
private final ToggleGroup optionToggleGroup = new ToggleGroup();
|
||||||
|
|
||||||
|
@FXML private VBox panesBox;
|
||||||
|
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ListView<Poll> activePollsListView;
|
||||||
|
@FXML
|
||||||
|
private ListView<Poll> inactivePollsListView;
|
||||||
|
|
||||||
|
private final List<TextField> optionFields = new ArrayList<>();
|
||||||
|
|
||||||
|
|
||||||
public void setVoter(Voter voter) {
|
public void setVoter(Voter voter) {
|
||||||
this.voter = voter;
|
this.voter = voter;
|
||||||
|
loadPolls();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshPolls() {
|
||||||
|
loadPolls();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setStage(Stage stage) {
|
public void setStage(Stage stage) {
|
||||||
this.stage = stage;
|
this.stage = stage;
|
||||||
|
|
||||||
|
usernameLabel.setText(voter.getName());
|
||||||
|
|
||||||
|
confirmButton.setOnAction(e -> castVote());
|
||||||
|
activePollsListView.getSelectionModel().selectedItemProperty()
|
||||||
|
.addListener((obs, oldVal, newVal) -> {
|
||||||
|
if (newVal != null) showPollDetail(newVal);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inactive polls are view-only — just show detail with no voting
|
||||||
|
inactivePollsListView.getSelectionModel().selectedItemProperty()
|
||||||
|
.addListener((obs, oldVal, newVal) -> {
|
||||||
|
if (newVal != null) showPollDetail(newVal);
|
||||||
|
});
|
||||||
|
|
||||||
|
refreshButton.setOnAction(e -> refreshPolls());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initialize() {
|
||||||
|
errorLabel.setVisible(false);
|
||||||
|
centerPane.setVisible(false);
|
||||||
|
alreadyVotedLabel.setVisible(false);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void castVote() {
|
||||||
|
clearError();
|
||||||
|
|
||||||
|
if (selectedPoll == null) return;
|
||||||
|
|
||||||
|
Toggle selected = optionToggleGroup.getSelectedToggle();
|
||||||
|
if (selected == null) {
|
||||||
|
showError("Please select an option before confirming.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String selectedOptionId = (String) selected.getUserData();
|
||||||
|
|
||||||
|
Dialog<String> passwordDialog = buildPasswordDialog();
|
||||||
|
passwordDialog.showAndWait().ifPresent(password -> {
|
||||||
|
if (password.isBlank()) {
|
||||||
|
showError("Password is required to sign your vote.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmButton.setDisable(true);
|
||||||
|
|
||||||
|
Task<Vote> task = new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected Vote call() throws Exception {
|
||||||
|
|
||||||
|
char[] ksPassword = KeystoreService.deriveKeystorePassword(
|
||||||
|
password, voter.getUsername());
|
||||||
|
PrivateKey voterPrivateKey = KeystoreService.getPrivateKey(
|
||||||
|
voter.getUsername(), ksPassword);
|
||||||
|
|
||||||
|
Organizer organizer = (Organizer) UserRepository
|
||||||
|
.findOrganizer(selectedPoll.getOrganizerId());
|
||||||
|
if (organizer == null)
|
||||||
|
throw new IllegalStateException("Organizer not found.");
|
||||||
|
|
||||||
|
PublicKey organizerPublicKey = organizer.getCertificate().getPublicKey();
|
||||||
|
|
||||||
|
return VotingService.castVote(
|
||||||
|
selectedOptionId,
|
||||||
|
selectedPoll,
|
||||||
|
voter,
|
||||||
|
voterPrivateKey,
|
||||||
|
organizerPublicKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
task.setOnSucceeded(e -> {
|
||||||
|
showSuccess("");
|
||||||
|
loadPolls(); // refresh list
|
||||||
|
});
|
||||||
|
|
||||||
|
task.setOnFailed(e -> {
|
||||||
|
task.getException().printStackTrace();
|
||||||
|
showError("Vote failed: " + task.getException().getMessage());
|
||||||
|
confirmButton.setDisable(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
new Thread(task).start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void loadPolls() {
|
||||||
|
Task<List<Poll>> task = new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected List<Poll> call() throws Exception {
|
||||||
|
return PollRepository.findAll();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
task.setOnSucceeded(e -> {
|
||||||
|
List<Poll> polls = task.getValue();
|
||||||
|
|
||||||
|
List<Poll> active = polls.stream()
|
||||||
|
.filter(p -> p.getStatus() == PollStatus.ACTIVE)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
List<Poll> inactive = polls.stream()
|
||||||
|
.filter(p -> p.getStatus() != PollStatus.ACTIVE)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
activePollsListView.setItems(FXCollections.observableArrayList(active));
|
||||||
|
inactivePollsListView.setItems(FXCollections.observableArrayList(inactive));
|
||||||
|
|
||||||
|
activePollsListView.setCellFactory(lv -> new PollListCell());
|
||||||
|
inactivePollsListView.setCellFactory(lv -> new PollListCell());
|
||||||
|
});
|
||||||
|
|
||||||
|
task.setOnFailed(e -> {
|
||||||
|
task.getException().printStackTrace();
|
||||||
|
showError("Failed to load polls: " + task.getException().getMessage());
|
||||||
|
});
|
||||||
|
|
||||||
|
new Thread(task).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dialog<String> buildPasswordDialog() {
|
||||||
|
Dialog<String> dialog = new Dialog<>();
|
||||||
|
dialog.setTitle("Confirm Vote");
|
||||||
|
dialog.setHeaderText("Enter your password to sign and submit your vote.");
|
||||||
|
|
||||||
|
PasswordField pf = new PasswordField();
|
||||||
|
pf.setPromptText("Password");
|
||||||
|
|
||||||
|
VBox content = new VBox(8);
|
||||||
|
content.getChildren().addAll(new Label("Password:"), pf);
|
||||||
|
dialog.getDialogPane().setContent(content);
|
||||||
|
|
||||||
|
ButtonType confirmType = new ButtonType("Submit Vote", ButtonBar.ButtonData.OK_DONE);
|
||||||
|
dialog.getDialogPane().getButtonTypes().addAll(confirmType, ButtonType.CANCEL);
|
||||||
|
|
||||||
|
dialog.setResultConverter(btn ->
|
||||||
|
btn == confirmType ? pf.getText() : null);
|
||||||
|
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showPoll(Poll poll) {
|
||||||
|
centerPane.getChildren().clear();
|
||||||
|
|
||||||
|
VBox mainBox = new VBox(10);
|
||||||
|
mainBox.setPadding(new Insets(20));
|
||||||
|
mainBox.setAlignment(Pos.TOP_LEFT);
|
||||||
|
|
||||||
|
Label titleLabel = new Label("Title: " + poll.getTitle());
|
||||||
|
titleLabel.setStyle("-fx-font-size: 18px; -fx-font-weight: bold;");
|
||||||
|
|
||||||
|
Label descLabel = new Label("Description: " + poll.getDescription());
|
||||||
|
descLabel.setWrapText(true);
|
||||||
|
|
||||||
|
Label timeLabel = new Label(
|
||||||
|
"Start: " + poll.getStartTime() + "\nEnd: " + poll.getEndTime()
|
||||||
|
);
|
||||||
|
|
||||||
|
ToggleGroup toggleGroup = new ToggleGroup();
|
||||||
|
VBox optionsBox = new VBox(5);
|
||||||
|
for (PollOption option : poll.getOptions()) {
|
||||||
|
RadioButton rb = new RadioButton(option.getText());
|
||||||
|
rb.setToggleGroup(toggleGroup);
|
||||||
|
optionsBox.getChildren().add(rb);
|
||||||
|
}
|
||||||
|
|
||||||
|
Button confirmBtn = new Button("Confirm");
|
||||||
|
confirmBtn.setOnAction(e -> {
|
||||||
|
RadioButton selected = (RadioButton) toggleGroup.getSelectedToggle();
|
||||||
|
if (selected != null) {
|
||||||
|
System.out.println("Voted for: " + selected.getText());
|
||||||
|
// TODO vote logic
|
||||||
|
} else {
|
||||||
|
Alert alert = new Alert(Alert.AlertType.WARNING, "Please select an option.");
|
||||||
|
alert.showAndWait();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
HBox confirmBox = new HBox(confirmBtn);
|
||||||
|
confirmBox.setAlignment(Pos.BOTTOM_RIGHT);
|
||||||
|
|
||||||
|
mainBox.getChildren().addAll(titleLabel, descLabel, timeLabel, optionsBox, confirmBox);
|
||||||
|
|
||||||
|
centerPane.getChildren().add(mainBox);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showPollDetail(Poll poll) {
|
||||||
|
selectedPoll = poll;
|
||||||
|
clearError();
|
||||||
|
|
||||||
|
titleText.setText(poll.getTitle());
|
||||||
|
|
||||||
|
descriptionText.setText(poll.getDescription());
|
||||||
|
|
||||||
|
startDateText.setText(poll.getStartTime().toString());
|
||||||
|
|
||||||
|
endDateText.setText(poll.getEndTime().toString());
|
||||||
|
|
||||||
|
|
||||||
|
optionsContainer.getChildren().clear();
|
||||||
|
optionToggleGroup.getToggles().clear();
|
||||||
|
|
||||||
|
for (PollOption option : poll.getOptions()) {
|
||||||
|
RadioButton rb = new RadioButton(option.getText());
|
||||||
|
rb.setToggleGroup(optionToggleGroup);
|
||||||
|
rb.setUserData(option.getOptionId());
|
||||||
|
optionsContainer.getChildren().add(rb);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasVoted = VotingService.hasVoted(voter.getUsername(), poll.getPollId());
|
||||||
|
|
||||||
|
confirmButton.setVisible(poll.getStatus() == PollStatus.ACTIVE && !hasVoted);
|
||||||
|
confirmButton.setDisable(hasVoted);
|
||||||
|
|
||||||
|
alreadyVotedLabel.setVisible(hasVoted);
|
||||||
|
|
||||||
|
// If already voted show verify button instead
|
||||||
|
if (hasVoted) {
|
||||||
|
showVerifyButton(poll);
|
||||||
|
}
|
||||||
|
|
||||||
|
centerPane.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showVerifyButton(Poll poll) {
|
||||||
|
// Remove old verify button if present
|
||||||
|
optionsContainer.getChildren().removeIf(
|
||||||
|
node -> node instanceof Button &&
|
||||||
|
((Button) node).getText().equals("Verify My Vote"));
|
||||||
|
|
||||||
|
Button verifyBtn = new Button("Verify My Vote");
|
||||||
|
verifyBtn.setStyle("-fx-background-color: #7ED321; -fx-text-fill: white;");
|
||||||
|
verifyBtn.setOnAction(e -> verifyVote(poll));
|
||||||
|
optionsContainer.getChildren().add(verifyBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyVote(Poll poll) {
|
||||||
|
Task<Boolean> task = new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected Boolean call() throws Exception {
|
||||||
|
return VotingService.verifyVote(voter.getUsername(), poll.getPollId());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
task.setOnSucceeded(e -> {
|
||||||
|
boolean verified = task.getValue();
|
||||||
|
Alert alert = new Alert(
|
||||||
|
verified ? Alert.AlertType.INFORMATION : Alert.AlertType.WARNING);
|
||||||
|
alert.setTitle("Vote Verification");
|
||||||
|
alert.setHeaderText(null);
|
||||||
|
alert.setContentText(verified
|
||||||
|
? "✅ Your vote is recorded and its integrity is verified."
|
||||||
|
: "⚠️ Could not verify your vote. Please contact the organizer.");
|
||||||
|
alert.showAndWait();
|
||||||
|
});
|
||||||
|
|
||||||
|
task.setOnFailed(e -> showError("Verification failed: "
|
||||||
|
+ task.getException().getMessage()));
|
||||||
|
|
||||||
|
new Thread(task).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private class PollListCell extends ListCell<Poll> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateItem(Poll poll, boolean empty) {
|
||||||
|
super.updateItem(poll, empty);
|
||||||
|
|
||||||
|
if (empty || poll == null) {
|
||||||
|
setGraphic(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
VBox card = new VBox(4);
|
||||||
|
card.setPadding(new Insets(8));
|
||||||
|
|
||||||
|
Label title = new Label(poll.getTitle());
|
||||||
|
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
||||||
|
|
||||||
|
Label period = new Label(
|
||||||
|
poll.getStartTime().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))
|
||||||
|
+ " → " +
|
||||||
|
poll.getEndTime().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))
|
||||||
|
);
|
||||||
|
period.setStyle("-fx-text-fill: #666; -fx-font-size: 11;");
|
||||||
|
|
||||||
|
Label status = new Label(formatStatus(poll));
|
||||||
|
status.setStyle("-fx-text-fill: " + statusColor(poll) + "; -fx-font-size: 11;");
|
||||||
|
|
||||||
|
boolean hasVoted = VotingService.hasVoted(voter.getUsername(), poll.getPollId());
|
||||||
|
if (hasVoted) {
|
||||||
|
Label votedBadge = new Label("✅ Voted");
|
||||||
|
votedBadge.setStyle("-fx-text-fill: #7ED321; -fx-font-size: 10;");
|
||||||
|
card.getChildren().addAll(title, period, status, votedBadge);
|
||||||
|
} else {
|
||||||
|
card.getChildren().addAll(title, period, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
setGraphic(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatStatus(Poll poll) {
|
||||||
|
return switch (poll.getStatus()) {
|
||||||
|
case PENDING -> "⏳ Pending";
|
||||||
|
case ACTIVE -> "✅ Active";
|
||||||
|
case FINISHED -> "🔒 Finished";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String statusColor(Poll poll) {
|
||||||
|
return switch (poll.getStatus()) {
|
||||||
|
case PENDING -> "#F5A623";
|
||||||
|
case ACTIVE -> "#7ED321";
|
||||||
|
case FINISHED -> "#9B9B9B";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showError(String msg) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
errorLabel.setText("⚠️ " + msg);
|
||||||
|
errorLabel.setStyle("-fx-text-fill: red;");
|
||||||
|
errorLabel.setVisible(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSuccess(String msg) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
errorLabel.setText(msg);
|
||||||
|
errorLabel.setStyle("-fx-text-fill: green;");
|
||||||
|
errorLabel.setVisible(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearError() {
|
||||||
|
errorLabel.setText("");
|
||||||
|
errorLabel.setVisible(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import java.io.FileInputStream;
|
|||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.ObjectInputStream;
|
import java.io.ObjectInputStream;
|
||||||
import java.io.ObjectOutputStream;
|
import java.io.ObjectOutputStream;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
import java.nio.channels.FileLock;
|
import java.nio.channels.FileLock;
|
||||||
import java.nio.file.DirectoryStream;
|
import java.nio.file.DirectoryStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@ -32,6 +33,8 @@ public class PollRepository {
|
|||||||
writeLock.lock();
|
writeLock.lock();
|
||||||
try {
|
try {
|
||||||
writeToFile(poll, resolvePath(poll.getPollId()));
|
writeToFile(poll, resolvePath(poll.getPollId()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
} finally {
|
} finally {
|
||||||
writeLock.unlock();
|
writeLock.unlock();
|
||||||
}
|
}
|
||||||
@ -63,6 +66,8 @@ public class PollRepository {
|
|||||||
readLock.unlock();
|
readLock.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
@ -101,23 +106,28 @@ public class PollRepository {
|
|||||||
|
|
||||||
private static void writeToFile(Object obj, Path targetPath) throws Exception {
|
private static void writeToFile(Object obj, Path targetPath) throws Exception {
|
||||||
Path tmpPath = targetPath.resolveSibling(targetPath.getFileName() + ".tmp");
|
Path tmpPath = targetPath.resolveSibling(targetPath.getFileName() + ".tmp");
|
||||||
|
|
||||||
try (FileOutputStream fos = new FileOutputStream(tmpPath.toFile());
|
try (FileOutputStream fos = new FileOutputStream(tmpPath.toFile());
|
||||||
FileLock fileLock = fos.getChannel().lock();
|
FileChannel channel = fos.getChannel()) {
|
||||||
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
|
|
||||||
oos.writeObject(obj);
|
try (FileLock fileLock = channel.lock()) {
|
||||||
oos.flush();
|
ObjectOutputStream oos = new ObjectOutputStream(fos);
|
||||||
|
oos.writeObject(obj);
|
||||||
|
oos.flush();
|
||||||
|
channel.force(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Files.move(tmpPath, targetPath,
|
Files.move(tmpPath, targetPath,
|
||||||
StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
StandardCopyOption.ATOMIC_MOVE,
|
||||||
|
StandardCopyOption.REPLACE_EXISTING);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Object deserialize(Path path) throws Exception {
|
private static Object deserialize(Path path) throws Exception {
|
||||||
if (!Files.exists(path)) return null;
|
if (!Files.exists(path)) return null;
|
||||||
|
|
||||||
try (FileInputStream fis = new FileInputStream(path.toFile());
|
try (FileInputStream fis = new FileInputStream(path.toFile());
|
||||||
FileLock fileLock = fis.getChannel().tryLock(0, Long.MAX_VALUE, true);
|
|
||||||
ObjectInputStream ois = new ObjectInputStream(fis)) {
|
ObjectInputStream ois = new ObjectInputStream(fis)) {
|
||||||
if (fileLock == null)
|
|
||||||
throw new IllegalStateException("Could not acquire read lock on: " + path);
|
|
||||||
return ois.readObject();
|
return ois.readObject();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,23 +93,29 @@ public class VoteRepository {
|
|||||||
|
|
||||||
private static void writeToFile(Object obj, Path targetPath) throws Exception {
|
private static void writeToFile(Object obj, Path targetPath) throws Exception {
|
||||||
Path tmpPath = targetPath.resolveSibling(targetPath.getFileName() + ".tmp");
|
Path tmpPath = targetPath.resolveSibling(targetPath.getFileName() + ".tmp");
|
||||||
|
|
||||||
try (FileOutputStream fos = new FileOutputStream(tmpPath.toFile());
|
try (FileOutputStream fos = new FileOutputStream(tmpPath.toFile());
|
||||||
FileLock fileLock = fos.getChannel().lock();
|
|
||||||
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
|
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
|
||||||
oos.writeObject(obj);
|
|
||||||
oos.flush();
|
FileLock lock = fos.getChannel().lock();
|
||||||
|
try {
|
||||||
|
oos.writeObject(obj);
|
||||||
|
oos.flush();
|
||||||
|
} finally {
|
||||||
|
lock.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Files.move(tmpPath, targetPath,
|
Files.move(tmpPath, targetPath,
|
||||||
StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
StandardCopyOption.ATOMIC_MOVE,
|
||||||
|
StandardCopyOption.REPLACE_EXISTING);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Object deserialize(Path path) throws Exception {
|
private static Object deserialize(Path path) throws Exception {
|
||||||
if (!Files.exists(path)) return null;
|
if (!Files.exists(path)) return null;
|
||||||
|
|
||||||
try (FileInputStream fis = new FileInputStream(path.toFile());
|
try (FileInputStream fis = new FileInputStream(path.toFile());
|
||||||
FileLock fileLock = fis.getChannel().tryLock(0, Long.MAX_VALUE, true);
|
|
||||||
ObjectInputStream ois = new ObjectInputStream(fis)) {
|
ObjectInputStream ois = new ObjectInputStream(fis)) {
|
||||||
if (fileLock == null)
|
|
||||||
throw new IllegalStateException("Could not acquire read lock on: " + path);
|
|
||||||
return ois.readObject();
|
return ois.readObject();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,123 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.Button?>
|
||||||
|
<?import javafx.scene.control.DatePicker?>
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.ListView?>
|
||||||
|
<?import javafx.scene.control.ScrollPane?>
|
||||||
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.control.TitledPane?>
|
||||||
|
<?import javafx.scene.layout.BorderPane?>
|
||||||
|
<?import javafx.scene.layout.HBox?>
|
||||||
|
<?import javafx.scene.layout.StackPane?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
<?import javafx.scene.text.Text?>
|
||||||
|
|
||||||
|
<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="dev.ksan.ui.OrganizerController">
|
||||||
|
<left>
|
||||||
|
<VBox prefHeight="200.0" prefWidth="250.0" BorderPane.alignment="CENTER">
|
||||||
|
<children>
|
||||||
|
<Button fx:id="newButton" mnemonicParsing="false" text="new" textAlignment="CENTER">
|
||||||
|
<VBox.margin>
|
||||||
|
<Insets bottom="5.0" left="30.0" top="5.0" />
|
||||||
|
</VBox.margin>
|
||||||
|
</Button>
|
||||||
|
<ScrollPane prefHeight="200.0" prefWidth="200.0" VBox.vgrow="ALWAYS">
|
||||||
|
<content>
|
||||||
|
<VBox fx:id="panesBox">
|
||||||
|
<children>
|
||||||
|
<TitledPane fx:id="activeList" animated="false" text="untitled" VBox.vgrow="ALWAYS">
|
||||||
|
<content>
|
||||||
|
<ListView fx:id="activePollsListView" prefHeight="200.0" prefWidth="200.0" />
|
||||||
|
</content>
|
||||||
|
<VBox.margin>
|
||||||
|
<Insets bottom="5.0" />
|
||||||
|
</VBox.margin>
|
||||||
|
</TitledPane>
|
||||||
|
<TitledPane fx:id="inactiveList" animated="false" text="untitled" VBox.vgrow="ALWAYS">
|
||||||
|
<content>
|
||||||
|
<ListView fx:id="inactivePollsListView" />
|
||||||
|
</content>
|
||||||
|
</TitledPane>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
|
</content>
|
||||||
|
</ScrollPane>
|
||||||
|
</children>
|
||||||
|
<BorderPane.margin>
|
||||||
|
<Insets />
|
||||||
|
</BorderPane.margin>
|
||||||
|
</VBox>
|
||||||
|
</left>
|
||||||
|
<top>
|
||||||
|
<HBox prefHeight="27.0" prefWidth="600.0" BorderPane.alignment="CENTER">
|
||||||
|
<children>
|
||||||
|
<Label fx:id="usernameLabel" text="teemp" textAlignment="CENTER">
|
||||||
|
<HBox.margin>
|
||||||
|
<Insets left="20.0" top="5.0" />
|
||||||
|
</HBox.margin>
|
||||||
|
</Label>
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
</top>
|
||||||
|
<center>
|
||||||
|
<StackPane fx:id="centerPane" prefHeight="150.0" prefWidth="200.0" BorderPane.alignment="CENTER">
|
||||||
|
<children>
|
||||||
|
<VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0">
|
||||||
|
<children>
|
||||||
|
<HBox prefHeight="100.0" prefWidth="200.0">
|
||||||
|
<children>
|
||||||
|
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Title:" />
|
||||||
|
<TextField fx:id="titleField" />
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
<HBox prefHeight="100.0" prefWidth="200.0">
|
||||||
|
<children>
|
||||||
|
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Description:" />
|
||||||
|
<TextField fx:id="descriptionField" />
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
<VBox prefHeight="200.0" prefWidth="100.0">
|
||||||
|
<children>
|
||||||
|
<HBox prefHeight="100.0" prefWidth="200.0">
|
||||||
|
<children>
|
||||||
|
<DatePicker fx:id="startDatePicker" />
|
||||||
|
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="<--->">
|
||||||
|
<HBox.margin>
|
||||||
|
<Insets top="3.0" />
|
||||||
|
</HBox.margin>
|
||||||
|
</Text>
|
||||||
|
<DatePicker fx:id="endDatePicker" />
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
<HBox prefHeight="100.0" prefWidth="200.0">
|
||||||
|
<children>
|
||||||
|
<TextField fx:id="startTimeField" />
|
||||||
|
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="<--->" />
|
||||||
|
<TextField fx:id="endTimeField" />
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
|
<ScrollPane VBox.vgrow="ALWAYS">
|
||||||
|
<content>
|
||||||
|
<HBox>
|
||||||
|
<children>
|
||||||
|
<Button fx:id="newOptionButton" mnemonicParsing="false" text="new" />
|
||||||
|
<VBox fx:id="optionsContainer" alignment="CENTER" />
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
</content>
|
||||||
|
</ScrollPane>
|
||||||
|
<Button fx:id="confirmButton" alignment="BOTTOM_RIGHT" mnemonicParsing="false" text="create">
|
||||||
|
<graphic>
|
||||||
|
<Label fx:id="errorLabel" text="Label" />
|
||||||
|
</graphic>
|
||||||
|
</Button>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
|
</children>
|
||||||
|
</StackPane>
|
||||||
|
</center>
|
||||||
|
</BorderPane>
|
||||||
@ -1,43 +1,107 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
<?import com.gluonhq.charm.glisten.control.Avatar?>
|
|
||||||
<?import com.gluonhq.charm.glisten.control.DropdownButton?>
|
|
||||||
<?import com.gluonhq.charm.glisten.control.ExpansionPanel?>
|
|
||||||
<?import javafx.geometry.Insets?>
|
<?import javafx.geometry.Insets?>
|
||||||
<?import javafx.scene.control.MenuItem?>
|
<?import javafx.scene.control.Button?>
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.ListView?>
|
||||||
|
<?import javafx.scene.control.ScrollPane?>
|
||||||
|
<?import javafx.scene.control.TitledPane?>
|
||||||
<?import javafx.scene.layout.BorderPane?>
|
<?import javafx.scene.layout.BorderPane?>
|
||||||
<?import javafx.scene.layout.HBox?>
|
<?import javafx.scene.layout.HBox?>
|
||||||
|
<?import javafx.scene.layout.StackPane?>
|
||||||
<?import javafx.scene.layout.VBox?>
|
<?import javafx.scene.layout.VBox?>
|
||||||
<?import javafx.scene.text.Text?>
|
<?import javafx.scene.text.Text?>
|
||||||
|
|
||||||
|
<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="dev.ksan.ui.VoterController">
|
||||||
<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1">
|
|
||||||
<left>
|
<left>
|
||||||
<VBox prefHeight="200.0" prefWidth="100.0" BorderPane.alignment="CENTER">
|
<VBox prefHeight="200.0" prefWidth="250.0" BorderPane.alignment="CENTER">
|
||||||
|
<children>
|
||||||
|
<ScrollPane prefHeight="200.0" prefWidth="200.0" VBox.vgrow="ALWAYS">
|
||||||
|
<content>
|
||||||
|
<VBox fx:id="panesBox">
|
||||||
|
<children>
|
||||||
|
<Button fx:id="refreshButton" mnemonicParsing="false" text="Button" />
|
||||||
|
<TitledPane fx:id="activeList" animated="false" text="untitled" VBox.vgrow="ALWAYS">
|
||||||
|
<content>
|
||||||
|
<ListView fx:id="activePollsListView" prefHeight="200.0" prefWidth="200.0" />
|
||||||
|
</content>
|
||||||
|
<VBox.margin>
|
||||||
|
<Insets bottom="5.0" />
|
||||||
|
</VBox.margin>
|
||||||
|
</TitledPane>
|
||||||
|
<TitledPane fx:id="inactiveList" animated="false" text="untitled" VBox.vgrow="ALWAYS">
|
||||||
|
<content>
|
||||||
|
<ListView fx:id="inactivePollsListView" />
|
||||||
|
</content>
|
||||||
|
</TitledPane>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
|
</content>
|
||||||
|
</ScrollPane>
|
||||||
|
</children>
|
||||||
|
<BorderPane.margin>
|
||||||
|
<Insets />
|
||||||
|
</BorderPane.margin>
|
||||||
|
</VBox>
|
||||||
|
</left>
|
||||||
|
<top>
|
||||||
|
<HBox prefHeight="27.0" prefWidth="600.0" BorderPane.alignment="CENTER">
|
||||||
|
<children>
|
||||||
|
<Label fx:id="usernameLabel" text="teemp" textAlignment="CENTER">
|
||||||
|
<HBox.margin>
|
||||||
|
<Insets left="20.0" top="5.0" />
|
||||||
|
</HBox.margin>
|
||||||
|
</Label>
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
</top>
|
||||||
|
<center>
|
||||||
|
<StackPane fx:id="centerPane" prefHeight="150.0" prefWidth="200.0" BorderPane.alignment="CENTER">
|
||||||
<children>
|
<children>
|
||||||
<HBox alignment="CENTER" prefHeight="100.0" prefWidth="200.0">
|
|
||||||
<children>
|
|
||||||
<Avatar>
|
|
||||||
<HBox.margin>
|
|
||||||
<Insets left="5.0" right="5.0" />
|
|
||||||
</HBox.margin>
|
|
||||||
</Avatar>
|
|
||||||
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Text" />
|
|
||||||
</children>
|
|
||||||
</HBox>
|
|
||||||
<VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0">
|
<VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0">
|
||||||
<children>
|
<children>
|
||||||
<DropdownButton>
|
<HBox prefHeight="100.0" prefWidth="200.0">
|
||||||
<items>
|
<children>
|
||||||
<MenuItem text="Choice 1" />
|
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Title:" />
|
||||||
<MenuItem text="Choice 2" />
|
<Text fx:id="titleText" strokeType="OUTSIDE" strokeWidth="0.0" text="Text" />
|
||||||
<MenuItem text="Choice 3" />
|
</children>
|
||||||
</items>
|
</HBox>
|
||||||
</DropdownButton>
|
<HBox prefHeight="100.0" prefWidth="200.0">
|
||||||
<ExpansionPanel />
|
<children>
|
||||||
|
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Description:" />
|
||||||
|
<Text fx:id="descriptionText" strokeType="OUTSIDE" strokeWidth="0.0" text="Text" />
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
<VBox prefHeight="200.0" prefWidth="100.0">
|
||||||
|
<children>
|
||||||
|
<HBox prefHeight="100.0" prefWidth="200.0">
|
||||||
|
<children>
|
||||||
|
<Text fx:id="startDateText" strokeType="OUTSIDE" strokeWidth="0.0" text="Text" />
|
||||||
|
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="<--->">
|
||||||
|
<HBox.margin>
|
||||||
|
<Insets top="3.0" />
|
||||||
|
</HBox.margin>
|
||||||
|
</Text>
|
||||||
|
<Text fx:id="endDateText" strokeType="OUTSIDE" strokeWidth="0.0" text="Text" />
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
|
<ScrollPane VBox.vgrow="ALWAYS">
|
||||||
|
<content>
|
||||||
|
<HBox>
|
||||||
|
<children>
|
||||||
|
<VBox fx:id="optionsContainer" alignment="CENTER" />
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
</content>
|
||||||
|
</ScrollPane>
|
||||||
|
<Label fx:id="alreadyVotedLabel" text="Label" />
|
||||||
|
<Label fx:id="errorLabel" text="Label" />
|
||||||
|
<Button fx:id="confirmButton" alignment="BOTTOM_RIGHT" mnemonicParsing="false" text="create" />
|
||||||
</children>
|
</children>
|
||||||
</VBox>
|
</VBox>
|
||||||
</children>
|
</children>
|
||||||
</VBox>
|
</StackPane>
|
||||||
</left>
|
</center>
|
||||||
</BorderPane>
|
</BorderPane>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user