1 package de.spiritscorp.datasync.gui;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 import java.io.File;
24 import java.nio.file.Path;
25 import java.nio.file.Paths;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.List;
29
30 import org.kordamp.ikonli.materialdesign2.MaterialDesignD;
31 import org.kordamp.ikonli.materialdesign2.MaterialDesignP;
32 import org.kordamp.ikonli.materialdesign2.MaterialDesignS;
33
34 import de.spiritscorp.datasync.BgTime;
35 import de.spiritscorp.datasync.Main;
36 import de.spiritscorp.datasync.ScanType;
37 import de.spiritscorp.datasync.controller.SyncJobContext;
38 import de.spiritscorp.datasync.controller.ViewController;
39 import de.spiritscorp.datasync.io.Preference;
40 import de.spiritscorp.datasync.io.PreferenceManager;
41 import de.spiritscorp.datasync.theme.AppTheme;
42 import javafx.animation.KeyFrame;
43 import javafx.animation.Timeline;
44 import javafx.collections.FXCollections;
45 import javafx.collections.ObservableList;
46 import javafx.geometry.Insets;
47 import javafx.geometry.Pos;
48 import javafx.scene.Node;
49 import javafx.scene.control.Button;
50 import javafx.scene.control.CheckBox;
51 import javafx.scene.control.ComboBox;
52 import javafx.scene.control.Label;
53 import javafx.scene.control.ListCell;
54 import javafx.scene.control.ListView;
55 import javafx.scene.control.ProgressBar;
56 import javafx.scene.control.ScrollPane;
57 import javafx.scene.control.Separator;
58 import javafx.scene.control.TableColumn;
59 import javafx.scene.control.TableView;
60 import javafx.scene.control.TextArea;
61 import javafx.scene.control.TextField;
62 import javafx.scene.control.Tooltip;
63 import javafx.scene.control.cell.CheckBoxTableCell;
64 import javafx.scene.layout.GridPane;
65 import javafx.scene.layout.HBox;
66 import javafx.scene.layout.Priority;
67 import javafx.scene.layout.StackPane;
68 import javafx.scene.layout.VBox;
69 import javafx.stage.DirectoryChooser;
70 import javafx.util.Duration;
71
72
73
74
75
76
77 public final class WorkspaceView extends VBox {
78
79 private final Gui mainGui;
80 private final ViewController controller;
81 private final Label workspaceHeaderLabel;
82 private final Label contextInfoLabel;
83 private final HBox controlToolbar;
84 private final StackPane centerViewport;
85
86 private final ScrollPane consoleViewNode;
87 private final TextArea consoleTextArea;
88 private final VBox duplicateViewNode;
89 private TableView<SyncJobContext.FileRow> duplicateTable;
90
91 private final Button actionButton;
92 private final Button cancelButton;
93 private Button deleteButton;
94 private final ProgressBar progressBar;
95 private final Label statusLabel;
96
97 public enum NotifyStatus {
98 SUCESS( "status-success"),
99 ERROR( "status-error"),
100 WARNING( "status-warning");
101
102 private final String cssClass;
103
104 NotifyStatus( String cssClass ) {
105 this.cssClass = cssClass;
106 }
107
108 String getCssClass() { return cssClass; }
109 }
110
111
112
113
114
115
116
117 public WorkspaceView( Gui mainGui, ViewController controller ) {
118 this.mainGui = mainGui;
119 this.controller = controller;
120 this.setPadding( new Insets( 24 ) );
121 this.setSpacing( 12 );
122
123 workspaceHeaderLabel = new Label( "Kein Task aktiv" );
124 workspaceHeaderLabel.setStyle( "-fx-font-size: 22px; -fx-font-weight: bold;" );
125
126
127 contextInfoLabel = new Label( "" );
128 contextInfoLabel.setStyle( "-fx-font-size: 13px; -fx-font-style: italic; -fx-padding: 0 0 8px 0;" );
129
130 controlToolbar = new HBox( 12 );
131 controlToolbar.setAlignment( Pos.CENTER_LEFT );
132
133 actionButton = new Button( "Ausführen", Gui.createIcon( MaterialDesignP.PLAY ) );
134 actionButton.setStyle( "-fx-background-color: #2ecc71; -fx-text-fill: white; -fx-font-weight: bold; -fx-padding: 10px 20px;" );
135 actionButton.setTooltip( new Tooltip( "Starte Job" ) );
136 cancelButton = new Button( "Abbrechen", Gui.createIcon( MaterialDesignS.STOP ) );
137 cancelButton.setStyle( "-fx-background-color: #95a5a6; -fx-text-fill: white; -fx-font-weight: bold; -fx-padding: 10px 16px;" );
138 cancelButton.setTooltip( new Tooltip( "Stoppe Job" ) );
139
140 controlToolbar.getChildren().addAll( actionButton, cancelButton );
141
142 consoleTextArea = new TextArea();
143 consoleTextArea.setEditable( false );
144 consoleTextArea.setStyle( " -fx-font-family: 'Consolas', monospace; -fx-font-size: 12px;" );
145 consoleViewNode = new ScrollPane( consoleTextArea );
146 consoleViewNode.setFitToWidth( true );
147 consoleViewNode.setFitToHeight( true );
148
149 duplicateViewNode = assembleDuplicateTableView();
150 centerViewport = new StackPane( consoleViewNode );
151
152 final HBox statusFooter = new HBox( 12 );
153 statusFooter.setAlignment( Pos.CENTER_LEFT );
154 progressBar = new ProgressBar( 0 );
155 progressBar.setTooltip( new Tooltip( "Aktueller Fortschritt" ) );
156 statusLabel = new Label( "Bereit" );
157 statusLabel.setTooltip( new Tooltip( "Aktueller Status" ) );
158 statusFooter.getChildren().addAll( progressBar, statusLabel );
159
160 this.getChildren().addAll( workspaceHeaderLabel, contextInfoLabel, controlToolbar, centerViewport, statusFooter );
161 setVgrow( centerViewport, Priority.ALWAYS );
162 }
163
164
165
166
167 private VBox assembleDuplicateTableView() {
168 duplicateTable = new TableView<>();
169 duplicateTable.setEditable( true );
170 duplicateTable.setFixedCellSize( 24.0 );
171
172 final TableColumn<SyncJobContext.FileRow, Boolean> selCol = new TableColumn<>( "Auswahl" );
173 selCol.setCellValueFactory( d -> d.getValue().selectedProperty() );
174 selCol.setCellFactory( CheckBoxTableCell.forTableColumn( selCol ) );
175 selCol.setPrefWidth( 50 );
176
177 final TableColumn<SyncJobContext.FileRow, String> nameCol = new TableColumn<>( "Dateiname" );
178 nameCol.setCellValueFactory( d -> d.getValue().fileNameProperty() );
179 nameCol.setPrefWidth( 250 );
180
181 final TableColumn<SyncJobContext.FileRow, String> sizeCol = new TableColumn<>( "Größe" );
182 sizeCol.setCellValueFactory( d -> d.getValue().sizeProperty() );
183 sizeCol.setPrefWidth( 250 );
184
185 final TableColumn<SyncJobContext.FileRow, String> pathCol = new TableColumn<>( "Pfad" );
186 pathCol.setCellValueFactory( d -> d.getValue().pathProperty() );
187 pathCol.setPrefWidth( 600 );
188
189 final TableColumn<SyncJobContext.FileRow, String> hashCol = new TableColumn<>( "Hash" );
190 hashCol.setCellValueFactory( d -> d.getValue().hashProperty() );
191 hashCol.setPrefWidth( 250 );
192
193 duplicateTable.getColumns().addAll( List.of( selCol, nameCol, sizeCol, hashCol, pathCol ) );
194 deleteButton = new Button( "Duplikate löschen", Gui.createIcon( MaterialDesignD.DELETE ) );
195 deleteButton.setStyle( "-fx-background-color: #e74c3c; -fx-text-fill: white;" );
196 deleteButton.setTooltip( new Tooltip( "Ausgewählte Dateien werden gelöscht" ) );
197
198 final VBox frame = new VBox( 8, duplicateTable, deleteButton );
199 setVgrow( duplicateTable, Priority.ALWAYS );
200 return frame;
201 }
202
203
204
205
206
207
208
209 public void refreshView( final Gui.ViewState state, final SyncJobContext job ) {
210 if( job == null ) return;
211
212 centerViewport.getChildren().clear();
213
214 if( state == Gui.ViewState.MONITOR ) {
215 workspaceHeaderLabel.setText( "Task-Monitor: " + job.getJobName() );
216 controlToolbar.setVisible( true );
217
218
219 final Preference p = job.getPreference();
220 final String src = p.getSourcePath() != null ? Arrays.toString( p.getSourcePath().toArray() ) : "Keine Quelle";
221 final String dest = p.getDestPath() != null && !p.getDestPath().isEmpty() ? p.getDestPath().toString() : "Kein Ziel";
222
223 if( ScanType.DUBLICATE_SCAN.equals( job.getSelectedMode() ) ) {
224 contextInfoLabel.setText( String.format( "Modus: %s | Ziel: %s", job.getSelectedMode().getDescription(), src ) );
225 centerViewport.getChildren().add( duplicateViewNode );
226 }else {
227 contextInfoLabel.setText( String.format( "Modus: %s | Quelle: %s | Ziel: %s", job.getSelectedMode().getDescription(), src, dest ) );
228 centerViewport.getChildren().add( consoleViewNode );
229 }
230 }else if( state == Gui.ViewState.SETTINGS ) {
231 workspaceHeaderLabel.setText( "Einstellungen für: " + job.getJobName() );
232 contextInfoLabel.setText( "Konfiguration der task-spezifischen Ablaufparameter, Dateiattribute und Verzeichnisstrukturen." );
233 controlToolbar.setVisible( false );
234 displayCustomViewNode( buildSettingsGridTab( job ) );
235 }
236 }
237
238
239
240
241
242 public void displayCustomViewNode( Node content ) {
243 centerViewport.getChildren().clear();
244 final ScrollPane scroll = new ScrollPane( content );
245 scroll.setFitToWidth( true );
246 scroll.setPadding( new Insets( 12 ) );
247 centerViewport.getChildren().add( scroll );
248 }
249
250
251
252
253
254 public void bindJob( SyncJobContext job ) {
255 statusLabel.textProperty().unbind();
256 consoleTextArea.textProperty().unbind();
257 statusLabel.textProperty().bind( job.statusMessageProperty() );
258 consoleTextArea.textProperty().bind( job.logOutputProperty() );
259 duplicateTable.setItems( job.getDuplicateFiles() );
260
261 cancelButton.disableProperty().unbind();
262 actionButton.disableProperty().unbind();
263 cancelButton.disableProperty().bind( job.runningProperty().not() );
264 actionButton.disableProperty().bind( job.runningProperty() );
265
266 cancelButton.setOnAction( _ -> controller.handleStopTask( job ) );
267 actionButton.setOnAction( _ -> controller.handleExecuteTask( job ) );
268 deleteButton.setOnAction( _ -> controller.deleteSelectedDuplicates( job ) );
269 }
270
271
272
273
274
275
276
277
278
279
280 void displayTemporaryStatus( String message, NotifyStatus cssNotifyStatus, int durationSec ) {
281 final String originalContextText = "Konfiguration der task-spezifischen Ablaufparameter, Dateiattribute und Verzeichnisstrukturen.";
282 final String originalStyle = contextInfoLabel.getStyle();
283
284 contextInfoLabel.getStyleClass().removeAll( "status-success", "status-error", "status-warning" );
285
286
287
288
289 switch( cssNotifyStatus ) {
290 case SUCESS -> {
291 contextInfoLabel.setStyle( "-fx-text-fill: #22aa22 !important; -fx-font-weight: bold;" );
292 contextInfoLabel.setText( "✔ " + message );
293 }
294 case ERROR -> {
295 contextInfoLabel.setStyle( "-fx-text-fill: #ff3333 !important; -fx-font-weight: bold;" );
296 contextInfoLabel.setText( "❌ " + message );
297 }
298 case WARNING -> {
299 contextInfoLabel.setStyle( "-fx-text-fill: #ffaa00 !important; -fx-font-weight: bold;" );
300 contextInfoLabel.setText( "⚠ " + message );
301 }
302 }
303
304
305
306
307 contextInfoLabel.getStyleClass().add( cssNotifyStatus.getCssClass() );
308
309
310 final Timeline fallbackTimeline = new Timeline( new KeyFrame(
311 Duration.seconds( durationSec ),
312 _ -> {
313 contextInfoLabel.setText( originalContextText );
314 contextInfoLabel.getStyleClass().remove( cssNotifyStatus.getCssClass() );
315 contextInfoLabel.setStyle( originalStyle );
316 } ) );
317
318 fallbackTimeline.setCycleCount( 1 );
319 fallbackTimeline.play();
320 }
321
322
323
324
325 private Node buildSettingsGridTab( SyncJobContext job ) {
326 final Preference pref = job.getPreference();
327 final GridPane grid = new GridPane();
328 grid.setHgap( 24 );
329 grid.setVgap( 16 );
330 grid.setPadding( new Insets( 10 ) );
331
332
333 final Label modeTitle = new Label( "Ausführungsmodus:" );
334 modeTitle.setStyle( "-fx-font-weight: bold;" );
335 final ComboBox<String> taskModeComboBox = new ComboBox<>();
336 taskModeComboBox.setTooltip( new Tooltip( "Listet die möglichen Betriebsmodis auf" ) );
337 taskModeComboBox.getItems().addAll( ScanType.getAllDescriptions() );
338 job.selectedModeProperty().set( pref.getScanMode() != null ? pref.getScanMode().getDescription() : ScanType.FLAT_SCAN.getDescription() );
339 taskModeComboBox.valueProperty().bindBidirectional( job.selectedModeProperty() );
340 taskModeComboBox.setPrefWidth( 260 );
341 grid.add( modeTitle, 0, 0 );
342 grid.add( taskModeComboBox, 1, 0 );
343
344
345 final VBox dynamicPathsContainer = new VBox( 12 );
346 grid.add( dynamicPathsContainer, 0, 1, 2, 1 );
347
348 final PathContext pathCtx = new PathContext();
349 if( pref.getSourcePath() != null ) {
350 pathCtx.sources.addAll( pref.getSourcePath() );
351 }
352 if( pref.getDestPath() != null ) {
353 pathCtx.destinations.addAll( pref.getDestPath() );
354 }
355
356 taskModeComboBox.valueProperty().addListener( ( obs, o, n ) -> renderContextPaths( ScanType.get( n ), dynamicPathsContainer, pref, pathCtx ) );
357 renderContextPaths( job.getSelectedMode(), dynamicPathsContainer, pref, pathCtx );
358
359
360 final Label paramsTitle = new Label( "Erweiterte Ablaufparameter" );
361 paramsTitle.setStyle( "-fx-font-size: 14px; -fx-font-weight: bold;" );
362
363 final CheckBox subDirCheck = new CheckBox( "Unterordner einbeziehen (SubDir)" );
364 subDirCheck.setSelected( pref.isSubDir() );
365 final String subDirText = """
366 Aktiviert: Kopiert nur die nackten Dateien und Unterordner DIREKT in das Zielverzeichnis
367 (ideal, um mehrere Quellen in einem einzigen Zielordner zusammenzuführen).
368 Deaktiviert: Erstellt für jeden Quellpfad einen eigenen Hauptordner im Zielverzeichnis, um die Quellen sauber voneinander zu trennen.
369 """;
370 subDirCheck.setTooltip( new Tooltip( subDirText ) );
371 final CheckBox trashbinCheck = new CheckBox( "Papierkorb verwenden (Trashbin)" );
372 trashbinCheck.setSelected( pref.isTrashbin() );
373 trashbinCheck.setTooltip( new Tooltip( "Verschiebt modifizierte/gelöschte Dateien temporär in Sicherungsstrukturen" ) );
374 final CheckBox autoDelCheck = new CheckBox( "Automatisches Löschen erlauben (AutoDel)" );
375 autoDelCheck.setSelected( pref.isAutoDel() );
376 autoDelCheck.setTooltip( new Tooltip( "Erlaubt dem System, verwaiste Dateien im Zielordner restlos zu bereinigen" ) );
377 final CheckBox autoSyncCheck = new CheckBox( "Automatisches kopieren erlauben (AutoSync)" );
378 autoSyncCheck.setSelected( pref.isAutoSync() );
379 autoSyncCheck.setTooltip( new Tooltip( "Erlaubt dem System, nach einem Scan alle nötigen Dateien zu kopieren." ) );
380 final CheckBox logOnCheck = new CheckBox( "Protokollierung aktivieren (LogOn)" );
381 logOnCheck.setSelected( pref.isLogOn() );
382 logOnCheck.setTooltip( new Tooltip( "Schreibt detaillierte Transaktionsprotokolle in das System-Logverzeichnis" ) );
383 final CheckBox bgSyncCheck = new CheckBox( "Hintergrund-Synchronisation aktiv" );
384 bgSyncCheck.setSelected( pref.isBgSync() );
385 bgSyncCheck.setTooltip( new Tooltip( "Setzt den Autostart und aktiviert die Hintergrundsyncronisierung im nachfolgenden Intervall" ) );
386
387 final Label bgTimeLabel = new Label( "Hintergrund Scan-Intervall:" );
388 final ComboBox<String> bgTimeComboBox = new ComboBox<>();
389 bgTimeComboBox.setTooltip( new Tooltip( "Listet die möglichen Job Intervalle auf" ) );
390 bgTimeComboBox.getItems().addAll( BgTime.getNames() );
391 bgTimeComboBox.getSelectionModel().select( pref.getBgTime() != null ? pref.getBgTime().getName() : BgTime.MIN_30.getName() );
392 bgTimeComboBox.disableProperty().bind( bgSyncCheck.selectedProperty().not() );
393
394 final VBox optionsBox = new VBox( 12, paramsTitle, subDirCheck, trashbinCheck, autoDelCheck, autoSyncCheck, logOnCheck, bgSyncCheck, new HBox( 8, bgTimeLabel, bgTimeComboBox ) );
395 grid.add( optionsBox, 0, 2, 2, 1 );
396
397
398 final Label globalTitle = new Label( "Globale System-Konfiguration" );
399 globalTitle.setStyle( "-fx-font-size: 14px; -fx-font-weight: bold;" );
400
401 final CheckBox globalAutostartCheck = new CheckBox( "DataSync beim Systemstart minimiert laden (Autostart OS)" );
402 globalAutostartCheck.setSelected( PreferenceManager.getInstance().isGlobalAutoStart() );
403
404
405 final Label themeLabel = new Label( "Visuelles Anwendungs-Theme:" );
406 final ComboBox<AppTheme> themeComboBox = new ComboBox<>( mainGui.getAvailableThemes() );
407 themeComboBox.setTooltip( new Tooltip( "Listet alle möglichen Themes auf" ) );
408
409 themeComboBox.setCellFactory( _ -> new ListCell<>() {
410 @Override
411 protected void updateItem( AppTheme item, boolean empty ) {
412 super.updateItem( item, empty );
413 setText( empty || item == null ? "" : item.getName() );
414 }
415 } );
416 themeComboBox.setButtonCell( new ListCell<>() {
417 @Override
418 protected void updateItem( AppTheme item, boolean empty ) {
419 super.updateItem( item, empty );
420 setText( empty || item == null ? "" : item.getName() );
421 }
422 } );
423 themeComboBox.getSelectionModel().select( mainGui.getCurrentTheme() );
424
425 final VBox globalBox = new VBox( 10, globalTitle, globalAutostartCheck, new HBox( 8, themeLabel, themeComboBox ) );
426 grid.add( globalBox, 0, 3, 2, 1 );
427
428
429 final Button saveButton = new Button( "Einstellungen speichern", Gui.createIcon( MaterialDesignD.DISC ) );
430 saveButton.setStyle( "-fx-background-color: #2ecc71; -fx-text-fill: white; -fx-font-weight: bold; -fx-padding: 10px 24px;" );
431 saveButton.setTooltip( new Tooltip( "Übernimmt alle geänderten Zustandsparameter permanent in die JSON-Konfigurationsdatei" ) );
432 saveButton.setOnAction( _ -> {
433 PreferenceManager.getInstance().setGlobalAutoStart( globalAutostartCheck.isSelected() );
434 PreferenceManager.getInstance().setTheme( themeComboBox.getValue() );
435 final Preference jobPref = job.getPreference();
436 final ScanType scanType = ScanType.get( taskModeComboBox.getValue() );
437 jobPref.setSubDir( subDirCheck.isSelected() );
438 jobPref.setTrashbin( trashbinCheck.isSelected() );
439 jobPref.setAutoDel( autoDelCheck.isSelected() );
440 jobPref.setLogOn( logOnCheck.isSelected() );
441 jobPref.setBgSync( bgSyncCheck.isSelected() );
442 jobPref.setBgTime( BgTime.get( bgTimeComboBox.getValue() ) );
443 jobPref.setAutoSync( autoSyncCheck.isSelected() );
444 jobPref.setScanMode( scanType );
445
446 jobPref.setSourcePath( new ArrayList<>( pathCtx.sources ) );
447 jobPref.setDestPath( new ArrayList<>( pathCtx.destinations ) );
448
449 if( scanType == ScanType.DUBLICATE_SCAN && !pathCtx.sources.isEmpty() ) {
450 jobPref.setStartSourcePath( pathCtx.sources.get( 0 ) );
451 jobPref.setStartDestPath( Paths.get( "" ) );
452 }else if( !pathCtx.sources.isEmpty() && !pathCtx.destinations.isEmpty() ) {
453 jobPref.setStartSourcePath( pathCtx.sources.get( 0 ) );
454 jobPref.setStartDestPath( pathCtx.destinations.get( 0 ) );
455 }else {
456 displayTemporaryStatus( "Fehlender Pfad! Einstellungen nicht gespeichert!", NotifyStatus.WARNING, Main.INFO_DELAY );
457 return;
458 }
459 controller.handleSaveSettings( job.getPreference(), themeComboBox.getValue() );
460 } );
461
462 final HBox buttonRow = new HBox( saveButton );
463 buttonRow.setAlignment( Pos.CENTER_RIGHT );
464 grid.add( buttonRow, 1, 4 );
465
466 return grid;
467 }
468
469
470
471
472 private void renderContextPaths( ScanType type, VBox container, Preference pref, PathContext pathCtx ) {
473 container.getChildren().clear();
474 final GridPane pathsGrid = new GridPane();
475 pathsGrid.setHgap( 12 );
476 pathsGrid.setVgap( 10 );
477
478 final Label title = new Label( "Verzeichnis-Konfiguration (" + type.getDescription() + ")" );
479 title.setStyle( "-fx-font-size: 14px; -fx-font-weight: bold;" );
480 container.getChildren().add( title );
481
482 if( ScanType.SYNCHRONIZE.equals( type ) || ScanType.DUBLICATE_SCAN.equals( type ) ) {
483 final Path initialSrc = ( pref.getSourcePath() != null && !pref.getSourcePath().isEmpty() ) ? pref.getSourcePath().get( 0 ) : null;
484 final TextField srcField = new TextField( initialSrc.toString() );
485 srcField.setPrefWidth( 400 );
486 final Button srcBtn = new Button( "Durchsuchen..." );
487 srcBtn.setOnAction( _ -> {
488 final File f = chooseDirectory( initialSrc.toFile(), "Quellverzeichnis für " + pref.getScanMode().getDescription() );
489 if( f != null ) {
490 srcField.setText( f.getAbsolutePath() );
491 pathCtx.sources.clear();
492 pathCtx.sources.add( Paths.get( f.getAbsolutePath() ) );
493 }
494 } );
495 pathsGrid.add( new Label( ScanType.DUBLICATE_SCAN.equals( type ) ? "Scanverzeichnis:" : "Quellverzeichnis:" ), 0, 0 );
496 pathsGrid.add( new HBox( 8, srcField, srcBtn ), 1, 0 );
497 }
498
499 if( ScanType.SYNCHRONIZE.equals( type ) ) {
500 final Path initialDest = ( pref.getDestPath() != null && !pref.getDestPath().isEmpty() ) ? pref.getDestPath().get( 0 ) : null;
501 final TextField destField = new TextField( initialDest.toString() );
502 destField.setPrefWidth( 400 );
503 final Button destBtn = new Button( "Durchsuchen..." );
504 destBtn.setOnAction( _ -> {
505 final File f = chooseDirectory( initialDest.toFile(), "Zielverzeichnis für " + pref.getScanMode().getDescription() );
506 if( f != null ) {
507 destField.setText( f.getAbsolutePath() );
508 pathCtx.destinations.clear();
509 pathCtx.destinations.add( Paths.get( f.getAbsolutePath() ) );
510 }
511 } );
512 pathsGrid.add( new Label( "Zielverzeichnis:" ), 0, 1 );
513 pathsGrid.add( new HBox( 8, destField, destBtn ), 1, 1 );
514 }
515
516 if( ScanType.FLAT_SCAN.equals( type ) || ScanType.DEEP_SCAN.equals( type ) ) {
517 final VBox multiSrcBox = new VBox( 6 );
518 final Label multiLabel = new Label( "Quellverzeichnisse (Multi-Source Pathing):" );
519 multiLabel.setStyle( "-fx-font-weight: bold;" );
520
521 final ObservableList<String> backupPaths = FXCollections.observableArrayList();
522 for( final Path p : pathCtx.sources ) {
523 backupPaths.add( p.toString() );
524 }
525
526 final ListView<String> pathsListView = new ListView<>( backupPaths );
527 pathsListView.setPrefHeight( 100 );
528 final Button add = new Button( "Verzeichnis hinzufügen", Gui.createIcon( MaterialDesignP.PLUS ) );
529 add.setOnAction( _ -> {
530 final File f = chooseDirectory( pathCtx.sources.getLast().toFile(), "Quellverzeichnis für " + pref.getScanMode().getDescription() );
531 if( f != null && !backupPaths.contains( f.getAbsolutePath() ) ) {
532 backupPaths.add( f.getAbsolutePath() );
533 pathCtx.sources.add( Paths.get( f.getAbsolutePath() ) );
534 }
535 } );
536 final Button rem = new Button( "Entfernen", Gui.createIcon( MaterialDesignD.DELETE ) );
537 rem.setOnAction( _ -> {
538 final String sel = pathsListView.getSelectionModel().getSelectedItem();
539 if( sel != null ) {
540 backupPaths.remove( sel );
541 pathCtx.sources.remove( Paths.get( sel ) );
542 }
543 } );
544
545 multiSrcBox.getChildren().addAll( multiLabel, pathsListView, new HBox( 8, add, rem ) );
546 pathsGrid.add( multiSrcBox, 0, 0, 2, 1 );
547
548 final GridPane destGrid = new GridPane();
549 destGrid.setHgap( 12 );
550 final String initialDest = ( pref.getDestPath() != null && !pref.getDestPath().isEmpty() ) ? pref.getDestPath().get( 0 ).toString() : "";
551 final TextField destField = new TextField( initialDest );
552 destField.setPrefWidth( 400 );
553 final Button destBtn = new Button( "Durchsuchen..." );
554 destBtn.setOnAction( _ -> {
555 final File f = chooseDirectory( pathCtx.destinations.getFirst().toFile(), "Ziielverzeichnis für " + pref.getScanMode().getDescription() );
556 if( f != null ) {
557 destField.setText( f.getAbsolutePath() );
558 pathCtx.destinations.clear();
559 pathCtx.destinations.add( Paths.get( f.getAbsolutePath() ) );
560 }
561 } );
562 destGrid.add( new Label( "Zielverzeichnis:" ), 0, 0 );
563 destGrid.add( new HBox( 8, destField, destBtn ), 1, 0 );
564 pathsGrid.add( new VBox( 10, new Separator(), destGrid ), 0, 1, 2, 1 );
565 }
566
567 container.getChildren().add( pathsGrid );
568 }
569
570 private File chooseDirectory( File initialDir, String title ) {
571 final DirectoryChooser chooser = new DirectoryChooser();
572 if( initialDir != null && initialDir.exists() ) chooser.setInitialDirectory( initialDir );
573 chooser.setTitle( title );
574 return chooser.showDialog( mainGui.getWindowStage() );
575 }
576
577
578
579
580
581 private static final class PathContext {
582 private final ArrayList<Path> sources = new ArrayList<>();
583 private final ArrayList<Path> destinations = new ArrayList<>();
584 }
585 }