diff --git a/TestAvg.java b/TestAvg.java
deleted file mode 100644
index 7d0fbb8..0000000
--- a/TestAvg.java
+++ /dev/null
@@ -1,16 +0,0 @@
-import io.github.tomhula.jecnaapi.data.grade.*;
-import io.github.tomhula.jecnaapi.util.Name;
-import kotlinx.datetime.LocalDate;
-import java.util.Map;
-import java.util.List;
-
-public class TestAvg {
- public static void main(String[] args) {
- // Let's see if we can create a Grade
- Grade g1 = new Grade(1, false, new Name("Jan", "Novak"), "Test", new LocalDate(2023, 1, 1), 0);
- Grade g2 = new Grade(2, true, new Name("Jan", "Novak"), "Test", new LocalDate(2023, 1, 1), 0);
-
- System.out.println("Grade 1: " + g1.getValue() + " small: " + g1.getSmall());
- System.out.println("Grade 2: " + g2.getValue() + " small: " + g2.getSmall());
- }
-}
diff --git a/pom.xml b/pom.xml
index 675a3ee..7d7c6fb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,6 +24,14 @@
cz.jzitnik.Main
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.1
+
+ cz.jzitnik.Main
+
+
@@ -39,6 +47,11 @@
javafx-fxml
21.0.1
+
+ org.openjfx
+ javafx-web
+ 21.0.1
+
diff --git a/src/main/java/cz/jzitnik/controllers/AbsencesController.java b/src/main/java/cz/jzitnik/controllers/AbsencesController.java
new file mode 100644
index 0000000..50ee2e0
--- /dev/null
+++ b/src/main/java/cz/jzitnik/controllers/AbsencesController.java
@@ -0,0 +1,173 @@
+package cz.jzitnik.controllers;
+
+import cz.jzitnik.router.Route;
+import cz.jzitnik.router.Router;
+import cz.jzitnik.query.QueryOptions;
+import cz.jzitnik.query.QueryResult;
+import io.github.tomhula.jecnaapi.data.absence.AbsenceInfo;
+import io.github.tomhula.jecnaapi.data.absence.AbsencesPage;
+import kotlinx.datetime.LocalDate;
+import javafx.application.Platform;
+import javafx.fxml.FXML;
+import javafx.geometry.Pos;
+import javafx.scene.control.*;
+import javafx.scene.layout.*;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+@Route(path = "/absences", fxml = "/absences.fxml")
+public class AbsencesController extends DashboardBaseController {
+
+ @FXML
+ private ProgressIndicator loadingIndicator;
+
+ @FXML
+ private ScrollPane scrollPane;
+
+ @FXML
+ private HBox summaryBox;
+
+ @FXML
+ private FlowPane detailFlow;
+
+ private AbsencesPage currentPage;
+
+ @Override
+ public void onNavigate(Map props) {
+ super.onNavigate(props);
+ loadAbsences();
+ }
+
+ private void loadAbsences() {
+ loadingIndicator.setVisible(true);
+ scrollPane.setVisible(false);
+ summaryBox.getChildren().clear();
+ detailFlow.getChildren().clear();
+
+ appState.getQueryClient().fetch("absences:page", () -> {
+ try {
+ return appState.getClient().getAbsencesPage().join();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }, QueryOptions.defaultOptions()).thenAccept(this::handleAbsencesResult);
+ }
+
+ private void handleAbsencesResult(QueryResult result) {
+ Platform.runLater(() -> {
+ loadingIndicator.setVisible(false);
+
+ if (result.isSuccess() && result.getData().isPresent()) {
+ currentPage = result.getData().get();
+ renderAbsences();
+ scrollPane.setVisible(true);
+ } else {
+ Label errorLabel = new Label("Failed to load absences.");
+ errorLabel.getStyleClass().addAll("text-danger", "title-3");
+ summaryBox.getChildren().add(errorLabel);
+ scrollPane.setVisible(true);
+ }
+ });
+ }
+
+ private void renderAbsences() {
+ summaryBox.getChildren().clear();
+ detailFlow.getChildren().clear();
+
+ if (currentPage.getDays().isEmpty()) {
+ Label noAbsences = new Label("Žádné absence v tomto roce!");
+ noAbsences.getStyleClass().addAll("title-3", "text-success");
+ summaryBox.getChildren().add(noAbsences);
+ return;
+ }
+
+ int totalAbsent = 0;
+ int totalUnexcused = 0;
+ int totalLate = 0;
+
+ List sortedDays = new ArrayList<>(currentPage.getDays());
+ sortedDays.sort(Comparator.reverseOrder());
+
+ for (LocalDate day : sortedDays) {
+ AbsenceInfo info = currentPage.get(day);
+ if (info != null) {
+ totalAbsent += info.getHoursAbsent();
+ totalUnexcused += info.getUnexcusedHours();
+ totalLate += info.getLateEntryCount();
+
+ detailFlow.getChildren().add(createAbsenceCard(day, info));
+ }
+ }
+
+ summaryBox.getChildren().addAll(
+ createSummaryCard("Zameškané hodiny", totalAbsent, "absence-summary-total"),
+ createSummaryCard("Neomluvené", totalUnexcused, totalUnexcused > 0 ? "absence-summary-danger" : "absence-summary-safe"),
+ createSummaryCard("Pozdní příchody", totalLate, totalLate > 0 ? "absence-summary-warning" : "absence-summary-safe")
+ );
+ }
+
+ private VBox createSummaryCard(String title, int count, String styleClass) {
+ VBox card = new VBox(5);
+ card.setAlignment(Pos.CENTER);
+ card.getStyleClass().addAll("card", "absence-summary-card", styleClass);
+
+ Label valueLbl = new Label(String.valueOf(count));
+ valueLbl.getStyleClass().add("absence-summary-value");
+
+ Label titleLbl = new Label(title);
+ titleLbl.getStyleClass().add("text-muted");
+
+ card.getChildren().addAll(valueLbl, titleLbl);
+ return card;
+ }
+
+ private VBox createAbsenceCard(LocalDate day, AbsenceInfo info) {
+ VBox card = new VBox(8);
+ card.getStyleClass().addAll("card", "absence-card");
+
+ if (info.getUnexcusedHours() > 0) {
+ card.getStyleClass().add("absence-card-danger");
+ } else if (info.getLateEntryCount() > 0) {
+ card.getStyleClass().add("absence-card-warning");
+ }
+
+ Label dateLbl = new Label(formatDate(day));
+ dateLbl.getStyleClass().addAll("title-4", "absence-card-date");
+
+ HBox stats = new HBox(15);
+ stats.setAlignment(Pos.CENTER_LEFT);
+
+ Label hoursLbl = new Label("Hodiny: " + info.getHoursAbsent());
+ hoursLbl.getStyleClass().add("text-light");
+ stats.getChildren().add(hoursLbl);
+
+ if (info.getUnexcusedHours() > 0) {
+ Label unexcusedLbl = new Label("Neomluveno: " + info.getUnexcusedHours());
+ unexcusedLbl.getStyleClass().addAll("text-danger", "text-bold");
+ stats.getChildren().add(unexcusedLbl);
+ }
+
+ if (info.getLateEntryCount() > 0) {
+ Label lateLbl = new Label("Pozdě: " + info.getLateEntryCount());
+ lateLbl.getStyleClass().addAll("text-warning", "text-bold");
+ stats.getChildren().add(lateLbl);
+ }
+
+ card.getChildren().addAll(dateLbl, stats);
+ return card;
+ }
+
+ @SuppressWarnings("deprecation")
+ private String formatDate(LocalDate date) {
+ return date.getDayOfMonth() + ". " + date.getMonthNumber() + ". " + date.getYear();
+ }
+
+ @FXML
+ protected void onBackToDashboard() {
+ Router.getInstance().navigate("/dashboard");
+ }
+}
diff --git a/src/main/java/cz/jzitnik/controllers/DashboardBaseController.java b/src/main/java/cz/jzitnik/controllers/DashboardBaseController.java
index 43922f2..211aeb4 100644
--- a/src/main/java/cz/jzitnik/controllers/DashboardBaseController.java
+++ b/src/main/java/cz/jzitnik/controllers/DashboardBaseController.java
@@ -41,6 +41,9 @@ public class DashboardBaseController implements Routable {
@FXML
protected void onNavigateToSpecial() { Router.getInstance().navigate("/special"); }
+ @FXML
+ protected void onNavigateToMoodle() { Router.getInstance().navigate("/moodle"); }
+
@FXML
protected void onDoNothing() {
}
diff --git a/src/main/java/cz/jzitnik/controllers/MoodleController.java b/src/main/java/cz/jzitnik/controllers/MoodleController.java
new file mode 100644
index 0000000..b02ef70
--- /dev/null
+++ b/src/main/java/cz/jzitnik/controllers/MoodleController.java
@@ -0,0 +1,167 @@
+package cz.jzitnik.controllers;
+
+import cz.jzitnik.router.Route;
+import cz.jzitnik.router.Router;
+import javafx.concurrent.Worker;
+import javafx.fxml.FXML;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.ProgressIndicator;
+import javafx.scene.web.WebEngine;
+import javafx.scene.web.WebHistory;
+import javafx.scene.web.WebView;
+import java.util.Map;
+import java.util.Objects;
+
+@Route(path = "/moodle", fxml = "/moodle.fxml")
+public class MoodleController extends DashboardBaseController {
+
+ @FXML
+ private WebView webView;
+
+ @FXML
+ private ProgressIndicator loadingIndicator;
+
+ @FXML
+ private Label titleLabel;
+
+ @FXML
+ private Button btnBack;
+
+ @FXML
+ private Button btnForward;
+
+ private WebEngine webEngine;
+
+ @Override
+ public void onNavigate(Map props) {
+ super.onNavigate(props);
+
+ webEngine = webView.getEngine();
+
+ webEngine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> {
+ if (newState == Worker.State.SUCCEEDED) {
+ String currentUrl = webEngine.getLocation();
+ boolean isLoginPage = currentUrl != null && currentUrl.contains("login/index.php");
+
+ Boolean isLoggedOut = false;
+ try {
+ Object result = webEngine.executeScript("document.body && document.body.classList.contains('notloggedin')");
+ if (result instanceof Boolean) {
+ isLoggedOut = (Boolean) result;
+ }
+ } catch (Exception e) {}
+
+ if (isLoginPage) {
+ String username = appState.getUsername();
+ String password = appState.getPassword();
+
+ if (username != null && password != null) {
+ String script = String.format(
+ "var u = document.getElementById('username');" +
+ "var p = document.getElementById('password');" +
+ "var b = document.getElementById('loginbtn');" +
+ "if (u && p && b) {" +
+ " u.value = '%s';" +
+ " p.value = '%s';" +
+ " b.click();" +
+ "}",
+ username.replace("'", "\\'"),
+ password.replace("'", "\\'")
+ );
+ webEngine.executeScript(script);
+ } else {
+ loadingIndicator.setVisible(false);
+ webView.setVisible(true);
+ }
+ } else if (isLoggedOut) {
+ webEngine.executeScript("window.location.href = 'https://moodle.spsejecna.cz/login/index.php';");
+ } else {
+ loadingIndicator.setVisible(false);
+ webView.setVisible(true);
+ }
+
+ } else if (newState == Worker.State.FAILED) {
+ titleLabel.setText("Failed to load");
+ loadingIndicator.setVisible(false);
+ }
+ });
+
+ webEngine.titleProperty().addListener((obs, oldTitle, newTitle) -> {
+ if (newTitle != null && !newTitle.isEmpty()) {
+ titleLabel.setText(newTitle);
+ }
+ });
+
+ webEngine.getHistory().currentIndexProperty().addListener((obs, oldVal, newVal) -> {
+ updateNavigationButtons();
+ });
+ updateNavigationButtons();
+
+ loadingIndicator.setVisible(true);
+ webView.setVisible(false);
+ webEngine.setUserStyleSheetLocation(Objects.requireNonNull(getClass().getResource("/styles/moodle-dark.css")).toExternalForm());
+ webEngine.load("https://moodle.spsejecna.cz/");
+ }
+
+ private void updateNavigationButtons() {
+ WebHistory history = webEngine.getHistory();
+ int currentIndex = history.getCurrentIndex();
+
+ int validBackIndex = -1;
+ for (int i = currentIndex - 1; i >= 0; i--) {
+ String url = history.getEntries().get(i).getUrl();
+ if (url == null || !url.contains("login/index.php")) {
+ validBackIndex = i;
+ break;
+ }
+ }
+ btnBack.setDisable(validBackIndex == -1);
+
+ int validForwardIndex = -1;
+ for (int i = currentIndex + 1; i < history.getEntries().size(); i++) {
+ String url = history.getEntries().get(i).getUrl();
+ if (url == null || !url.contains("login/index.php")) {
+ validForwardIndex = i;
+ break;
+ }
+ }
+ btnForward.setDisable(validForwardIndex == -1);
+ }
+
+ @FXML
+ protected void onBrowserBack() {
+ WebHistory history = webEngine.getHistory();
+ int currentIndex = history.getCurrentIndex();
+ for (int i = currentIndex - 1; i >= 0; i--) {
+ String url = history.getEntries().get(i).getUrl();
+ if (url == null || !url.contains("login/index.php")) {
+ history.go(i - currentIndex);
+ break;
+ }
+ }
+ }
+
+ @FXML
+ protected void onBrowserForward() {
+ WebHistory history = webEngine.getHistory();
+ int currentIndex = history.getCurrentIndex();
+ for (int i = currentIndex + 1; i < history.getEntries().size(); i++) {
+ String url = history.getEntries().get(i).getUrl();
+ if (url == null || !url.contains("login/index.php")) {
+ history.go(i - currentIndex);
+ break;
+ }
+ }
+ }
+
+ @FXML
+ protected void onBrowserReload() {
+ webEngine.reload();
+ }
+
+ @FXML
+ protected void onBackToDashboard() {
+ Router.getInstance().navigate("/dashboard");
+ }
+}
diff --git a/src/main/java/cz/jzitnik/state/AppState.java b/src/main/java/cz/jzitnik/state/AppState.java
index cbe0d7b..69a26d4 100644
--- a/src/main/java/cz/jzitnik/state/AppState.java
+++ b/src/main/java/cz/jzitnik/state/AppState.java
@@ -8,6 +8,7 @@ import cz.jzitnik.query.QueryClient;
public class AppState {
private String username;
+ private String password;
private JecnaClientJavaWrapper client;
private QueryClient queryClient;
@@ -15,6 +16,10 @@ public class AppState {
return username;
}
+ public String getPassword() {
+ return password;
+ }
+
public JecnaClientJavaWrapper getClient() {
return client;
}
@@ -38,6 +43,7 @@ public class AppState {
boolean ok = client.login(username, password).join();
if (ok) {
this.username = username;
+ this.password = password;
try {
CredentialStore.saveCredentials(username, password);
} catch (Throwable t) {
@@ -57,6 +63,7 @@ public class AppState {
public void clear() {
this.username = null;
+ this.password = null;
if (this.client != null) {
try {
this.client.logout();
diff --git a/src/main/resources/absences.fxml b/src/main/resources/absences.fxml
new file mode 100644
index 0000000..1d010eb
--- /dev/null
+++ b/src/main/resources/absences.fxml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/dashboard_modern.fxml b/src/main/resources/dashboard_modern.fxml
index 47b49ce..7a01295 100644
--- a/src/main/resources/dashboard_modern.fxml
+++ b/src/main/resources/dashboard_modern.fxml
@@ -43,8 +43,11 @@
-
+
+
+
+
diff --git a/src/main/resources/moodle.fxml b/src/main/resources/moodle.fxml
new file mode 100644
index 0000000..d630d4d
--- /dev/null
+++ b/src/main/resources/moodle.fxml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/styles/absences.css b/src/main/resources/styles/absences.css
new file mode 100644
index 0000000..8838a24
--- /dev/null
+++ b/src/main/resources/styles/absences.css
@@ -0,0 +1,77 @@
+/* Absences Styles */
+
+.absence-summary-card {
+ -fx-min-width: 150px;
+ -fx-min-height: 100px;
+ -fx-background-color: rgba(255, 255, 255, 0.05);
+ -fx-border-radius: 12px;
+ -fx-background-radius: 12px;
+}
+
+.absence-summary-value {
+ -fx-font-size: 32px;
+ -fx-font-weight: bold;
+ -fx-text-fill: white;
+}
+
+.absence-summary-total .absence-summary-value {
+ -fx-text-fill: #58a6ff;
+}
+
+.absence-summary-safe .absence-summary-value {
+ -fx-text-fill: #2ea043;
+}
+
+.absence-summary-warning {
+ -fx-border-color: rgba(210, 153, 34, 0.4);
+}
+.absence-summary-warning .absence-summary-value {
+ -fx-text-fill: #d29922;
+}
+
+.absence-summary-danger {
+ -fx-border-color: rgba(248, 81, 73, 0.4);
+}
+.absence-summary-danger .absence-summary-value {
+ -fx-text-fill: #f85149;
+}
+
+/* Detail Cards */
+.absence-card {
+ -fx-min-width: 250px;
+ -fx-background-color: rgba(30, 40, 56, 0.7);
+ -fx-border-color: rgba(255, 255, 255, 0.05);
+ -fx-border-radius: 8px;
+ -fx-background-radius: 8px;
+ -fx-padding: 15px;
+ -fx-alignment: center-left;
+}
+
+.absence-card-warning {
+ -fx-border-color: rgba(210, 153, 34, 0.5);
+ -fx-background-color: rgba(210, 153, 34, 0.05);
+}
+
+.absence-card-danger {
+ -fx-border-color: rgba(248, 81, 73, 0.5);
+ -fx-background-color: rgba(248, 81, 73, 0.05);
+}
+
+.absence-card-date {
+ -fx-text-fill: white;
+ -fx-font-size: 16px;
+ -fx-font-weight: bold;
+ -fx-padding: 0 0 5 0;
+}
+
+.text-success {
+ -fx-text-fill: #2ea043;
+}
+
+.text-warning {
+ -fx-text-fill: #d29922;
+}
+
+.text-bold {
+ -fx-font-weight: bold;
+}
\ No newline at end of file
diff --git a/src/main/resources/styles/moodle-dark.css b/src/main/resources/styles/moodle-dark.css
new file mode 100644
index 0000000..ba1d1ee
--- /dev/null
+++ b/src/main/resources/styles/moodle-dark.css
@@ -0,0 +1,8 @@
+html {
+ filter: invert(100%) hue-rotate(180deg) brightness(105%) contrast(85%);
+ background: white;
+}
+
+img, video, iframe, canvas, svg, [style*='background-image'] {
+ filter: invert(100%) hue-rotate(180deg) !important;
+}
\ No newline at end of file
diff --git a/src/main/resources/styles/moodle.css b/src/main/resources/styles/moodle.css
new file mode 100644
index 0000000..37e0730
--- /dev/null
+++ b/src/main/resources/styles/moodle.css
@@ -0,0 +1,30 @@
+.browser-btn {
+ -fx-background-color: transparent;
+ -fx-border-color: transparent;
+ -fx-text-fill: #c9d1d9;
+ -fx-font-weight: bold;
+ -fx-font-size: 14px;
+ -fx-min-width: 35px;
+ -fx-min-height: 35px;
+ -fx-padding: 0;
+ -fx-cursor: hand;
+ -fx-background-radius: 4px;
+}
+
+.browser-btn:hover {
+ -fx-background-color: rgba(255, 255, 255, 0.1);
+}
+
+.browser-btn:pressed {
+ -fx-background-color: rgba(255, 255, 255, 0.15);
+}
+
+.browser-btn:disabled {
+ -fx-opacity: 0.3;
+ -fx-cursor: default;
+}
+
+.moodle-title {
+ -fx-padding: 0 0 0 15px;
+ -fx-text-fill: white;
+}
\ No newline at end of file