diff --git a/src/main/java/org/cryptomator/common/settings/Settings.java b/src/main/java/org/cryptomator/common/settings/Settings.java index 36d4d16aa..7e383bc18 100644 --- a/src/main/java/org/cryptomator/common/settings/Settings.java +++ b/src/main/java/org/cryptomator/common/settings/Settings.java @@ -32,6 +32,7 @@ public class Settings { public static final boolean DEFAULT_ASKED_FOR_UPDATE_CHECK = false; public static final boolean DEFAULT_CHECK_FOR_UPDATES = false; public static final boolean DEFAULT_START_HIDDEN = false; + public static final boolean DEFAULT_AUTO_CLOSE_VAULTS = false; public static final int DEFAULT_PORT = 42427; public static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3; public static final WebDavUrlScheme DEFAULT_GVFS_SCHEME = WebDavUrlScheme.DAV; @@ -51,6 +52,7 @@ public class Settings { private final BooleanProperty askedForUpdateCheck = new SimpleBooleanProperty(DEFAULT_ASKED_FOR_UPDATE_CHECK); private final BooleanProperty checkForUpdates = new SimpleBooleanProperty(DEFAULT_CHECK_FOR_UPDATES); private final BooleanProperty startHidden = new SimpleBooleanProperty(DEFAULT_START_HIDDEN); + private final BooleanProperty autoCloseVaults = new SimpleBooleanProperty(DEFAULT_AUTO_CLOSE_VAULTS); private final IntegerProperty port = new SimpleIntegerProperty(DEFAULT_PORT); private final IntegerProperty numTrayNotifications = new SimpleIntegerProperty(DEFAULT_NUM_TRAY_NOTIFICATIONS); private final ObjectProperty preferredGvfsScheme = new SimpleObjectProperty<>(DEFAULT_GVFS_SCHEME); @@ -82,6 +84,7 @@ public class Settings { askedForUpdateCheck.addListener(this::somethingChanged); checkForUpdates.addListener(this::somethingChanged); startHidden.addListener(this::somethingChanged); + autoCloseVaults.addListener(this::somethingChanged); port.addListener(this::somethingChanged); numTrayNotifications.addListener(this::somethingChanged); preferredGvfsScheme.addListener(this::somethingChanged); @@ -133,6 +136,10 @@ public class Settings { return startHidden; } + public BooleanProperty autoCloseVaults() { + return autoCloseVaults; + } + public IntegerProperty port() { return port; } diff --git a/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java b/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java index d10066f8f..3f4614604 100644 --- a/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java +++ b/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java @@ -41,6 +41,7 @@ public class SettingsJsonAdapter extends TypeAdapter { out.name("askedForUpdateCheck").value(value.askedForUpdateCheck().get()); out.name("checkForUpdatesEnabled").value(value.checkForUpdates().get()); out.name("startHidden").value(value.startHidden().get()); + out.name("autoCloseVaults").value(value.autoCloseVaults().get()); out.name("port").value(value.port().get()); out.name("numTrayNotifications").value(value.numTrayNotifications().get()); out.name("preferredGvfsScheme").value(value.preferredGvfsScheme().get().name()); @@ -82,6 +83,7 @@ public class SettingsJsonAdapter extends TypeAdapter { case "askedForUpdateCheck" -> settings.askedForUpdateCheck().set(in.nextBoolean()); case "checkForUpdatesEnabled" -> settings.checkForUpdates().set(in.nextBoolean()); case "startHidden" -> settings.startHidden().set(in.nextBoolean()); + case "autoCloseVaults" -> settings.autoCloseVaults().set(in.nextBoolean()); case "port" -> settings.port().set(in.nextInt()); case "numTrayNotifications" -> settings.numTrayNotifications().set(in.nextInt()); case "preferredGvfsScheme" -> settings.preferredGvfsScheme().set(parseWebDavUrlSchemePrefix(in.nextString())); diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java index bc952f9d1..e1912aad1 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -23,6 +23,7 @@ public enum FxmlFile { MIGRATION_SUCCESS("/fxml/migration_success.fxml"), // PREFERENCES("/fxml/preferences.fxml"), // QUIT("/fxml/quit.fxml"), // + QUIT_FORCED("/fxml/quit_forced.fxml"), // RECOVERYKEY_CREATE("/fxml/recoverykey_create.fxml"), // RECOVERYKEY_RECOVER("/fxml/recoverykey_recover.fxml"), // RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), // diff --git a/src/main/java/org/cryptomator/ui/common/VaultService.java b/src/main/java/org/cryptomator/ui/common/VaultService.java index a6486f35f..8486a09c0 100644 --- a/src/main/java/org/cryptomator/ui/common/VaultService.java +++ b/src/main/java/org/cryptomator/ui/common/VaultService.java @@ -86,7 +86,8 @@ public class VaultService { } /** - * Creates but doesn't start a lock-all task. + * Creates a lock-all task. + * This task itself is _not started_, but its subtasks locking each vault will be already executed. * * @param vaults The list of vaults to be locked * @param forced Whether to attempt a forced lock diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java index 85e46dffa..cdeb764be 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java @@ -14,6 +14,7 @@ import org.cryptomator.ui.lock.LockComponent; import org.cryptomator.ui.mainwindow.MainWindowComponent; import org.cryptomator.ui.preferences.PreferencesComponent; import org.cryptomator.ui.quit.QuitComponent; + import org.cryptomator.ui.traymenu.TrayMenuComponent; import org.cryptomator.ui.unlock.UnlockComponent; @@ -57,4 +58,4 @@ abstract class FxApplicationModule { static QuitComponent provideQuitComponent(QuitComponent.Builder builder) { return builder.build(); } -} +} \ No newline at end of file diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationTerminator.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationTerminator.java index 7c7b07c1e..569a3674a 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationTerminator.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationTerminator.java @@ -2,10 +2,12 @@ package org.cryptomator.ui.fxapp; import com.google.common.base.Preconditions; import org.cryptomator.common.ShutdownHook; +import org.cryptomator.common.settings.Settings; import org.cryptomator.common.vaults.LockNotCompletedException; import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultState; import org.cryptomator.common.vaults.Volume; +import org.cryptomator.ui.common.VaultService; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,18 +29,24 @@ import static org.cryptomator.common.vaults.VaultState.Value.*; public class FxApplicationTerminator { private static final Set STATES_ALLOWING_TERMINATION = EnumSet.of(LOCKED, NEEDS_MIGRATION, MISSING, ERROR); + private static final Set STATES_PREVENT_TERMINATION = EnumSet.of(PROCESSING); private static final Logger LOG = LoggerFactory.getLogger(FxApplicationTerminator.class); private final ObservableList vaults; private final ShutdownHook shutdownHook; private final FxApplicationWindows appWindows; private final AtomicBoolean allowQuitWithoutPrompt = new AtomicBoolean(); + private final AtomicBoolean preventQuitWithGracefulLock = new AtomicBoolean(); + private final Settings settings; + private final VaultService vaultService; @Inject - public FxApplicationTerminator(ObservableList vaults, ShutdownHook shutdownHook, FxApplicationWindows appWindows) { + public FxApplicationTerminator(ObservableList vaults, ShutdownHook shutdownHook, FxApplicationWindows appWindows, Settings settings, VaultService vaultService) { this.vaults = vaults; this.shutdownHook = shutdownHook; this.appWindows = appWindows; + this.settings = settings; + this.vaultService = vaultService; } public void initialize() { @@ -72,6 +80,10 @@ public class FxApplicationTerminator { private void vaultListChanged(@SuppressWarnings("unused") Observable observable) { boolean allowSuddenTermination = vaults.stream().map(Vault::getState).allMatch(STATES_ALLOWING_TERMINATION::contains); boolean stateChanged = allowQuitWithoutPrompt.compareAndSet(!allowSuddenTermination, allowSuddenTermination); + + boolean preventGracefulTermination = vaults.stream().map(Vault::getState).anyMatch(STATES_PREVENT_TERMINATION::contains); + preventQuitWithGracefulLock.set(preventGracefulTermination); + Desktop desktop = Desktop.getDesktop(); if (stateChanged && desktop.isSupported(Desktop.Action.APP_SUDDEN_TERMINATION)) { if (allowSuddenTermination) { @@ -92,10 +104,22 @@ public class FxApplicationTerminator { */ private void handleQuitRequest(@SuppressWarnings("unused") @Nullable EventObject e, QuitResponse response) { var exitingResponse = new ExitingQuitResponse(response); + if (allowQuitWithoutPrompt.get()) { exitingResponse.performQuit(); + } else if (settings.autoCloseVaults().get() && !preventQuitWithGracefulLock.get()) { + var lockAllTask = vaultService.createLockAllTask(vaults.filtered(Vault::isUnlocked), false); + lockAllTask.setOnSucceeded(event -> { + LOG.info("Locked remaining vaults was succesful."); + exitingResponse.performQuit(); + }); + lockAllTask.setOnFailed(event -> { + LOG.warn("Unable to lock all vaults."); + appWindows.showQuitWindow(exitingResponse, true); + }); + lockAllTask.run(); } else { - appWindows.showQuitWindow(exitingResponse); + appWindows.showQuitWindow(exitingResponse, false); } } @@ -115,7 +139,7 @@ public class FxApplicationTerminator { /** * A dummy QuitResponse that ignores the response. - * + *

* To be used with {@link #handleQuitRequest(EventObject, QuitResponse)} if the invoking method is not interested in the response. */ private static class NoopQuitResponse implements QuitResponse { diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java index ba28f9bc4..5d38a9017 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java @@ -40,7 +40,7 @@ public class FxApplicationWindows { private final Optional trayIntegration; private final Lazy mainWindow; private final Lazy preferencesWindow; - private final Lazy quitWindow; + private final QuitComponent.Builder quitWindowBuilder; private final UnlockComponent.Factory unlockWorkflowFactory; private final LockComponent.Factory lockWorkflowFactory; private final ErrorComponent.Factory errorWindowFactory; @@ -48,12 +48,12 @@ public class FxApplicationWindows { private final FilteredList visibleWindows; @Inject - public FxApplicationWindows(@PrimaryStage Stage primaryStage, Optional trayIntegration, Lazy mainWindow, Lazy preferencesWindow, Lazy quitWindow, UnlockComponent.Factory unlockWorkflowFactory, LockComponent.Factory lockWorkflowFactory, ErrorComponent.Factory errorWindowFactory, ExecutorService executor) { + public FxApplicationWindows(@PrimaryStage Stage primaryStage, Optional trayIntegration, Lazy mainWindow, Lazy preferencesWindow, QuitComponent.Builder quitWindowBuilder, UnlockComponent.Factory unlockWorkflowFactory, LockComponent.Factory lockWorkflowFactory, ErrorComponent.Factory errorWindowFactory, ExecutorService executor) { this.primaryStage = primaryStage; this.trayIntegration = trayIntegration; this.mainWindow = mainWindow; this.preferencesWindow = preferencesWindow; - this.quitWindow = quitWindow; + this.quitWindowBuilder = quitWindowBuilder; this.unlockWorkflowFactory = unlockWorkflowFactory; this.lockWorkflowFactory = lockWorkflowFactory; this.errorWindowFactory = errorWindowFactory; @@ -104,8 +104,8 @@ public class FxApplicationWindows { return CompletableFuture.supplyAsync(() -> preferencesWindow.get().showPreferencesWindow(selectedTab), Platform::runLater).whenComplete(this::reportErrors); } - public CompletionStage showQuitWindow(QuitResponse response) { - return CompletableFuture.supplyAsync(() -> quitWindow.get().showQuitWindow(response), Platform::runLater).whenComplete(this::reportErrors); + public void showQuitWindow(QuitResponse response, boolean forced) { + CompletableFuture.runAsync(() -> quitWindowBuilder.build().showQuitWindow(response,forced), Platform::runLater); } public CompletionStage startUnlockWorkflow(Vault vault, @Nullable Stage owner) { diff --git a/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java index d33e919b6..ace6dadca 100644 --- a/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java +++ b/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java @@ -36,6 +36,7 @@ public class GeneralPreferencesController implements FxController { private final FxApplicationWindows appWindows; public ChoiceBox keychainBackendChoiceBox; public CheckBox startHiddenCheckbox; + public CheckBox autoCloseVaultsCheckbox; public CheckBox debugModeCheckbox; public CheckBox autoStartCheckbox; public ToggleGroup nodeOrientation; @@ -54,9 +55,8 @@ public class GeneralPreferencesController implements FxController { @FXML public void initialize() { startHiddenCheckbox.selectedProperty().bindBidirectional(settings.startHidden()); - + autoCloseVaultsCheckbox.selectedProperty().bindBidirectional(settings.autoCloseVaults()); debugModeCheckbox.selectedProperty().bindBidirectional(settings.debugMode()); - autoStartProvider.ifPresent(autoStart -> autoStartCheckbox.setSelected(autoStart.isEnabled())); var keychainSettingsConverter = new KeychainProviderClassNameConverter(keychainAccessProviders); diff --git a/src/main/java/org/cryptomator/ui/quit/QuitComponent.java b/src/main/java/org/cryptomator/ui/quit/QuitComponent.java index e100c52e9..801aece51 100644 --- a/src/main/java/org/cryptomator/ui/quit/QuitComponent.java +++ b/src/main/java/org/cryptomator/ui/quit/QuitComponent.java @@ -13,6 +13,7 @@ import org.cryptomator.ui.common.FxmlScene; import javafx.scene.Scene; import javafx.stage.Stage; import java.awt.desktop.QuitResponse; +import java.util.concurrent.atomic.AtomicReference; @QuitScoped @Subcomponent(modules = {QuitModule.class}) @@ -22,23 +23,28 @@ public interface QuitComponent { Stage window(); @FxmlScene(FxmlFile.QUIT) - Lazy scene(); + Lazy quitScene(); - QuitController controller(); + @FxmlScene(FxmlFile.QUIT_FORCED) + Lazy quitForcedScene(); - default Stage showQuitWindow(QuitResponse response) { - controller().updateQuitRequest(response); + @QuitWindow + AtomicReference quitResponse(); + + default void showQuitWindow(QuitResponse response, boolean forced) { Stage stage = window(); - stage.setScene(scene().get()); + quitResponse().set(response); + if(forced){ + stage.setScene(quitForcedScene().get()); + } else{ + stage.setScene(quitScene().get()); + } + stage.sizeToScene(); stage.show(); - stage.requestFocus(); - return stage; } @Subcomponent.Builder interface Builder { - QuitComponent build(); } - -} +} \ No newline at end of file diff --git a/src/main/java/org/cryptomator/ui/quit/QuitController.java b/src/main/java/org/cryptomator/ui/quit/QuitController.java index 207ae42c8..7a53f5b93 100644 --- a/src/main/java/org/cryptomator/ui/quit/QuitController.java +++ b/src/main/java/org/cryptomator/ui/quit/QuitController.java @@ -1,7 +1,10 @@ package org.cryptomator.ui.quit; +import dagger.Lazy; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.VaultService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,6 +13,7 @@ import javax.inject.Inject; import javafx.collections.ObservableList; import javafx.concurrent.Task; import javafx.fxml.FXML; +import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.ContentDisplay; import javafx.stage.Stage; @@ -29,27 +33,22 @@ public class QuitController implements FxController { private final ObservableList unlockedVaults; private final ExecutorService executorService; private final VaultService vaultService; - private final AtomicReference quitResponse = new AtomicReference<>(); - + private final AtomicReference quitResponse; + private final Lazy quitForcedScene; /* FXML */ public Button lockAndQuitButton; @Inject - QuitController(@QuitWindow Stage window, ObservableList vaults, ExecutorService executorService, VaultService vaultService) { + QuitController(@QuitWindow Stage window, ObservableList vaults, ExecutorService executorService, VaultService vaultService, @FxmlScene(FxmlFile.QUIT_FORCED) Lazy quitForcedScene, @QuitWindow AtomicReference quitResponse) { this.window = window; this.unlockedVaults = vaults.filtered(Vault::isUnlocked); this.executorService = executorService; this.vaultService = vaultService; + this.quitForcedScene = quitForcedScene; + this.quitResponse = quitResponse; window.setOnCloseRequest(windowEvent -> cancel()); } - public void updateQuitRequest(QuitResponse newResponse) { - var oldResponse = quitResponse.getAndSet(newResponse); - if (oldResponse != null) { - oldResponse.cancelQuit(); - } - } - private void respondToQuitRequest(Consumer action) { var response = quitResponse.getAndSet(null); if (response != null) { @@ -79,13 +78,8 @@ public class QuitController implements FxController { }); lockAllTask.setOnFailed(evt -> { LOG.warn("Locking failed", lockAllTask.getException()); - lockAndQuitButton.setDisable(false); - lockAndQuitButton.setContentDisplay(ContentDisplay.TEXT_ONLY); - // TODO: show force lock or force quit scene (and DO NOT cancelQuit() here!) (see https://github.com/cryptomator/cryptomator/pull/1416) - window.close(); - respondToQuitRequest(QuitResponse::cancelQuit); + window.setScene(quitForcedScene.get()); }); executorService.execute(lockAllTask); } - -} +} \ No newline at end of file diff --git a/src/main/java/org/cryptomator/ui/quit/QuitForcedController.java b/src/main/java/org/cryptomator/ui/quit/QuitForcedController.java new file mode 100644 index 000000000..64e99d39c --- /dev/null +++ b/src/main/java/org/cryptomator/ui/quit/QuitForcedController.java @@ -0,0 +1,86 @@ +package org.cryptomator.ui.quit; + +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.VaultService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javafx.collections.ObservableList; +import javafx.concurrent.Task; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.ContentDisplay; +import javafx.stage.Stage; +import java.awt.desktop.QuitResponse; +import java.util.Collection; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class QuitForcedController implements FxController { + + private static final Logger LOG = LoggerFactory.getLogger(QuitForcedController.class); + + private final Stage window; + private final ObservableList unlockedVaults; + private final ExecutorService executorService; + private final VaultService vaultService; + private final AtomicReference quitResponse; + + /* FXML */ + public Button forceLockAndQuitButton; + + @Inject + QuitForcedController(@QuitWindow Stage window, ObservableList vaults, ExecutorService executorService, VaultService vaultService, @QuitWindow AtomicReference quitResponse) { + this.window = window; + this.unlockedVaults = vaults.filtered(Vault::isUnlocked); + this.executorService = executorService; + this.vaultService = vaultService; + this.quitResponse = quitResponse; + window.setOnCloseRequest(windowEvent -> cancel()); + } + + private void respondToQuitRequest(Consumer action) { + var response = quitResponse.getAndSet(null); + if (response != null) { + action.accept(response); + } + } + + @FXML + public void cancel() { + LOG.info("Quitting application forced canceled by user."); + window.close(); + respondToQuitRequest(QuitResponse::cancelQuit); + } + + @FXML + public void forceLockAndQuit() { + forceLockAndQuitButton.setDisable(true); + forceLockAndQuitButton.setContentDisplay(ContentDisplay.LEFT); + + Task> lockAllTask = vaultService.createLockAllTask(unlockedVaults, true); // forced set to true + lockAllTask.setOnSucceeded(evt -> { + LOG.info("Locked {}", lockAllTask.getValue().stream().map(Vault::getDisplayName).collect(Collectors.joining(", "))); + if (unlockedVaults.isEmpty()) { + window.close(); + respondToQuitRequest(QuitResponse::performQuit); + } + }); + lockAllTask.setOnFailed(evt -> { + //TODO: what will happen if force lock and quit app fails? + + LOG.error("Forced locking failed", lockAllTask.getException()); + forceLockAndQuitButton.setDisable(false); + forceLockAndQuitButton.setContentDisplay(ContentDisplay.TEXT_ONLY); + + window.close(); + respondToQuitRequest(QuitResponse::cancelQuit); + }); + executorService.execute(lockAllTask); + } + +} diff --git a/src/main/java/org/cryptomator/ui/quit/QuitModule.java b/src/main/java/org/cryptomator/ui/quit/QuitModule.java index a1dc581ed..f6615b321 100644 --- a/src/main/java/org/cryptomator/ui/quit/QuitModule.java +++ b/src/main/java/org/cryptomator/ui/quit/QuitModule.java @@ -16,8 +16,10 @@ import javax.inject.Provider; import javafx.scene.Scene; import javafx.stage.Modality; import javafx.stage.Stage; +import java.awt.desktop.QuitResponse; import java.util.Map; import java.util.ResourceBundle; +import java.util.concurrent.atomic.AtomicReference; @Module abstract class QuitModule { @@ -41,6 +43,14 @@ abstract class QuitModule { return stage; } + @Provides + @QuitWindow + @QuitScoped + static AtomicReference provideQuitResponse() { + return new AtomicReference(); + } + + @Provides @FxmlScene(FxmlFile.QUIT) @QuitScoped @@ -48,6 +58,14 @@ abstract class QuitModule { return fxmlLoaders.createScene(FxmlFile.QUIT); } + @Provides + @FxmlScene(FxmlFile.QUIT_FORCED) + @QuitScoped + static Scene provideQuitForcedScene(@QuitWindow FxmlLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene(FxmlFile.QUIT_FORCED); + } + + // ------------------ @Binds @@ -55,4 +73,9 @@ abstract class QuitModule { @FxControllerKey(QuitController.class) abstract FxController bindQuitController(QuitController controller); + @Binds + @IntoMap + @FxControllerKey(QuitForcedController.class) + abstract FxController bindQuitForcedController(QuitForcedController controller); + } diff --git a/src/main/resources/fxml/preferences_general.fxml b/src/main/resources/fxml/preferences_general.fxml index 6d2b68447..c255e2a0f 100644 --- a/src/main/resources/fxml/preferences_general.fxml +++ b/src/main/resources/fxml/preferences_general.fxml @@ -24,6 +24,8 @@ + +