mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-04-22 18:46:53 -04:00
Feature/simple spinning (#1728)
* Define discrete rotation animation * New class FontAwesome5Spinner extending FontAwesome5IconView, animated * New class AutoAnimator to play Animation on a Node conditionally * Replace all occurences of Progress Indicator with FontAwesome5Spinner * Spin spinner icon in processing vault state * remove undocumented progress indicator styling
This commit is contained in:
@@ -76,6 +76,7 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
private final BooleanProperty readyToCreateVault;
|
||||
private final ObjectBinding<ContentDisplay> createVaultButtonState;
|
||||
|
||||
/* FXML */
|
||||
public ToggleGroup recoveryKeyChoice;
|
||||
public Toggle showRecoveryKey;
|
||||
public Toggle skipRecoveryKey;
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.RotateTransition;
|
||||
import javafx.animation.SequentialTransition;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.value.WritableValue;
|
||||
import javafx.scene.Node;
|
||||
import javafx.stage.Window;
|
||||
import javafx.util.Duration;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class Animations {
|
||||
|
||||
@@ -33,4 +39,19 @@ public class Animations {
|
||||
);
|
||||
}
|
||||
|
||||
public static SequentialTransition createDiscrete360Rotation(Node toAnimate) {
|
||||
var animation = new SequentialTransition(IntStream.range(0, 8).mapToObj(i -> Animations.createDiscrete45Rotation()).toArray(Animation[]::new));
|
||||
animation.setCycleCount(Animation.INDEFINITE);
|
||||
animation.setNode(toAnimate);
|
||||
return animation;
|
||||
}
|
||||
|
||||
private static RotateTransition createDiscrete45Rotation() {
|
||||
var animation = new RotateTransition(Duration.millis(100));
|
||||
animation.setInterpolator(Interpolator.DISCRETE);
|
||||
animation.setByAngle(45);
|
||||
animation.setCycleCount(1);
|
||||
return animation;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
108
src/main/java/org/cryptomator/ui/common/AutoAnimator.java
Normal file
108
src/main/java/org/cryptomator/ui/common/AutoAnimator.java
Normal file
@@ -0,0 +1,108 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import com.tobiasdiez.easybind.EasyBind;
|
||||
import com.tobiasdiez.easybind.Subscription;
|
||||
|
||||
import javafx.animation.Animation;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
/**
|
||||
* Animation which starts and stops automatically based on an observable condition.
|
||||
* <p>
|
||||
* During creation the consumer can optionally define actions to be executed everytime before the animation starts and after it stops.
|
||||
* The automatic playback of the animation based on the condition can be stopped by calling {@link #deactivateCondition()}. To reactivate it, {@link #activateCondition()} must be called.
|
||||
*/
|
||||
public class AutoAnimator<T extends Animation> {
|
||||
|
||||
|
||||
private final T animation;
|
||||
private final ObservableValue<Boolean> condition;
|
||||
private final Runnable beforeStart;
|
||||
private final Runnable afterStop;
|
||||
|
||||
private Subscription sub;
|
||||
|
||||
AutoAnimator(T animation, ObservableValue<Boolean> condition, Runnable beforeStart, Runnable afterStop) {
|
||||
this.animation = animation;
|
||||
this.condition = condition;
|
||||
this.beforeStart = beforeStart;
|
||||
this.afterStop = afterStop;
|
||||
|
||||
activateCondition();
|
||||
}
|
||||
|
||||
public void playFromStart() {
|
||||
beforeStart.run();
|
||||
animation.playFromStart();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
animation.stop();
|
||||
afterStop.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivates activation on the condition.
|
||||
* No-op if condition is already deactivated.
|
||||
*/
|
||||
public void deactivateCondition() {
|
||||
if (sub != null) {
|
||||
sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the condition
|
||||
* No-op if condition is already activated.
|
||||
*/
|
||||
public void activateCondition() {
|
||||
if (sub == null) {
|
||||
this.sub = EasyBind.subscribe(condition, this::togglePlay);
|
||||
}
|
||||
}
|
||||
|
||||
private void togglePlay(boolean play) {
|
||||
if (play) {
|
||||
this.playFromStart();
|
||||
} else {
|
||||
this.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public static Builder animate(Animation animation) {
|
||||
return new Builder(animation);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private Animation animation;
|
||||
private ObservableValue<Boolean> condition = new SimpleBooleanProperty(true);
|
||||
private Runnable beforeStart = () -> {};
|
||||
private Runnable afterStop = () -> {};
|
||||
|
||||
private Builder(Animation animation) {
|
||||
this.animation = animation;
|
||||
}
|
||||
|
||||
public Builder onCondition(ObservableValue<Boolean> condition) {
|
||||
this.condition = condition;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder beforeStart(Runnable beforeStart) {
|
||||
this.beforeStart = beforeStart;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder afterStop(Runnable afterStop) {
|
||||
this.afterStop = afterStop;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AutoAnimator build() {
|
||||
return new AutoAnimator(animation, condition, beforeStart, afterStop);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.cryptomator.ui.controls;
|
||||
|
||||
import org.cryptomator.ui.common.Animations;
|
||||
import org.cryptomator.ui.common.AutoAnimator;
|
||||
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* An animated progress spinner using the {@link FontAwesome5IconView} with the spinner glyph.
|
||||
* <p>
|
||||
* Using the default constructor, the animation is always played if the icon is visible. To animate on other conditions, use the constructor with the "spinning" property.
|
||||
*/
|
||||
public class FontAwesome5Spinner extends FontAwesome5IconView {
|
||||
|
||||
private AutoAnimator animator;
|
||||
|
||||
public FontAwesome5Spinner() {
|
||||
new FontAwesome5Spinner(Optional.empty());
|
||||
}
|
||||
|
||||
public FontAwesome5Spinner(@NamedArg("spinning") ObservableValue<Boolean> spinning) {
|
||||
new FontAwesome5Spinner(Optional.of(spinning));
|
||||
}
|
||||
|
||||
private FontAwesome5Spinner(Optional<ObservableValue<Boolean>> animateCondition) {
|
||||
setGlyph(FontAwesome5Icon.SPINNER);
|
||||
var animation = Animations.createDiscrete360Rotation(this);
|
||||
this.animator = AutoAnimator.animate(animation) //
|
||||
.afterStop(() -> setRotate(0)) //
|
||||
.onCondition(animateCondition.orElse(visibleProperty())) //
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.UserInteractionLock;
|
||||
import org.cryptomator.ui.common.WeakBindings;
|
||||
import org.cryptomator.ui.controls.FontAwesome5IconView;
|
||||
import org.cryptomator.ui.controls.NiceSecurePasswordField;
|
||||
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
@@ -59,8 +60,10 @@ public class PassphraseEntryController implements FxController {
|
||||
private final BooleanProperty unlockButtonDisabled;
|
||||
private final StringBinding vaultName;
|
||||
|
||||
/* FXML */
|
||||
public NiceSecurePasswordField passwordField;
|
||||
public CheckBox savePasswordCheckbox;
|
||||
public FontAwesome5IconView unlockInProgressView;
|
||||
public ImageView face;
|
||||
public ImageView leftArm;
|
||||
public ImageView rightArm;
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package org.cryptomator.ui.mainwindow;
|
||||
|
||||
import com.tobiasdiez.easybind.EasyBind;
|
||||
import com.tobiasdiez.easybind.Subscription;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.ui.common.Animations;
|
||||
import org.cryptomator.ui.common.AutoAnimator;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.controls.FontAwesome5Icon;
|
||||
import org.cryptomator.ui.controls.FontAwesome5IconView;
|
||||
import org.cryptomator.ui.fxapp.FxApplication;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -22,6 +26,12 @@ public class VaultDetailController implements FxController {
|
||||
private final Binding<FontAwesome5Icon> glyph;
|
||||
private final BooleanBinding anyVaultSelected;
|
||||
|
||||
private AutoAnimator spinAnimation;
|
||||
|
||||
/* FXML */
|
||||
public FontAwesome5IconView vaultStateView;
|
||||
|
||||
|
||||
@Inject
|
||||
VaultDetailController(ObjectProperty<Vault> vault, FxApplication application) {
|
||||
this.vault = vault;
|
||||
@@ -32,6 +42,13 @@ public class VaultDetailController implements FxController {
|
||||
this.anyVaultSelected = vault.isNotNull();
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
this.spinAnimation = AutoAnimator.animate(Animations.createDiscrete360Rotation(vaultStateView)) //
|
||||
.onCondition(EasyBind.select(vault).selectObject(Vault::stateProperty).map(VaultState.Value.PROCESSING::equals)) //
|
||||
.afterStop(() -> vaultStateView.setRotate(0)) //
|
||||
.build();
|
||||
}
|
||||
|
||||
// TODO deduplicate w/ VaultListCellController
|
||||
private FontAwesome5Icon getGlyphForVaultState(VaultState.Value state) {
|
||||
if (state != null) {
|
||||
|
||||
@@ -3,8 +3,11 @@ package org.cryptomator.ui.mainwindow;
|
||||
import com.tobiasdiez.easybind.EasyBind;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.ui.common.Animations;
|
||||
import org.cryptomator.ui.common.AutoAnimator;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.controls.FontAwesome5Icon;
|
||||
import org.cryptomator.ui.controls.FontAwesome5IconView;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Binding;
|
||||
@@ -17,6 +20,11 @@ public class VaultListCellController implements FxController {
|
||||
private final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
|
||||
private final Binding<FontAwesome5Icon> glyph;
|
||||
|
||||
private AutoAnimator spinAnimation;
|
||||
|
||||
/* FXML */
|
||||
public FontAwesome5IconView vaultStateView;
|
||||
|
||||
@Inject
|
||||
VaultListCellController() {
|
||||
this.glyph = EasyBind.select(vault) //
|
||||
@@ -24,6 +32,13 @@ public class VaultListCellController implements FxController {
|
||||
.map(this::getGlyphForVaultState);
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
this.spinAnimation = AutoAnimator.animate(Animations.createDiscrete360Rotation(vaultStateView)) //
|
||||
.onCondition(EasyBind.select(vault).selectObject(Vault::stateProperty).map(VaultState.Value.PROCESSING::equals)) //
|
||||
.afterStop(() -> vaultStateView.setRotate(0)) //
|
||||
.build();
|
||||
}
|
||||
|
||||
// TODO deduplicate w/ VaultDetailController
|
||||
private FontAwesome5Icon getGlyphForVaultState(VaultState.Value state) {
|
||||
if (state != null) {
|
||||
@@ -59,4 +74,5 @@ public class VaultListCellController implements FxController {
|
||||
public void setVault(Vault value) {
|
||||
vault.set(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -66,7 +66,10 @@ public class MigrationRunController implements FxController {
|
||||
private final Lazy<Scene> capabilityErrorScene;
|
||||
private final BooleanProperty migrationButtonDisabled;
|
||||
private final DoubleProperty migrationProgress;
|
||||
|
||||
private volatile double volatileMigrationProgress = -1.0;
|
||||
|
||||
/* FXML */
|
||||
public NiceSecurePasswordField passwordField;
|
||||
|
||||
@Inject
|
||||
|
||||
@@ -26,6 +26,8 @@ public class UpdatesPreferencesController implements FxController {
|
||||
private final ReadOnlyStringProperty latestVersion;
|
||||
private final ReadOnlyStringProperty currentVersion;
|
||||
private final BooleanBinding updateAvailable;
|
||||
|
||||
/* FXML */
|
||||
public CheckBox checkForUpdatesCheckbox;
|
||||
|
||||
@Inject
|
||||
|
||||
@@ -30,6 +30,8 @@ public class QuitController implements FxController {
|
||||
private final ExecutorService executorService;
|
||||
private final VaultService vaultService;
|
||||
private final AtomicReference<QuitResponse> quitResponse = new AtomicReference<>();
|
||||
|
||||
/* FXML */
|
||||
public Button lockAndQuitButton;
|
||||
|
||||
@Inject
|
||||
|
||||
@@ -32,6 +32,7 @@ public class UnlockSuccessController implements FxController {
|
||||
private final ObjectProperty<ContentDisplay> revealButtonState;
|
||||
private final BooleanProperty revealButtonDisabled;
|
||||
|
||||
/* FXML */
|
||||
public CheckBox rememberChoiceCheckbox;
|
||||
|
||||
@Inject
|
||||
|
||||
Reference in New Issue
Block a user