From 672cf67ef20b01cd6d22ede781c4c793668fd2bd Mon Sep 17 00:00:00 2001 From: Timur Gauzhaev Date: Sat, 10 Oct 2026 12:32:10 +0300 Subject: [PATCH 01/12] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20css=20=D0=B8=20=D1=80=D0=B5=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B3=D0=BB=D0=B0?= =?UTF-8?q?=D0=B2=D0=BD=D0=BE=D0=B9=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=86=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DesktopApplicationClientJava/Program.java | 7 +- .../controllers/LoginFormController.java | 1 - .../ResourceDetailsController.java | 58 ++- .../controllers/ResourceListController.java | 230 ++++++++++- .../controllers/utils/Controller.java | 3 + .../services/ResourceService.java | 3 +- .../services/impl/ResourceServiceImpl.java | 15 + .../services/mock/MockResourceService.java | 31 ++ src/main/resources/fxml/LoginForm.fxml | 69 ++-- src/main/resources/fxml/ResourceDetails.fxml | 88 +++-- src/main/resources/fxml/ResourceList.fxml | 100 ++++- src/main/resources/styles.css | 370 +++++++++++++++++- 12 files changed, 869 insertions(+), 106 deletions(-) diff --git a/src/main/java/com/DesktopApplicationClientJava/Program.java b/src/main/java/com/DesktopApplicationClientJava/Program.java index 9a02450..c8b664d 100644 --- a/src/main/java/com/DesktopApplicationClientJava/Program.java +++ b/src/main/java/com/DesktopApplicationClientJava/Program.java @@ -16,8 +16,8 @@ public class Program extends Application { private static final String INITIAL_PAGE_PATH = System.getenv().getOrDefault("INITIAL_PAGE_PATH", "/fxml/LoginForm.fxml"); - private static final boolean MOCK_DATA = - Boolean.parseBoolean(System.getenv().getOrDefault("MOCK_DATA", "false")); + private static final boolean MOCK_DATA = true; + //Boolean.parseBoolean(System.getenv().getOrDefault("MOCK_DATA", "false")); private static final double WINDOW_WIDTH = 800.0; private static final double WINDOW_HEIGHT = 600.0; @@ -25,9 +25,6 @@ public class Program extends Application { private final ApiClient apiClient = new ApiClient(session); private final ServiceRegistry services = createServices(); - public static void main(String[] args) { - launch(args); - } @Override public void start(Stage stage) throws IOException { diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/LoginFormController.java b/src/main/java/com/DesktopApplicationClientJava/controllers/LoginFormController.java index a88dd82..5af6832 100644 --- a/src/main/java/com/DesktopApplicationClientJava/controllers/LoginFormController.java +++ b/src/main/java/com/DesktopApplicationClientJava/controllers/LoginFormController.java @@ -35,7 +35,6 @@ private void onSubmit(ActionEvent event) { } try { - // ← ВЫЗОВ СЕРВИСА (middleware): session уже обновлён сервисом при успехе User user = services.getAuthService().login(email, password); if (user == null) { return; diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceDetailsController.java b/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceDetailsController.java index 7d7f92c..0551be9 100644 --- a/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceDetailsController.java +++ b/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceDetailsController.java @@ -13,8 +13,10 @@ import java.util.List; import java.util.UUID; import java.util.function.Consumer; +import javafx.beans.property.SimpleStringProperty; import javafx.event.ActionEvent; import javafx.fxml.FXML; +import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TableCell; @@ -38,7 +40,7 @@ public class ResourceDetailsController extends Controller { @FXML private Label statusLabel; @FXML private TableView filesTable; @FXML private TableColumn nameColumn; - @FXML private TableColumn sizeColumn; + @FXML private TableColumn sizeColumn; @FXML private TableColumn contentTypeColumn; @FXML private TableColumn downloadColumn; @FXML private Button backButton; @@ -48,10 +50,27 @@ public class ResourceDetailsController extends Controller { @FXML private void initialize() { hideError(); + nameColumn.setCellValueFactory(new PropertyValueFactory<>("name")); - sizeColumn.setCellValueFactory(new PropertyValueFactory<>("size")); contentTypeColumn.setCellValueFactory(new PropertyValueFactory<>("contentType")); + sizeColumn.setCellValueFactory(c -> + new SimpleStringProperty(formatSize(c.getValue().getSize()))); + sizeColumn.setCellFactory(col -> { + TableCell cell = new TableCell<>() { + @Override + protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + setText(empty ? null : item); + setAlignment(Pos.CENTER_RIGHT); + } + }; + return cell; + }); + downloadColumn.setCellFactory(col -> new DownloadCell(this::onDownload)); + filesTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN); + filesTable.setPlaceholder(new Label("У этого документа нет файлов")); + autoSelectFirstInMockMode(); } @@ -75,7 +94,8 @@ private void onDownload(FileInfo file) { return; } try { - Path saved = services.getFileService().downloadFile(resourceId, file.getUuid(), dir.toPath()); + Path saved = + services.getFileService().downloadFile(resourceId, file.getUuid(), dir.toPath()); statusLabel.setText("Сохранено: " + saved.getFileName()); } catch (ServiceException e) { showError(e.getMessage()); @@ -87,9 +107,9 @@ protected File chooseDirectory(Window owner) { } /** - * Dev-only fallback: when the page is opened without an id (e.g. straight via INITIAL_PAGE_PATH) - * against a mock service, show the first seeded resource so the UI is clickable without the - * resource list. Never triggers against the real backend. + * Dev-only fallback: when the page is opened without an id (e.g. straight via + * INITIAL_PAGE_PATH) against a mock service, show the first seeded resource so the UI is + * clickable without the resource list. Never triggers against the real backend. */ private void autoSelectFirstInMockMode() { if (resourceId != null || !(services.getResourceService() instanceof MockService mock)) { @@ -97,13 +117,13 @@ private void autoSelectFirstInMockMode() { } mock.doIfInitialPage(); services.getResourceService().search(ResourceFilter.empty(), 0, 1).stream() - .map(Resource::getUuid) - .findFirst() - .ifPresent( - id -> { - setResourceId(id); - load(); - }); + .map(Resource::getUuid) + .findFirst() + .ifPresent( + id -> { + setResourceId(id); + load(); + }); } private void showResource(Resource resource) { @@ -120,6 +140,14 @@ private static String formatDateTime(LocalDateTime value) { return value == null ? "" : DATE_TIME_FORMAT.format(value); } + private static String formatSize(Long bytes) { + if (bytes == null) return ""; + if (bytes < 1024) return bytes + " B"; + if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0); + if (bytes < 1024L * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024)); + return String.format("%.1f GB", bytes / (1024.0 * 1024 * 1024)); + } + private void showError(String message) { errorLabel.setText(message); errorLabel.setVisible(true); @@ -136,7 +164,7 @@ private static final class DownloadCell extends TableCell { private final Button button = new Button("Скачать"); DownloadCell(Consumer onDownload) { - button.getStyleClass().setAll("btn", "btn-default"); + button.getStyleClass().setAll("btn-ghost"); button.setOnAction(event -> onDownload.accept(getTableView().getItems().get(getIndex()))); } @@ -146,4 +174,4 @@ protected void updateItem(Void item, boolean empty) { setGraphic(empty ? null : button); } } -} +} \ No newline at end of file diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceListController.java b/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceListController.java index 690b421..3f619e7 100644 --- a/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceListController.java +++ b/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceListController.java @@ -1,5 +1,233 @@ package com.DesktopApplicationClientJava.controllers; import com.DesktopApplicationClientJava.controllers.utils.Controller; +import com.DesktopApplicationClientJava.entities.Resource; +import com.DesktopApplicationClientJava.entities.User; +import com.DesktopApplicationClientJava.services.ServiceException; +import com.DesktopApplicationClientJava.services.filter.ResourceFilter; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import javafx.application.Platform; +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.concurrent.Task; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableRow; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.control.cell.PropertyValueFactory; -public class ResourceListController extends Controller {} +public class ResourceListController extends Controller { + + private static final DateTimeFormatter DATE_TIME_FORMAT = + DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"); + private static final int PAGE_SIZE = 20; + + @FXML private Label userInfoLabel; + @FXML private Label errorLabel; + @FXML private Label pageInfoLabel; + @FXML private Label statusLabel; + @FXML private TextField searchField; + @FXML private ComboBox sortCombo; + @FXML private TableView resourceTable; + @FXML private TableColumn titleColumn; + @FXML private TableColumn descriptionColumn; + @FXML private TableColumn createdColumn; + @FXML private TableColumn updatedColumn; + @FXML private Button createButton; + @FXML private Button prevButton; + @FXML private Button nextButton; + @FXML private Button logoutButton; // ← добавь, если ещё нет + + private final ObservableList rows = FXCollections.observableArrayList(); + private long offset = 0; + + // ===== Lifecycle ===== + + @FXML + private void initialize() { + hideError(); + setupUser(); + setupSort(); + setupTable(); + setupRbac(); + + searchField.setOnAction(e -> reloadFromStart()); + sortCombo.setOnAction(e -> reloadFromStart()); + resourceTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN); + } + + @Override + public void onLoad() { + // Стартовая загрузка — после wiring и построения сцены + reloadFromStart(); + } + + // ===== UI setup ===== + + private void setupUser() { + var me = services.getAuthService().getCurrentUser(); + userInfoLabel.setText(me == null + ? "" + : me.getFirstName() + " " + me.getLastName() + " · " + me.getRole()); + } + + private void setupSort() { + sortCombo.getItems().setAll( + "Сначала новые", + "Сначала старые", + "По названию (А-Я)", + "По названию (Я-А)"); + sortCombo.getSelectionModel().selectFirst(); + } + + private void setupTable() { + titleColumn.setCellValueFactory(new PropertyValueFactory<>("title")); + descriptionColumn.setCellValueFactory(new PropertyValueFactory<>("description")); + createdColumn.setCellValueFactory(c -> + new SimpleStringProperty(formatDate(c.getValue().getCreatedAt()))); + updatedColumn.setCellValueFactory(c -> + new SimpleStringProperty(formatDate(c.getValue().getUpdatedAt()))); + + resourceTable.setItems(rows); + resourceTable.setPlaceholder(new Label("Документов пока нет")); + + resourceTable.setRowFactory(tv -> { + var row = new TableRow(); + row.setOnMouseClicked(e -> { + if (e.getClickCount() == 2 && !row.isEmpty()) { + openDetails(row.getItem()); + } + }); + return row; + }); + } + + private void setupRbac() { + boolean canCreate = services.getAuthService().isAdmin() + || services.getAuthService().isModerator(); + createButton.setVisible(canCreate); + createButton.setManaged(canCreate); + } + + // ===== Data loading ===== + + private void reloadFromStart() { + offset = 0; + loadAsync(); + } + + private void loadAsync() { + hideError(); + statusLabel.setText("Загрузка…"); + + var filter = new ResourceFilter( + searchField.getText() == null || searchField.getText().isBlank() + ? null + : searchField.getText().trim(), + null); + var sort = resolveSort(); + long currentOffset = offset; + + var task = new Task>() { + @Override + protected List call() { + return services.getResourceService() + .search(filter, currentOffset, PAGE_SIZE, sort.sortBy(), sort.sortDir()); + } + }; + + task.setOnSucceeded(e -> { + var page = task.getValue(); + rows.setAll(page); + updatePaginationUi(page.size()); + statusLabel.setText(""); + }); + + task.setOnFailed(e -> { + var ex = task.getException(); + String msg = ex instanceof ServiceException se + ? se.getMessage() + : "Не удалось загрузить список"; + showError(msg); + rows.clear(); + updatePaginationUi(0); + statusLabel.setText(""); + }); + + var thread = new Thread(task, "resource-list-load"); + thread.setDaemon(true); + thread.start(); + } + + private record SortSpec(String sortBy, String sortDir) {} + + private SortSpec resolveSort() { + String choice = sortCombo.getValue(); + if (choice == null) return new SortSpec("createdAt", "desc"); + return switch (choice) { + case "Сначала старые" -> new SortSpec("createdAt", "asc"); + case "По названию (А-Я)" -> new SortSpec("title", "asc"); + case "По названию (Я-А)" -> new SortSpec("title", "desc"); + default -> new SortSpec("createdAt", "desc"); + }; + } + + private void updatePaginationUi(int pageSize) { + long from = rows.isEmpty() ? 0 : offset + 1; + long to = offset + pageSize; + pageInfoLabel.setText("Показано " + from + "–" + to); + prevButton.setDisable(offset == 0); + nextButton.setDisable(pageSize < PAGE_SIZE); + } + + // ===== Event handlers ===== + + @FXML + private void onCreate(ActionEvent e) { + // Страница создания — отдельная задача; пока просто заглушка + statusLabel.setText("Раздел создания ещё не реализован"); + } + + @FXML + private void onLogout(ActionEvent e) { + services.getAuthService().logout(); + navigator.goTo("/fxml/LoginForm.fxml"); + } + + @FXML private void onSearch(ActionEvent e) { offset = 0; loadAsync(); } + @FXML private void onSort(ActionEvent e) { offset = 0; loadAsync(); } + @FXML private void onNext(ActionEvent e) { offset += PAGE_SIZE; loadAsync(); } + @FXML private void onPrev(ActionEvent e) { offset = Math.max(0, offset - PAGE_SIZE); loadAsync(); } + + private void openDetails(Resource resource) { + navigator.goTo( + "/fxml/ResourceDetails.fxml", + c -> ((ResourceDetailsController) c).setResourceId(resource.getUuid())); + } + + // ===== helpers ===== + + private static String formatDate(LocalDateTime value) { + return value == null ? "" : DATE_TIME_FORMAT.format(value); + } + + private void showError(String message) { + errorLabel.setText(message); + errorLabel.setVisible(true); + errorLabel.setManaged(true); + } + + private void hideError() { + errorLabel.setText(""); + errorLabel.setVisible(false); + errorLabel.setManaged(false); + } +} \ No newline at end of file diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java b/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java index 2e25530..3386be1 100644 --- a/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java +++ b/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java @@ -14,4 +14,7 @@ public abstract class Controller implements SessionAware, NavigatorAware, Servic protected Session session; protected Navigator navigator; protected ServiceRegistry services; + + public void onLoad() { + } } diff --git a/src/main/java/com/DesktopApplicationClientJava/services/ResourceService.java b/src/main/java/com/DesktopApplicationClientJava/services/ResourceService.java index 4075e5b..a74dd53 100644 --- a/src/main/java/com/DesktopApplicationClientJava/services/ResourceService.java +++ b/src/main/java/com/DesktopApplicationClientJava/services/ResourceService.java @@ -7,7 +7,8 @@ public interface ResourceService { List search(ResourceFilter filter, long offset, long count); - + List search(ResourceFilter filter, long offset, long count, + String sortBy, String sortDir); Resource create(String title, String description); Resource getById(UUID resourceId); diff --git a/src/main/java/com/DesktopApplicationClientJava/services/impl/ResourceServiceImpl.java b/src/main/java/com/DesktopApplicationClientJava/services/impl/ResourceServiceImpl.java index b2473cf..9066d7f 100644 --- a/src/main/java/com/DesktopApplicationClientJava/services/impl/ResourceServiceImpl.java +++ b/src/main/java/com/DesktopApplicationClientJava/services/impl/ResourceServiceImpl.java @@ -60,6 +60,21 @@ public Resource getById(UUID resourceId) { } } + @Override + public List search(ResourceFilter filter, long offset, long count, + String sortBy, String sortDir) { + ResourceFilterRequest req = new ResourceFilterRequest( + filter.getTitle(), filter.getDescription(), null, null); + try { + return apiClient.findResourcesByFilters(req, offset, count, sortBy, sortDir) + .stream().map(this::toEntity).toList(); + } catch (ApiException e) { + throw new ServiceException(e.getMessage(), e.getStatus()); + } catch (IOException | InterruptedException e) { + throw new ServiceException("Ошибка соединения с сервером"); + } + } + @Override public Resource update(UUID resourceId, String title, String description) { UpdateResourceRequest req = new UpdateResourceRequest(title, description); diff --git a/src/main/java/com/DesktopApplicationClientJava/services/mock/MockResourceService.java b/src/main/java/com/DesktopApplicationClientJava/services/mock/MockResourceService.java index 7689d38..ed3ac80 100644 --- a/src/main/java/com/DesktopApplicationClientJava/services/mock/MockResourceService.java +++ b/src/main/java/com/DesktopApplicationClientJava/services/mock/MockResourceService.java @@ -6,8 +6,10 @@ import com.DesktopApplicationClientJava.services.filter.ResourceFilter; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; /** In-memory {@link ResourceService} over {@link MockStore}. */ public class MockResourceService implements ResourceService, MockService { @@ -45,6 +47,35 @@ public Resource getById(UUID resourceId) { return resource; } + @Override + public List search(ResourceFilter filter, long offset, long count, + String sortBy, String sortDir) { + // 1. Сначала фильтрация (как в текущей 3-аргументной версии) + List filtered = store.getResources().values().stream() + .filter(r -> matches(r, filter)) + .collect(Collectors.toList()); + + // 2. Сортировка по sortBy/sortDir + Comparator comparator = switch (sortBy == null ? "createdAt" : sortBy) { + case "title" -> Comparator.comparing( + r -> r.getTitle() == null ? "" : r.getTitle(), + String.CASE_INSENSITIVE_ORDER); + default -> Comparator.comparing( + Resource::getCreatedAt, + Comparator.nullsLast(Comparator.naturalOrder())); + }; + if ("desc".equalsIgnoreCase(sortDir)) { + comparator = comparator.reversed(); + } + filtered.sort(comparator); + + // 3. Пагинация + int from = (int) Math.max(0, offset); + int to = (int) Math.min(filtered.size(), from + count); + if (from >= filtered.size()) return List.of(); + return List.copyOf(filtered.subList(from, to)); + } + @Override public Resource update(UUID resourceId, String title, String description) { Resource resource = getById(resourceId); diff --git a/src/main/resources/fxml/LoginForm.fxml b/src/main/resources/fxml/LoginForm.fxml index 52d75ac..74357d9 100644 --- a/src/main/resources/fxml/LoginForm.fxml +++ b/src/main/resources/fxml/LoginForm.fxml @@ -7,33 +7,52 @@ - - - - - - - -