1 package de.spiritscorp.datasync.controller;
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.Map;
24
25 import javafx.application.Platform;
26 import javafx.collections.FXCollections;
27 import javafx.collections.ObservableList;
28 import javafx.scene.control.Alert;
29 import javafx.scene.control.ButtonType;
30 import javafx.scene.control.ListCell;
31 import javafx.scene.control.TextInputDialog;
32
33 import de.spiritscorp.datasync.Main;
34 import de.spiritscorp.datasync.gui.DialogService;
35 import de.spiritscorp.datasync.gui.Gui;
36 import de.spiritscorp.datasync.gui.WorkspaceView.NotifyStatus;
37 import de.spiritscorp.datasync.io.Debug;
38 import de.spiritscorp.datasync.io.Logger;
39 import de.spiritscorp.datasync.io.Preference;
40 import de.spiritscorp.datasync.io.PreferenceManager;
41 import de.spiritscorp.datasync.theme.AppTheme;
42
43
44
45
46
47 public class MainViewController implements ViewController {
48
49 private final Gui gui;
50 private final SyncJobService helper;
51 private final PreferenceManager manager;
52 private BgController activeBgController;
53
54
55
56
57
58
59 public MainViewController( final Gui gui ) {
60 this(
61 gui,
62 new SyncJobService( new DialogService( gui.getWindowStage() ), new UiLogFormatter() ),
63 PreferenceManager.getInstance() );
64 loadInitialJobList();
65 }
66
67 MainViewController( final Gui gui, final SyncJobService helper, final PreferenceManager manager ) {
68 this.gui = gui;
69 this.helper = helper;
70 this.manager = manager;
71 }
72
73 @Override
74 public void registerNativeShutdownHook() {
75 Runtime.getRuntime().addShutdownHook( new Thread( () -> {
76
77 Debug.printDebug( "[Exit] Host operating system shutdown signal intercepted via native runtime hook." );
78
79 executeCoreShutdownSequence( true );
80 Debug.printDebug( "[Exit] BYE, BYE" );
81 }, "DataSync-OS-Shutdown-Hook-Thread" ) );
82 Debug.printDebug( "[Info] Native OS runtime shutdown hook successfully registered." );
83 }
84
85 @Override
86 public void handleNavigate( final Gui.ViewState state ) {
87 gui.setViewState( state );
88 if( !gui.getWindowStage().isShowing() ) {
89 gui.getWindowStage().show();
90 gui.getWindowStage().toFront();
91 }
92 }
93
94 @Override
95 public void handleApplicationShutdown() {
96 final Alert confirmation = new Alert( Alert.AlertType.CONFIRMATION, "Hintergrunddienste werden beendet.", ButtonType.OK, ButtonType.CANCEL );
97 confirmation.setTitle( "Programm beenden" );
98 confirmation.setHeaderText( "Möchten Sie DataSync wirklich schließen?" );
99 confirmation.setContentText( "Aktive Hintergrunddienste werden wenn möglich sauber beendet." );
100 confirmation.initOwner( gui.getWindowStage() );
101
102 if( confirmation.showAndWait().orElse( ButtonType.CANCEL ) == ButtonType.OK ) {
103 Debug.printDebug( "[Exit] Complete system teardown triggered manually via user confirmation." );
104
105
106 Platform.exit();
107
108 Debug.printDebug( "[Exit] Manual graceful teardown completed. Evicting core JVM runtime context loop." );
109 System.exit( 0 );
110 }
111 }
112
113
114 private void loadInitialJobList() {
115 final ObservableList<SyncJobContext> jobList = FXCollections.observableArrayList();
116
117 if( manager.loadAllPreferences() ) {
118 for( final Map.Entry<String, Preference> entry : manager.getLoadedProfiles().entrySet() ) {
119 final SyncJobContext ctx = new SyncJobContext( entry.getKey(), entry.getValue() );
120 ctx.setSelectedMode( entry.getValue().getScanMode().getDescription() );
121 jobList.add( ctx );
122 }
123 }else {
124 jobList.add( new SyncJobContext( "NAS Dokumenten-Spiegel", manager.createProfile( "NAS Dokus" ) ) );
125 jobList.add( new SyncJobContext( "Lokales Code-Workspace Backup", manager.createProfile( "WorkspaceRepo" ) ) );
126 }
127 gui.setInitialJobConfigurations( jobList );
128 }
129
130 @Override
131 public void handleCreateNewJob() {
132 final String name = "Sync Job " + ( gui.getJobList().size() + 1 );
133 gui.getJobList().add( new SyncJobContext( name, manager.createProfile( name ) ) );
134 }
135
136 @Override
137 public void handleRenameJob( final ListCell<SyncJobContext> cell ) {
138 final SyncJobContext selectedJob = cell.getItem();
139 if( selectedJob != null ) {
140 final String oldName = selectedJob.getJobName();
141 final TextInputDialog dialog = new TextInputDialog( selectedJob.getJobName() );
142 dialog.setTitle( "Task umbenennen" );
143 dialog.setHeaderText( "Geben Sie einen neuen Namen für den Task ein:" );
144 dialog.setContentText( "Name:" );
145 dialog.initOwner( gui.getWindowStage() );
146 dialog.showAndWait().ifPresent( newName -> {
147 final String trimmedName = newName.trim();
148 if( !trimmedName.isEmpty() && !trimmedName.equals( oldName ) ) {
149 selectedJob.setJobName( trimmedName );
150 gui.showStatusNotification( oldName + " wurde ersetzt und gespeichert durch" + newName, NotifyStatus.SUCESS, Main.INFO_DELAY );
151 }else {
152 gui.showStatusNotification( oldName + " wurde nicht ersetzt", NotifyStatus.WARNING, Main.INFO_DELAY );
153 }
154 } );
155 }
156 }
157
158 @Override
159 public void handleDuplicateJob( final SyncJobContext job ) {
160 if( job != null ) {
161 gui.getJobList().add( new SyncJobContext( job.getJobName() + " (Kopie)", job.getPreference() ) );
162 }
163 }
164
165 @Override
166 public void handleDeleteJob( final SyncJobContext job ) {
167 if( job != null ) {
168 final Alert alert = new Alert( Alert.AlertType.CONFIRMATION, "Task '" + job.getJobName() + "' wirklich unwiderruflich löschen?", ButtonType.YES, ButtonType.NO );
169 alert.setTitle( "Task entfernen" );
170 alert.setHeaderText( null );
171 alert.initOwner( gui.getWindowStage() );
172 alert.showAndWait().ifPresent( response -> {
173 if( response == ButtonType.YES ) {
174 gui.getJobList().remove( job );
175 PreferenceManager.getInstance().removeProfile( job );
176 gui.showStatusNotification( job.getJobName() + " wurde erfolgreich gelöscht", NotifyStatus.SUCESS, Main.INFO_DELAY );
177 }else {
178 gui.showStatusNotification( job.getJobName() + " wurde nicht gelöscht", NotifyStatus.WARNING, Main.INFO_DELAY );
179 }
180 } );
181 }
182 }
183
184 @Override
185 public void handleExecuteTask( final SyncJobContext job ) {
186 if( job != null ) {
187 switch( job.getSelectedMode() ) {
188 case SYNCHRONIZE -> helper.startSynchronize( job );
189 case DUBLICATE_SCAN -> helper.startDuplicateScan( job );
190 case DEEP_SCAN, FLAT_SCAN -> helper.startBackup( job );
191 default -> throw new IllegalArgumentException( "Unexpected value: " + job.getSelectedMode() );
192 }
193 }
194 }
195
196 @Override
197 public void handleStopTask( final SyncJobContext job ) {
198 job.cancelRunningTask( Main.EXIT_THREAD_TIMEOUT );
199 }
200
201 @Override
202 public void handleSaveSettings( final Preference localPreferences, final AppTheme targetTheme ) {
203 if( localPreferences == null ) return;
204
205
206 final boolean prefsSaved = PreferenceManager.getInstance().saveAllPreferences();
207 if( prefsSaved ) {
208 Debug.printDebug( "[Settings] Configuration profile assets successfully serialized to disk." );
209 }else {
210 Debug.printDebug( "[Settings] Critical: Failed to persist configuration profile assets." );
211 }
212
213
214 final boolean autostartTargetState = PreferenceManager.getInstance().isGlobalAutoStart();
215 final boolean autostartSaved = helper.setOSAutostart( autostartTargetState );
216 if( autostartSaved ) {
217 Debug.printDebug( "[Settings] OS desktop autostart hooks successfully synchronized to state: " + autostartTargetState );
218 }else {
219 Debug.printDebug( "[Settings] Warning: Failed to apply host operating system autostart modifications." );
220 }
221
222 gui.changeTheme( targetTheme );
223 gui.setViewState( Gui.ViewState.SETTINGS );
224
225
226 if( prefsSaved && autostartSaved ) {
227 gui.showStatusNotification( "Settings successfully persisted to the configuration registry.", NotifyStatus.SUCESS, Main.INFO_DELAY );
228 }else if( !prefsSaved ) {
229 gui.showStatusNotification( "Error: Failed to write configuration payload to 'conf.json'.", NotifyStatus.ERROR, Main.INFO_DELAY );
230 }else {
231 gui.showStatusNotification( "Warning: Profiles saved, but host operating system autostart configuration failed.", NotifyStatus.WARNING, Main.INFO_DELAY );
232 }
233 }
234
235 @Override
236 public void runInBackground( final boolean firstStart ) {
237 activeBgController = new BgController( gui, this, gui.getJobList(), new Logger() );
238 activeBgController.startBgJob( firstStart );
239 }
240
241 @Override
242 public void deleteSelectedDuplicates( final SyncJobContext jobContext ) {
243 helper.deleteSelectedDuplicates( jobContext );
244 }
245
246
247
248
249
250
251
252
253 private void executeCoreShutdownSequence( final boolean dynamicGracePeriod ) {
254 final long timeoutPerThreadMs = dynamicGracePeriod ? Main.BACKGROUND_THREAD_TIMEOUT : Main.EXIT_THREAD_TIMEOUT;
255 Debug.printDebug( "[Exit] Internal system teardown invoked. Dynamic grace mode: %b -> %d ms", dynamicGracePeriod, timeoutPerThreadMs );
256
257 for( final SyncJobContext job : gui.getJobList() ) {
258 job.cancelRunningTask( timeoutPerThreadMs );
259 }
260 if( !gui.isShowing() && activeBgController != null ) activeBgController.interruptBgJob( timeoutPerThreadMs );
261 Debug.printDebug( "[Exit] Core teardown protocol finalized. Flushing runtime buffers." );
262 }
263 }