diff --git a/commander/pom.xml b/commander/pom.xml
index e3291643..503a41e8 100644
--- a/commander/pom.xml
+++ b/commander/pom.xml
@@ -12,22 +12,22 @@
UTF-8
5.12.1
+ 21.0.6
org.openjfx
- javafx
- 21.0.6
- pom
- compile
+ javafx-controls
+ ${javafx.version}
org.openjfx
javafx-fxml
- 21.0.6
+ ${javafx.version}
+
org.junit.jupiter
junit-jupiter-api
@@ -53,13 +53,13 @@
25
+
org.openjfx
javafx-maven-plugin
0.0.8
-
default-cli
hse.java.commander/hse.java.commander.CommanderApplication
diff --git a/commander/src/main/java/hse/java/commander/CommanderApplication.java b/commander/src/main/java/hse/java/commander/CommanderApplication.java
index 5b1ecff3..2c33f7df 100644
--- a/commander/src/main/java/hse/java/commander/CommanderApplication.java
+++ b/commander/src/main/java/hse/java/commander/CommanderApplication.java
@@ -4,16 +4,21 @@
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
-
import java.io.IOException;
public class CommanderApplication extends Application {
@Override
public void start(Stage stage) throws IOException {
- FXMLLoader fxmlLoader = new FXMLLoader(CommanderApplication.class.getResource("commander-ui.fxml"));
- Scene scene = new Scene(fxmlLoader.load(), 400, 400);
+ FXMLLoader fxml_loader = new FXMLLoader(CommanderApplication.class.getResource("commander-ui.fxml"));
+ Scene scene = new Scene(fxml_loader.load());
+ var css = CommanderApplication.class.getResource("commander.css");
+
+ if (css != null) scene.getStylesheets().add(css.toExternalForm());
+
stage.setTitle("Commander");
stage.setScene(scene);
+ stage.sizeToScene();
+ stage.setResizable(false);
stage.show();
}
-}
+}
\ No newline at end of file
diff --git a/commander/src/main/java/hse/java/commander/FileOperations.java b/commander/src/main/java/hse/java/commander/FileOperations.java
new file mode 100644
index 00000000..2789d720
--- /dev/null
+++ b/commander/src/main/java/hse/java/commander/FileOperations.java
@@ -0,0 +1,76 @@
+package hse.java.commander;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+
+public final class FileOperations {
+
+ private FileOperations() {
+ }
+
+ public static Path detectProjectRoot() {
+ Path dir = Path.of(System.getProperty("user.dir")).toAbsolutePath().normalize();
+ if (dir.getFileName() != null && "commander".equals(dir.getFileName().toString()) && dir.getParent() != null)
+ return dir.getParent().toAbsolutePath().normalize();
+ return dir;
+ }
+
+ public static List list(Path dir, Path root_dir) throws IOException {
+ List entries = new ArrayList<>();
+ if (dir == null || !Files.isDirectory(dir)) return entries;
+
+ Path parent = dir.getParent();
+ if (parent != null && root_dir != null && parent.startsWith(root_dir) && !dir.equals(root_dir)) entries.add(PathEntry.parent(parent));
+
+ try (var ds = Files.newDirectoryStream(dir)) {
+ for (Path p : ds) entries.add(PathEntry.of(p));
+ }
+
+ sort(entries);
+ return entries;
+ }
+
+ public static void move(Path source, Path destination) throws IOException {
+ Files.move(source, destination, REPLACE_EXISTING);
+ }
+
+ public static void rename(Path source, String new_name) throws IOException {
+ Files.move(source, source.getParent().resolve(new_name).normalize(), REPLACE_EXISTING);
+ }
+
+ public static void deleteRecursively(Path path) throws IOException {
+ if (Files.notExists(path)) return;
+
+ if (Files.isDirectory(path)) {
+ try (var ds = Files.newDirectoryStream(path)) {
+ for (Path child : ds) deleteRecursively(child);
+ }
+ }
+
+ Files.deleteIfExists(path);
+ }
+
+ private static void sort(List entries) {
+ for (int i = 1; i < entries.size(); i++) {
+ PathEntry key = entries.get(i);
+ int j = i - 1;
+
+ while (j >= 0 && compare(entries.get(j), key) > 0) {
+ entries.set(j + 1, entries.get(j));
+ j -= 1;
+ }
+
+ entries.set(j + 1, key);
+ }
+ }
+
+ private static int compare(PathEntry a, PathEntry b) {
+ if (a.isDirectory() != b.isDirectory()) return a.isDirectory() ? -1 : 1;
+ return String.CASE_INSENSITIVE_ORDER.compare(a.toString(), b.toString());
+ }
+}
\ No newline at end of file
diff --git a/commander/src/main/java/hse/java/commander/MainController.java b/commander/src/main/java/hse/java/commander/MainController.java
index 19c700fb..2c9d661d 100644
--- a/commander/src/main/java/hse/java/commander/MainController.java
+++ b/commander/src/main/java/hse/java/commander/MainController.java
@@ -1,36 +1,263 @@
package hse.java.commander;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
import javafx.fxml.FXML;
-import javafx.scene.control.Button;
-import javafx.scene.control.ListView;
+import javafx.scene.control.*;
+import javafx.scene.control.ButtonBar.ButtonData;
+import javafx.scene.input.MouseEvent;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
public class MainController {
@FXML
- public Button move;
+ private ListView left;
- public void initialize() {
- move.setOnMouseClicked(event -> {
+ @FXML
+ private ListView right;
- });
- System.out.println(System.getProperty("user.home"));
- left.getItems().add("Kek");
-
- left.setOnMouseClicked(event -> {
- if (event.getClickCount() == 2) {
- int index = left.getSelectionModel().getSelectedIndex();
- if (index >= 0) {
- left.getItems().set(index, "clicked");
- }
- }
- });
- }
+ @FXML
+ private Button left_to_right;
+
+ @FXML
+ private Button right_to_left;
@FXML
- public ListView left;
+ private Button rename;
@FXML
- public ListView right;
+ private Button remove;
+
+ @FXML
+ private Button refresh;
+
+ private Path root_dir;
+ private Path left_dir;
+ private Path right_dir;
+
+ private boolean left_active = true;
+ private String css_url;
+
+ public void initialize() {
+ root_dir = FileOperations.detectProjectRoot();
+ left_dir = root_dir;
+ right_dir = root_dir;
+
+ var css = MainController.class.getResource("commander.css");
+ css_url = css == null ? null : css.toExternalForm();
+
+ left.setOnMouseClicked(new EventHandler<>() {
+ @Override
+ public void handle(MouseEvent event) {
+ left_active = true;
+ if (event.getClickCount() != 2) return;
+ openSelected(true);
+ }
+ });
+
+ right.setOnMouseClicked(new EventHandler<>() {
+ @Override
+ public void handle(MouseEvent event) {
+ left_active = false;
+ if (event.getClickCount() != 2) return;
+ openSelected(false);
+ }
+ });
+
+ left_to_right.setOnAction(new EventHandler<>() {
+ @Override
+ public void handle(ActionEvent event) {
+ move(true);
+ }
+ });
+
+ right_to_left.setOnAction(new EventHandler<>() {
+ @Override
+ public void handle(ActionEvent event) {
+ move(false);
+ }
+ });
+
+ rename.setOnAction(new EventHandler<>() {
+ @Override
+ public void handle(ActionEvent event) {
+ renameSelected();
+ }
+ });
+
+ remove.setOnAction(new EventHandler<>() {
+ @Override
+ public void handle(ActionEvent event) {
+ removeSelected();
+ }
+ });
+
+ refresh.setOnAction(new EventHandler<>() {
+ @Override
+ public void handle(ActionEvent event) {
+ refreshBoth();
+ }
+ });
+
+ refreshBoth();
+ }
+
+ private void refreshBoth() {
+ refreshPanel(true);
+ refreshPanel(false);
+ }
+
+ private void refreshPanel(boolean is_left) {
+ Path dir = is_left ? left_dir : right_dir;
+ ListView view = is_left ? left : right;
+
+ try {
+ List entries = FileOperations.list(dir, root_dir);
+ view.getItems().setAll(entries);
+ } catch (IOException e) {
+ view.getItems().clear();
+ }
+ }
+
+ private void openSelected(boolean is_left) {
+ ListView view = is_left ? left : right;
+ PathEntry selected = view.getSelectionModel().getSelectedItem();
+ if (selected == null) return;
+
+ if (selected.isParent() || selected.isDirectory()) {
+ if (is_left) left_dir = selected.path();
+ else right_dir = selected.path();
+ refreshPanel(is_left);
+ return;
+ }
+
+ previewFile(selected.path());
+ }
+
+ private void move(boolean left_to_right_move) {
+ ListView from_view = left_to_right_move ? left : right;
+ Path from_dir = left_to_right_move ? left_dir : right_dir;
+ Path to_dir = left_to_right_move ? right_dir : left_dir;
+
+ PathEntry selected = from_view.getSelectionModel().getSelectedItem();
+ if (selected == null || selected.isParent()) return;
+
+ Path source = selected.path();
+ Path destination = to_dir.resolve(source.getFileName()).normalize();
+
+ if (Files.exists(destination) && isCancelled("Заменить существующий файл?", destination.toString())) return;
+
+ try {
+ FileOperations.move(source, destination);
+ refreshBoth();
+ } catch (IOException e) {
+ error("Не удалось перенести", e.getMessage());
+ }
+ }
+
+ private void renameSelected() {
+ ListView view = left_active ? left : right;
+ PathEntry selected = view.getSelectionModel().getSelectedItem();
+ if (selected == null || selected.isParent()) return;
+
+ Path path = selected.path();
+ String current_name = path.getFileName().toString();
+
+ TextInputDialog dialog = new TextInputDialog(current_name);
+ dialog.setTitle("Переименовать");
+ dialog.setHeaderText("Переименовать");
+ dialog.setContentText("Новое имя:");
+ applyDialogTheme(dialog.getDialogPane());
+
+ String new_name = dialog.showAndWait().orElse("").trim();
+ if (new_name.isEmpty() || new_name.equals(current_name)) return;
+
+ if (new_name.indexOf('/') >= 0 || new_name.indexOf('\\') >= 0) {
+ error("Некорректное имя", "Имя не должно содержать разделители пути");
+ return;
+ }
+
+ Path destination = path.getParent().resolve(new_name).normalize();
+ if (Files.exists(destination) && isCancelled("Заменить существующий файл?", destination.toString())) return;
+
+ try {
+ FileOperations.rename(path, new_name);
+ refreshBoth();
+ } catch (IOException e) {
+ error("Не удалось переименовать", e.getMessage());
+ }
+ }
+
+ private void removeSelected() {
+ ListView view = left_active ? left : right;
+ PathEntry selected = view.getSelectionModel().getSelectedItem();
+ if (selected == null || selected.isParent()) return;
+
+ Path path = selected.path();
+ if (isCancelled("Удалить выбранный объект?", path.toString())) return;
+
+ try {
+ FileOperations.deleteRecursively(path);
+ refreshBoth();
+ } catch (IOException e) {
+ error("Не удалось удалить", e.getMessage());
+ }
+ }
+
+ private void previewFile(Path path) {
+ String content;
+ try {
+ content = Files.readString(path);
+ } catch (IOException e) {
+ error("Не удалось прочитать файл", e.getMessage());
+ return;
+ }
+
+ Alert alert = new Alert(Alert.AlertType.INFORMATION);
+ alert.setTitle("Просмотр");
+ alert.setHeaderText(path.getFileName() == null ? path.toString() : path.getFileName().toString());
+
+ TextArea area = new TextArea(content);
+ area.setEditable(false);
+ area.setWrapText(false);
+ area.setPrefColumnCount(80);
+ area.setPrefRowCount(25);
+
+ alert.getDialogPane().setContent(area);
+ applyDialogTheme(alert.getDialogPane());
+ alert.getButtonTypes().setAll(new ButtonType("Закрыть", ButtonData.CANCEL_CLOSE));
+ alert.showAndWait();
+ }
+
+ private boolean isCancelled(String header, String content) {
+ Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
+ alert.setTitle("Подтверждение");
+ alert.setHeaderText(header);
+ alert.setContentText(content);
+ applyDialogTheme(alert.getDialogPane());
+
+ ButtonType ok = new ButtonType("ОК", ButtonData.OK_DONE);
+ ButtonType cancel = new ButtonType("Отмена", ButtonData.CANCEL_CLOSE);
+ alert.getButtonTypes().setAll(ok, cancel);
+
+ return alert.showAndWait().orElse(cancel) == cancel;
+ }
+ private void error(String title, String message) {
+ Alert alert = new Alert(Alert.AlertType.ERROR);
+ alert.setTitle(title);
+ alert.setHeaderText(title);
+ alert.setContentText(message == null ? "" : message);
+ applyDialogTheme(alert.getDialogPane());
+ alert.showAndWait();
+ }
-}
+ private void applyDialogTheme(DialogPane pane) {
+ if (css_url == null || pane.getStylesheets().contains(css_url)) return;
+ pane.getStylesheets().add(css_url);
+ }
+}
\ No newline at end of file
diff --git a/commander/src/main/java/hse/java/commander/PathEntry.java b/commander/src/main/java/hse/java/commander/PathEntry.java
new file mode 100644
index 00000000..0f8f5b61
--- /dev/null
+++ b/commander/src/main/java/hse/java/commander/PathEntry.java
@@ -0,0 +1,36 @@
+package hse.java.commander;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public final class PathEntry {
+
+ private final Path path;
+ private final String display_name;
+ private final boolean parent;
+
+ private PathEntry(Path path, String display_name, boolean parent) {
+ this.path = path;
+ this.display_name = display_name;
+ this.parent = parent;
+ }
+
+ public static PathEntry parent(Path parent_dir) {
+ return new PathEntry(parent_dir, "..", true);
+ }
+
+ public static PathEntry of(Path path) {
+ String name = path.getFileName() == null ? path.toString() : path.getFileName().toString();
+ if (Files.isDirectory(path)) name += "/";
+ return new PathEntry(path, name, false);
+ }
+
+ public Path path() {return path;}
+
+ public boolean isParent() {return parent;}
+
+ public boolean isDirectory() {return Files.isDirectory(path);}
+
+ @Override
+ public String toString() {return display_name;}
+}
\ No newline at end of file
diff --git a/commander/src/main/resources/hse/java/commander/commander-ui.fxml b/commander/src/main/resources/hse/java/commander/commander-ui.fxml
index 23d8db23..9e616389 100644
--- a/commander/src/main/resources/hse/java/commander/commander-ui.fxml
+++ b/commander/src/main/resources/hse/java/commander/commander-ui.fxml
@@ -3,12 +3,21 @@
-
+
-
+
+
+
+
+
+
+
+
-
-
+
\ No newline at end of file
diff --git a/commander/src/main/resources/hse/java/commander/commander.css b/commander/src/main/resources/hse/java/commander/commander.css
new file mode 100644
index 00000000..0e90a117
--- /dev/null
+++ b/commander/src/main/resources/hse/java/commander/commander.css
@@ -0,0 +1,118 @@
+.root {
+ -fx-background-color: #0D1B2A;
+ -fx-font-smoothing-type: lcd;
+}
+
+.label {
+ -fx-text-fill: #E0E1DD;
+}
+
+.list-view {
+ -fx-background-color: #1B263B;
+ -fx-control-inner-background: #1B263B;
+ -fx-border-color: #415A77;
+ -fx-border-width: 1;
+
+ -fx-background-radius: 12;
+ -fx-border-radius: 12;
+}
+
+.list-view .viewport {
+ -fx-background-color: #1B263B;
+ -fx-background-radius: 12;
+}
+
+.list-view .corner {
+ -fx-background-color: #1B263B;
+}
+
+.list-cell {
+ -fx-background-color: #1B263B;
+ -fx-text-fill: #E0E1DD;
+
+ -fx-border-color: #415A77;
+ -fx-border-width: 0 0 1 0;
+}
+
+.list-cell:hover {
+ -fx-background-color: #415A77;
+}
+
+.list-cell:filled:selected {
+ -fx-background-color: #778DA9;
+ -fx-text-fill: #0D1B2A;
+}
+
+.button {
+ -fx-background-color: #1B263B;
+ -fx-text-fill: #E0E1DD;
+
+ -fx-border-color: #415A77;
+ -fx-border-width: 1;
+
+ -fx-background-radius: 10;
+ -fx-border-radius: 10;
+ -fx-padding: 6 12 6 12;
+}
+
+.button:hover {
+ -fx-background-color: #415A77;
+}
+
+.button:pressed {
+ -fx-background-color: #778DA9;
+ -fx-text-fill: #0D1B2A;
+}
+
+.dialog-pane {
+ -fx-background-color: #0D1B2A;
+}
+
+.dialog-pane .header-panel {
+ -fx-background-color: #0D1B2A;
+}
+
+.dialog-pane .content {
+ -fx-background-color: #0D1B2A;
+}
+
+.dialog-pane .label {
+ -fx-text-fill: #E0E1DD;
+}
+
+.dialog-pane .text-field {
+ -fx-background-color: #1B263B;
+ -fx-text-fill: #E0E1DD;
+ -fx-border-color: #415A77;
+ -fx-border-width: 1;
+ -fx-background-radius: 8;
+ -fx-border-radius: 8;
+}
+
+.dialog-pane .text-area {
+ -fx-control-inner-background: #1B263B;
+ -fx-text-fill: #E0E1DD;
+ -fx-border-color: #415A77;
+ -fx-border-width: 1;
+ -fx-background-radius: 8;
+ -fx-border-radius: 8;
+}
+
+/* Scrollbars */
+.scroll-bar:vertical, .scroll-bar:horizontal {
+ -fx-background-color: #0D1B2A;
+}
+
+.scroll-bar .track {
+ -fx-background-color: #0D1B2A;
+ -fx-border-color: #415A77;
+}
+
+.scroll-bar .thumb {
+ -fx-background-color: #415A77;
+ -fx-background-radius: 8;
+}
+
+.scroll-bar .thumb:hover {
+ -fx-background-color: #778DA9;
+}
\ No newline at end of file
diff --git a/commander/target/classes/hse/java/commander/commander-ui.fxml b/commander/target/classes/hse/java/commander/commander-ui.fxml
index 23d8db23..9e616389 100644
--- a/commander/target/classes/hse/java/commander/commander-ui.fxml
+++ b/commander/target/classes/hse/java/commander/commander-ui.fxml
@@ -3,12 +3,21 @@
-
+
-
+
+
+
+
+
+
+
+
-
-
+
\ No newline at end of file
diff --git a/commander/target/classes/hse/java/commander/commander.css b/commander/target/classes/hse/java/commander/commander.css
new file mode 100644
index 00000000..0e90a117
--- /dev/null
+++ b/commander/target/classes/hse/java/commander/commander.css
@@ -0,0 +1,118 @@
+.root {
+ -fx-background-color: #0D1B2A;
+ -fx-font-smoothing-type: lcd;
+}
+
+.label {
+ -fx-text-fill: #E0E1DD;
+}
+
+.list-view {
+ -fx-background-color: #1B263B;
+ -fx-control-inner-background: #1B263B;
+ -fx-border-color: #415A77;
+ -fx-border-width: 1;
+
+ -fx-background-radius: 12;
+ -fx-border-radius: 12;
+}
+
+.list-view .viewport {
+ -fx-background-color: #1B263B;
+ -fx-background-radius: 12;
+}
+
+.list-view .corner {
+ -fx-background-color: #1B263B;
+}
+
+.list-cell {
+ -fx-background-color: #1B263B;
+ -fx-text-fill: #E0E1DD;
+
+ -fx-border-color: #415A77;
+ -fx-border-width: 0 0 1 0;
+}
+
+.list-cell:hover {
+ -fx-background-color: #415A77;
+}
+
+.list-cell:filled:selected {
+ -fx-background-color: #778DA9;
+ -fx-text-fill: #0D1B2A;
+}
+
+.button {
+ -fx-background-color: #1B263B;
+ -fx-text-fill: #E0E1DD;
+
+ -fx-border-color: #415A77;
+ -fx-border-width: 1;
+
+ -fx-background-radius: 10;
+ -fx-border-radius: 10;
+ -fx-padding: 6 12 6 12;
+}
+
+.button:hover {
+ -fx-background-color: #415A77;
+}
+
+.button:pressed {
+ -fx-background-color: #778DA9;
+ -fx-text-fill: #0D1B2A;
+}
+
+.dialog-pane {
+ -fx-background-color: #0D1B2A;
+}
+
+.dialog-pane .header-panel {
+ -fx-background-color: #0D1B2A;
+}
+
+.dialog-pane .content {
+ -fx-background-color: #0D1B2A;
+}
+
+.dialog-pane .label {
+ -fx-text-fill: #E0E1DD;
+}
+
+.dialog-pane .text-field {
+ -fx-background-color: #1B263B;
+ -fx-text-fill: #E0E1DD;
+ -fx-border-color: #415A77;
+ -fx-border-width: 1;
+ -fx-background-radius: 8;
+ -fx-border-radius: 8;
+}
+
+.dialog-pane .text-area {
+ -fx-control-inner-background: #1B263B;
+ -fx-text-fill: #E0E1DD;
+ -fx-border-color: #415A77;
+ -fx-border-width: 1;
+ -fx-background-radius: 8;
+ -fx-border-radius: 8;
+}
+
+/* Scrollbars */
+.scroll-bar:vertical, .scroll-bar:horizontal {
+ -fx-background-color: #0D1B2A;
+}
+
+.scroll-bar .track {
+ -fx-background-color: #0D1B2A;
+ -fx-border-color: #415A77;
+}
+
+.scroll-bar .thumb {
+ -fx-background-color: #415A77;
+ -fx-background-radius: 8;
+}
+
+.scroll-bar .thumb:hover {
+ -fx-background-color: #778DA9;
+}
\ No newline at end of file
diff --git a/commander/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/commander/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
new file mode 100644
index 00000000..85e455a9
--- /dev/null
+++ b/commander/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
@@ -0,0 +1,13 @@
+hse\java\commander\MainController$1.class
+hse\java\commander\PathEntry.class
+hse\java\commander\MainController$6.class
+hse\java\commander\MainController$5.class
+module-info.class
+hse\java\commander\MainController$4.class
+hse\java\commander\CommanderApplication.class
+hse\java\commander\MainController$2.class
+hse\java\commander\Launcher.class
+hse\java\commander\MainController.class
+hse\java\commander\MainController$3.class
+hse\java\commander\FileOperations.class
+hse\java\commander\MainController$7.class
diff --git a/commander/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/commander/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
new file mode 100644
index 00000000..9f663837
--- /dev/null
+++ b/commander/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
@@ -0,0 +1,6 @@
+C:\Users\vikat\Documents\coding\hse-java\commander\src\main\java\hse\java\commander\CommanderApplication.java
+C:\Users\vikat\Documents\coding\hse-java\commander\src\main\java\hse\java\commander\FileOperations.java
+C:\Users\vikat\Documents\coding\hse-java\commander\src\main\java\hse\java\commander\Launcher.java
+C:\Users\vikat\Documents\coding\hse-java\commander\src\main\java\hse\java\commander\MainController.java
+C:\Users\vikat\Documents\coding\hse-java\commander\src\main\java\hse\java\commander\PathEntry.java
+C:\Users\vikat\Documents\coding\hse-java\commander\src\main\java\module-info.java