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.util.Arrays;
24 import java.util.List;
25
26 import javafx.animation.KeyFrame;
27 import javafx.animation.Timeline;
28 import javafx.beans.binding.Bindings;
29 import javafx.geometry.Insets;
30 import javafx.geometry.Pos;
31 import javafx.scene.Node;
32 import javafx.scene.control.Button;
33 import javafx.scene.control.Label;
34 import javafx.scene.control.ProgressBar;
35 import javafx.scene.control.ProgressIndicator;
36 import javafx.scene.control.ScrollPane;
37 import javafx.scene.control.TableColumn;
38 import javafx.scene.control.TableView;
39 import javafx.scene.control.TextArea;
40 import javafx.scene.control.Tooltip;
41 import javafx.scene.control.cell.CheckBoxTableCell;
42 import javafx.scene.layout.HBox;
43 import javafx.scene.layout.Priority;
44 import javafx.scene.layout.StackPane;
45 import javafx.scene.layout.VBox;
46 import javafx.util.Duration;
47
48 import org.kordamp.ikonli.materialdesign2.MaterialDesignD;
49 import org.kordamp.ikonli.materialdesign2.MaterialDesignP;
50 import org.kordamp.ikonli.materialdesign2.MaterialDesignS;
51
52 import de.spiritscorp.datasync.ScanType;
53 import de.spiritscorp.datasync.controller.SyncJobContext;
54 import de.spiritscorp.datasync.controller.ViewController;
55 import de.spiritscorp.datasync.io.Preference;
56
57
58
59
60
61
62
63 final class WorkspaceView extends VBox {
64
65
66 private final Gui mainGui;
67
68 private final ViewController controller;
69
70 private final SettingsGrid settingsGrid;
71
72 private final Label wrkspcHeaderLabel;
73
74 private final Label contextInfoLabel;
75
76 private final HBox controlToolbar;
77
78 private final StackPane centerViewport;
79
80
81 private final ScrollPane consoleViewNode;
82
83 private final TextArea consoleTextArea;
84
85 private final VBox duplicateViewNode;
86
87 private TableView<SyncJobContext.FileRow> duplicateTable;
88
89
90 private final Button actionButton;
91
92 private final Button cancelButton;
93
94 private Button deleteButton;
95
96 private final ProgressBar progressBar;
97
98 private final Label statusLabel;
99
100
101
102
103
104
105
106 WorkspaceView( final Gui mainGui, final ViewController controller ) {
107 this(
108 mainGui,
109 controller,
110 new SettingsGrid( controller, mainGui.getWindowStage(), new ContextPathRenderer() ) );
111 }
112
113
114
115
116
117
118
119
120
121
122 WorkspaceView( final Gui mainGui, final ViewController controller, final SettingsGrid settingsGrid ) {
123 super();
124 this.mainGui = mainGui;
125 this.controller = controller;
126 this.settingsGrid = settingsGrid;
127 this.setPadding( new Insets( 24 ) );
128 this.setSpacing( 12 );
129
130 wrkspcHeaderLabel = new Label( "Kein Task aktiv" );
131 wrkspcHeaderLabel.getStyleClass().addAll( "workspace-header-label" );
132
133
134 contextInfoLabel = new Label( "" );
135 contextInfoLabel.getStyleClass().addAll( "context-info-label" );
136
137 controlToolbar = new HBox( 12 );
138 controlToolbar.setAlignment( Pos.CENTER_LEFT );
139
140 actionButton = new Button( "Ausführen", Gui.createIcon( MaterialDesignP.PLAY ) );
141 actionButton.getGraphic().getStyleClass().addAll( Gui.CSS_BUTTON_ICON );
142 actionButton.getStyleClass().addAll( "action-button" );
143 actionButton.setTooltip( new Tooltip( "Starte Job" ) );
144 cancelButton = new Button( "Abbrechen", Gui.createIcon( MaterialDesignS.STOP ) );
145 cancelButton.getGraphic().getStyleClass().addAll( Gui.CSS_BUTTON_ICON );
146 cancelButton.getStyleClass().addAll( "cancel-button" );
147 cancelButton.setTooltip( new Tooltip( "Stoppe Job" ) );
148
149 controlToolbar.getChildren().addAll( actionButton, cancelButton );
150
151 consoleTextArea = new TextArea();
152 consoleTextArea.setEditable( false );
153 consoleTextArea.getStyleClass().addAll( "console-text-area" );
154 consoleViewNode = new ScrollPane( consoleTextArea );
155 consoleViewNode.setFitToWidth( true );
156 consoleViewNode.setFitToHeight( true );
157
158 duplicateViewNode = assembleDuplicateTableView();
159 centerViewport = new StackPane( consoleViewNode );
160
161 final HBox statusFooter = new HBox( 12 );
162 statusFooter.setAlignment( Pos.CENTER_LEFT );
163 progressBar = new ProgressBar( 0 );
164 progressBar.setTooltip( new Tooltip( "Aktueller Fortschritt" ) );
165 statusLabel = new Label( "Bereit" );
166 statusLabel.setTooltip( new Tooltip( "Aktueller Status" ) );
167 statusFooter.getChildren().addAll( progressBar, statusLabel );
168
169 this.getChildren().addAll( wrkspcHeaderLabel, contextInfoLabel, controlToolbar, centerViewport, statusFooter );
170 setVgrow( centerViewport, Priority.ALWAYS );
171 }
172
173
174
175
176 private VBox assembleDuplicateTableView() {
177 duplicateTable = new TableView<>();
178 duplicateTable.setEditable( true );
179 duplicateTable.setFixedCellSize( 24.0 );
180
181 final TableColumn<SyncJobContext.FileRow, Boolean> selCol = new TableColumn<>( "Auswahl" );
182 selCol.setCellValueFactory( d -> d.getValue().selectedProperty() );
183 selCol.setCellFactory( CheckBoxTableCell.forTableColumn( selCol ) );
184 selCol.setPrefWidth( 100 );
185
186 final TableColumn<SyncJobContext.FileRow, String> nameCol = new TableColumn<>( "Dateiname" );
187 nameCol.setCellValueFactory( d -> d.getValue().fileNameProperty() );
188 nameCol.setPrefWidth( 250 );
189
190 final TableColumn<SyncJobContext.FileRow, String> sizeCol = new TableColumn<>( "Größe" );
191 sizeCol.setCellValueFactory( d -> d.getValue().sizeProperty() );
192 sizeCol.setPrefWidth( 250 );
193
194 final TableColumn<SyncJobContext.FileRow, String> pathCol = new TableColumn<>( "Pfad" );
195 pathCol.setCellValueFactory( d -> d.getValue().pathProperty() );
196 pathCol.setPrefWidth( 600 );
197
198 final TableColumn<SyncJobContext.FileRow, String> hashCol = new TableColumn<>( "Hash" );
199 hashCol.setCellValueFactory( d -> d.getValue().hashProperty() );
200 hashCol.setPrefWidth( 250 );
201
202 duplicateTable.getColumns().addAll( List.of( selCol, nameCol, sizeCol, hashCol, pathCol ) );
203 deleteButton = new Button( "Duplikate löschen", Gui.createIcon( MaterialDesignD.DELETE ) );
204 deleteButton.getGraphic().getStyleClass().addAll( Gui.CSS_BUTTON_ICON );
205 deleteButton.getStyleClass().addAll( "delete-button" );
206 deleteButton.setTooltip( new Tooltip( "Ausgewählte Dateien werden gelöscht" ) );
207
208 final VBox frame = new VBox( 8, duplicateTable, deleteButton );
209 setVgrow( duplicateTable, Priority.ALWAYS );
210 return frame;
211 }
212
213
214
215
216
217
218
219 void refreshView( final Gui.ViewState state, final SyncJobContext job ) {
220
221 centerViewport.getChildren().clear();
222 contextInfoLabel.setTooltip( null );
223
224 if( state == Gui.ViewState.INFO ) {
225 wrkspcHeaderLabel.setText( "About" );
226 contextInfoLabel.setText( "Backup Software" );
227 controlToolbar.setVisible( false );
228 return;
229 }else if( job == null ) { return; }
230
231 final String finalTip;
232 if( state == Gui.ViewState.MONITOR ) {
233 wrkspcHeaderLabel.setText( "Task-Monitor: " + job.getJobName() );
234 controlToolbar.setVisible( true );
235
236
237 final Preference pref = job.getPreference();
238 final String src = pref.getSourcePaths() != null ? Arrays.toString( pref.getSourcePaths().toArray() ) : "Keine Quelle";
239 final String dest = pref.getDestPaths() != null && !pref.getDestPaths().isEmpty() ? pref.getDestPaths().toString() : "Kein Ziel";
240 final String srcTip = src.replace( '[', ' ' ).replace( ']', ' ' ).replace( ',', '\n' );
241
242 if( ScanType.DUBLICATE_SCAN == job.getSelectedMode() ) {
243 finalTip = String.format( """
244 Quelle:
245 %s
246 """, srcTip );
247 contextInfoLabel.setText( String.format( "Modus: %s | Verzeichnisse: %s", job.getSelectedMode().getDescription(), src ) );
248 centerViewport.getChildren().add( duplicateViewNode );
249 }else {
250 finalTip = String.format( """
251 Quelle:
252 %s
253 Ziel:
254 %s
255 """, srcTip, dest.replace( '[', ' ' ).replace( ']', ' ' ) );
256 contextInfoLabel.setText( String.format( "Modus: %s | Quelle: %s | Ziel: %s", job.getSelectedMode().getDescription(), src, dest ) );
257 centerViewport.getChildren().add( consoleViewNode );
258 }
259 contextInfoLabel.setTooltip( new Tooltip( finalTip ) );
260 }else if( state == Gui.ViewState.SETTINGS ) {
261 wrkspcHeaderLabel.setText( "Einstellungen für: " + job.getJobName() );
262 contextInfoLabel.setText( "Konfiguration der task-spezifischen Ablaufparameter, Dateiattribute und Verzeichnisstrukturen." );
263 controlToolbar.setVisible( false );
264 displayCustomViewNode( settingsGrid.buildSettingsGridTab( this, job, mainGui.getAvailableThemes() ) );
265 }
266 }
267
268
269
270
271
272
273 void displayCustomViewNode( final Node content ) {
274 centerViewport.getChildren().clear();
275 final ScrollPane scroll = new ScrollPane( content );
276 scroll.setFitToWidth( true );
277 scroll.setPadding( new Insets( 12 ) );
278 centerViewport.getChildren().add( scroll );
279 }
280
281
282
283
284
285
286 void bindJob( final SyncJobContext job ) {
287 statusLabel.textProperty().unbind();
288 consoleTextArea.textProperty().unbind();
289 statusLabel.textProperty().bind( job.statusMessageProperty() );
290 consoleTextArea.textProperty().bind( job.logOutputProperty() );
291 duplicateTable.setItems( job.getDuplicateFiles() );
292
293 progressBar.progressProperty().unbind();
294 progressBar.progressProperty().bind( Bindings.when( job.runningProperty() ).then( ProgressIndicator.INDETERMINATE_PROGRESS ).otherwise( 0.0 ) );
295
296 cancelButton.disableProperty().unbind();
297 actionButton.disableProperty().unbind();
298 cancelButton.disableProperty().bind( job.runningProperty().not() );
299 actionButton.disableProperty().bind( job.runningProperty() );
300
301 cancelButton.setOnAction( _ -> controller.handleStopTask( job ) );
302 actionButton.setOnAction( _ -> controller.handleExecuteTask( job ) );
303 deleteButton.setOnAction( _ -> controller.deleteSelectedDuplicates( job ) );
304 }
305
306
307
308
309
310
311
312
313
314
315 void displayTemporaryStatus( final String message, final NotifyStatus cssNotifyStatus, final int durationSec ) {
316 final String origContextText = contextInfoLabel.getText();
317 final String originalStyle = contextInfoLabel.getStyle();
318
319 contextInfoLabel.getStyleClass().removeAll( "status-success", "status-error", "status-warning" );
320
321
322
323
324 switch( cssNotifyStatus ) {
325 case SUCCESS -> {
326 contextInfoLabel.setStyle( "-fx-text-fill: #22aa22; -fx-font-weight: bold;" );
327 contextInfoLabel.setText( "✔ " + message );
328 }
329 case ERROR -> {
330 contextInfoLabel.setStyle( "-fx-text-fill: #ff3333; -fx-font-weight: bold;" );
331 contextInfoLabel.setText( "❌ " + message );
332 }
333 case WARNING -> {
334 contextInfoLabel.setStyle( "-fx-text-fill: #ffaa00; -fx-font-weight: bold;" );
335 contextInfoLabel.setText( "⚠ " + message );
336 }
337 }
338
339
340
341
342 contextInfoLabel.getStyleClass().addFirst( cssNotifyStatus.getCssClass() );
343
344
345 final Timeline fallbackTimeline = new Timeline( new KeyFrame(
346 Duration.seconds( durationSec ),
347 _ -> {
348 contextInfoLabel.setText( origContextText );
349 contextInfoLabel.getStyleClass().remove( cssNotifyStatus.getCssClass() );
350 contextInfoLabel.setStyle( originalStyle );
351 } ) );
352
353 fallbackTimeline.setCycleCount( 1 );
354 fallbackTimeline.play();
355 }
356 }