mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-22 07:30:24 +00:00
More work on custom updating methods
This commit is contained in:
parent
86ec53ea56
commit
8c549b8050
26 changed files with 695 additions and 514 deletions
|
@ -1,4 +1,4 @@
|
|||
<img src="https://user-images.githubusercontent.com/72509152/213873342-7638e830-8a95-4b5d-ad3e-5a9a0b4bf538.png" alt="drawing" width="300"/>
|
||||
<img src="https://user-images.githubusercontent.com/72509152/213873342-7638e830-8a95-4b5d-ad3e-5a9a0b4bf538.png" alt="drawing" width="250"/>
|
||||
|
||||
### A smart connection manager and remote file explorer
|
||||
|
||||
|
|
|
@ -42,8 +42,11 @@ public class AppLayoutComp extends Comp<CompStructure<BorderPane>> {
|
|||
private List<SideMenuBarComp.Entry> createEntryList() {
|
||||
var l = new ArrayList<>(List.of(
|
||||
new SideMenuBarComp.Entry(AppI18n.observable("connections"), "mdi2c-connection", new StoreLayoutComp()),
|
||||
new SideMenuBarComp.Entry(AppI18n.observable("browser"), "mdi2f-file-cabinet", new FileBrowserComp(FileBrowserModel.DEFAULT)),
|
||||
//new SideMenuBarComp.Entry(AppI18n.observable("data"), "mdsal-dvr", new SourceCollectionLayoutComp()),
|
||||
new SideMenuBarComp.Entry(
|
||||
AppI18n.observable("browser"),
|
||||
"mdi2f-file-cabinet",
|
||||
new FileBrowserComp(FileBrowserModel.DEFAULT)),
|
||||
// new SideMenuBarComp.Entry(AppI18n.observable("data"), "mdsal-dvr", new SourceCollectionLayoutComp()),
|
||||
new SideMenuBarComp.Entry(
|
||||
AppI18n.observable("settings"), "mdsmz-miscellaneous_services", new PrefsComp(this)),
|
||||
// new SideMenuBarComp.Entry(AppI18n.observable("help"), "mdi2b-book-open-variant", new
|
||||
|
@ -51,9 +54,10 @@ public class AppLayoutComp extends Comp<CompStructure<BorderPane>> {
|
|||
// new SideMenuBarComp.Entry(AppI18n.observable("account"), "mdi2a-account", new StorageLayoutComp()),
|
||||
new SideMenuBarComp.Entry(AppI18n.observable("about"), "mdi2p-package-variant", new AboutTabComp())));
|
||||
if (AppProperties.get().isDeveloperMode()) {
|
||||
l.add(new SideMenuBarComp.Entry(AppI18n.observable("developer"), "mdi2b-book-open-variant", new
|
||||
DeveloperTabComp()));
|
||||
l.add(new SideMenuBarComp.Entry(
|
||||
AppI18n.observable("developer"), "mdi2b-book-open-variant", new DeveloperTabComp()));
|
||||
}
|
||||
|
||||
// l.add(new SideMenuBarComp.Entry(AppI18n.observable("abc"), "mdi2b-book-open-variant", Comp.of(() -> {
|
||||
// var fi = new FontIcon("mdsal-dvr");
|
||||
// fi.setIconSize(30);
|
||||
|
|
|
@ -3,13 +3,11 @@ package io.xpipe.app.comp.about;
|
|||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.fxcomps.SimpleComp;
|
||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||
import io.xpipe.app.update.AppUpdater;
|
||||
import io.xpipe.app.update.UpdateAvailableAlert;
|
||||
import io.xpipe.app.util.Hyperlinks;
|
||||
import io.xpipe.app.util.XPipeDistributionType;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.app.update.XPipeDistributionType;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
|
@ -20,62 +18,51 @@ import org.kordamp.ikonli.javafx.FontIcon;
|
|||
|
||||
public class UpdateCheckComp extends SimpleComp {
|
||||
|
||||
private final ObservableBooleanValue updateAvailable;
|
||||
private final ObservableValue<Boolean> updateReady;
|
||||
|
||||
public UpdateCheckComp() {
|
||||
updateAvailable = Bindings.createBooleanBinding(
|
||||
updateReady = PlatformThread.sync(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return AppUpdater.get().getLastUpdateCheckResult().getValue() != null
|
||||
&& AppUpdater.get()
|
||||
.getLastUpdateCheckResult()
|
||||
.getValue()
|
||||
.isUpdate();
|
||||
return XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate().getValue() != null;
|
||||
},
|
||||
PlatformThread.sync(AppUpdater.get().getLastUpdateCheckResult()));
|
||||
updateReady = Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return AppUpdater.get().getDownloadedUpdate().getValue() != null;
|
||||
},
|
||||
PlatformThread.sync(AppUpdater.get().getDownloadedUpdate()));
|
||||
XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate()));
|
||||
}
|
||||
|
||||
private void restart() {
|
||||
AppUpdater.get().refreshUpdateCheckSilent();
|
||||
XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheckSilent();
|
||||
UpdateAvailableAlert.showIfNeeded();
|
||||
}
|
||||
|
||||
private void download() {
|
||||
AppUpdater.get().downloadUpdateAsync();
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
AppUpdater.get().checkForUpdateAsync();
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheck();
|
||||
XPipeDistributionType.get().getUpdateHandler().prepareUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
private ObservableValue<String> descriptionText() {
|
||||
return PlatformThread.sync(Bindings.createStringBinding(
|
||||
() -> {
|
||||
if (AppUpdater.get().getDownloadedUpdate().getValue() != null) {
|
||||
return AppI18n.get("updateRestart");
|
||||
if (XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate().getValue() != null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (AppUpdater.get().getLastUpdateCheckResult().getValue() != null
|
||||
&& AppUpdater.get()
|
||||
if (XPipeDistributionType.get().getUpdateHandler().getLastUpdateCheckResult().getValue() != null
|
||||
&& XPipeDistributionType.get().getUpdateHandler()
|
||||
.getLastUpdateCheckResult()
|
||||
.getValue()
|
||||
.isUpdate()) {
|
||||
return AppI18n.get(
|
||||
"updateAvailable",
|
||||
AppUpdater.get()
|
||||
XPipeDistributionType.get().getUpdateHandler()
|
||||
.getLastUpdateCheckResult()
|
||||
.getValue()
|
||||
.getVersion());
|
||||
}
|
||||
|
||||
if (AppUpdater.get().getLastUpdateCheckResult().getValue() != null) {
|
||||
if (XPipeDistributionType.get().getUpdateHandler().getLastUpdateCheckResult().getValue() != null) {
|
||||
return AppI18n.readableDuration(
|
||||
new SimpleObjectProperty<>(AppUpdater.get()
|
||||
new SimpleObjectProperty<>(XPipeDistributionType.get().getUpdateHandler()
|
||||
.getLastUpdateCheckResult()
|
||||
.getValue()
|
||||
.getCheckTime()),
|
||||
|
@ -85,15 +72,15 @@ public class UpdateCheckComp extends SimpleComp {
|
|||
return null;
|
||||
}
|
||||
},
|
||||
AppUpdater.get().getLastUpdateCheckResult(),
|
||||
AppUpdater.get().getDownloadedUpdate(),
|
||||
AppUpdater.get().getBusy()));
|
||||
XPipeDistributionType.get().getUpdateHandler().getLastUpdateCheckResult(),
|
||||
XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate(),
|
||||
XPipeDistributionType.get().getUpdateHandler().getBusy()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Region createSimple() {
|
||||
var button = new Button();
|
||||
button.disableProperty().bind(PlatformThread.sync(AppUpdater.get().getBusy()));
|
||||
button.disableProperty().bind(PlatformThread.sync(XPipeDistributionType.get().getUpdateHandler().getBusy()));
|
||||
button.textProperty()
|
||||
.bind(Bindings.createStringBinding(
|
||||
() -> {
|
||||
|
@ -101,30 +88,18 @@ public class UpdateCheckComp extends SimpleComp {
|
|||
return AppI18n.get("updateReady");
|
||||
}
|
||||
|
||||
if (updateAvailable.getValue()) {
|
||||
return XPipeDistributionType.get().supportsUpdate()
|
||||
? AppI18n.get("downloadUpdate")
|
||||
: AppI18n.get("checkOutUpdate");
|
||||
} else {
|
||||
return AppI18n.get("checkForUpdates");
|
||||
}
|
||||
return AppI18n.get("checkForUpdates");
|
||||
},
|
||||
updateAvailable,
|
||||
updateReady));
|
||||
button.graphicProperty()
|
||||
.bind(Bindings.createObjectBinding(
|
||||
() -> {
|
||||
if (updateReady.getValue()) {
|
||||
return new FontIcon("mdi2r-restart");
|
||||
return new FontIcon("mdi2a-apple-airplay");
|
||||
}
|
||||
|
||||
if (updateAvailable.getValue()) {
|
||||
return new FontIcon("mdi2d-download");
|
||||
} else {
|
||||
return new FontIcon("mdi2r-refresh");
|
||||
}
|
||||
return new FontIcon("mdi2r-refresh");
|
||||
},
|
||||
updateAvailable,
|
||||
updateReady));
|
||||
button.getStyleClass().add("button-comp");
|
||||
button.setOnAction(e -> {
|
||||
|
@ -133,14 +108,7 @@ public class UpdateCheckComp extends SimpleComp {
|
|||
return;
|
||||
}
|
||||
|
||||
if (updateAvailable.getValue() && !XPipeDistributionType.get().supportsUpdate()) {
|
||||
Hyperlinks.open(
|
||||
AppUpdater.get().getLastUpdateCheckResult().getValue().getReleaseUrl());
|
||||
} else if (updateAvailable.getValue()) {
|
||||
download();
|
||||
} else {
|
||||
refresh();
|
||||
}
|
||||
refresh();
|
||||
});
|
||||
|
||||
var checked = new Label();
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
package io.xpipe.app.comp.base;
|
||||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.fxcomps.Comp;
|
||||
import io.xpipe.app.fxcomps.CompStructure;
|
||||
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
||||
import io.xpipe.app.fxcomps.augment.GrowAugment;
|
||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||
import io.xpipe.app.update.UpdateAvailableAlert;
|
||||
import io.xpipe.app.update.XPipeDistributionType;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.css.PseudoClass;
|
||||
|
@ -40,6 +45,18 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
|||
});
|
||||
vbox.getChildren().add(b.createRegion());
|
||||
});
|
||||
|
||||
{
|
||||
// vbox.getChildren().add(new Spacer(Orientation.VERTICAL));
|
||||
var fi = new FontIcon("mdi2u-update");
|
||||
var b = new BigIconButton(AppI18n.observable("update"), fi, () -> UpdateAvailableAlert.showIfNeeded());
|
||||
b.apply(GrowAugment.create(true, false));
|
||||
b.hide(PlatformThread.sync(Bindings.createBooleanBinding(() -> {
|
||||
return XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate().getValue() == null;
|
||||
}, XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate())));
|
||||
vbox.getChildren().add(b.createRegion());
|
||||
}
|
||||
|
||||
vbox.getStyleClass().add("sidebar-comp");
|
||||
return new SimpleCompStructure<>(vbox);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import io.xpipe.app.comp.AppLayoutComp;
|
|||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.update.AppUpdater;
|
||||
import io.xpipe.app.update.XPipeDistributionType;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
|
@ -76,21 +76,17 @@ public class App extends Application {
|
|||
() -> {
|
||||
var base = String.format(
|
||||
"X-Pipe Desktop (%s)", AppProperties.get().getVersion());
|
||||
var suffix = AppUpdater.get().getLastUpdateCheckResult().getValue() != null
|
||||
&& AppUpdater.get()
|
||||
.getLastUpdateCheckResult()
|
||||
.getValue()
|
||||
.isUpdate()
|
||||
var suffix = XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate().getValue() != null
|
||||
? String.format(
|
||||
" (Update to %s available)",
|
||||
AppUpdater.get()
|
||||
.getLastUpdateCheckResult()
|
||||
" (Update to %s ready)",
|
||||
XPipeDistributionType.get().getUpdateHandler()
|
||||
.getPreparedUpdate()
|
||||
.getValue()
|
||||
.getVersion())
|
||||
: "";
|
||||
return base + suffix;
|
||||
},
|
||||
AppUpdater.get().getLastUpdateCheckResult());
|
||||
XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate());
|
||||
|
||||
var appWindow = new AppMainWindow(stage);
|
||||
appWindow.getStage().titleProperty().bind(PlatformThread.sync(titleBinding));
|
||||
|
|
|
@ -3,10 +3,12 @@ package io.xpipe.app.core.mode;
|
|||
import io.xpipe.app.comp.storage.collection.SourceCollectionViewState;
|
||||
import io.xpipe.app.comp.storage.store.StoreViewState;
|
||||
import io.xpipe.app.core.*;
|
||||
import io.xpipe.app.issue.*;
|
||||
import io.xpipe.app.issue.ErrorAction;
|
||||
import io.xpipe.app.issue.ErrorHandler;
|
||||
import io.xpipe.app.issue.LogErrorHandler;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.update.AppUpdater;
|
||||
import io.xpipe.app.util.FileBridge;
|
||||
import io.xpipe.core.util.JacksonMapper;
|
||||
|
||||
|
@ -40,7 +42,6 @@ public class BaseMode extends OperationMode {
|
|||
AppFileWatcher.init();
|
||||
FileBridge.init();
|
||||
AppSocketServer.init();
|
||||
AppUpdater.init();
|
||||
TrackEvent.info("mode", "Finished base components initialization");
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import io.xpipe.app.issue.TrackEvent;
|
|||
import io.xpipe.app.launcher.LauncherCommand;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.util.XPipeDaemonMode;
|
||||
import io.xpipe.core.util.XPipeSession;
|
||||
import io.xpipe.app.util.XPipeSession;
|
||||
import org.apache.commons.lang3.function.FailableRunnable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
|
@ -6,7 +6,7 @@ import io.sentry.protocol.User;
|
|||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.app.core.AppProperties;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.util.XPipeDistributionType;
|
||||
import io.xpipe.app.update.XPipeDistributionType;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
import java.nio.file.Files;
|
||||
|
|
|
@ -3,8 +3,8 @@ package io.xpipe.app.issue;
|
|||
import io.sentry.Sentry;
|
||||
import io.xpipe.app.core.*;
|
||||
import io.xpipe.app.core.mode.OperationMode;
|
||||
import io.xpipe.app.update.AppUpdater;
|
||||
import io.xpipe.app.util.Hyperlinks;
|
||||
import io.xpipe.app.update.XPipeDistributionType;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonBar;
|
||||
|
@ -87,8 +87,7 @@ public class TerminalErrorHandler implements ErrorHandler {
|
|||
|
||||
private static void handleProbableUpdate() {
|
||||
try {
|
||||
AppUpdater.init();
|
||||
var rel = AppUpdater.get().refreshUpdateCheck();
|
||||
var rel = XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheck();
|
||||
if (rel != null && rel.isUpdate()) {
|
||||
var update = AppWindowHelper.showBlockingAlert(alert -> {
|
||||
alert.setAlertType(Alert.AlertType.INFORMATION);
|
||||
|
|
|
@ -14,7 +14,6 @@ import io.xpipe.app.ext.PrefsHandler;
|
|||
import io.xpipe.app.ext.PrefsProvider;
|
||||
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.util.XPipeDistributionType;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
|
@ -146,13 +145,14 @@ public class AppPrefs {
|
|||
|
||||
// Automatically update
|
||||
// ====================
|
||||
private final BooleanProperty automaticallyUpdate =
|
||||
typed(new SimpleBooleanProperty(XPipeDistributionType.get().supportsUpdate()), Boolean.class);
|
||||
private final BooleanField automaticallyUpdateField =
|
||||
BooleanField.ofBooleanType(automaticallyUpdate).render(() -> new CustomToggleControl());
|
||||
private final BooleanProperty updateToPrereleases = typed(new SimpleBooleanProperty(false), Boolean.class);
|
||||
private final BooleanField updateToPrereleasesField =
|
||||
BooleanField.ofBooleanType(updateToPrereleases).render(() -> new CustomToggleControl());
|
||||
private final BooleanProperty automaticallyCheckForUpdates =
|
||||
typed(new SimpleBooleanProperty(true), Boolean.class);
|
||||
private final BooleanField automaticallyCheckForUpdatesField =
|
||||
BooleanField.ofBooleanType(automaticallyCheckForUpdates).render(() -> new CustomToggleControl());
|
||||
|
||||
private final BooleanProperty checkForPrereleases = typed(new SimpleBooleanProperty(false), Boolean.class);
|
||||
private final BooleanField checkForPrereleasesField =
|
||||
BooleanField.ofBooleanType(checkForPrereleases).render(() -> new CustomToggleControl());
|
||||
|
||||
private final BooleanProperty confirmDeletions = typed(new SimpleBooleanProperty(true), Boolean.class);
|
||||
|
||||
|
@ -236,11 +236,11 @@ public class AppPrefs {
|
|||
}
|
||||
|
||||
public ReadOnlyBooleanProperty automaticallyUpdate() {
|
||||
return automaticallyUpdate;
|
||||
return automaticallyCheckForUpdates;
|
||||
}
|
||||
|
||||
public ReadOnlyBooleanProperty updateToPrereleases() {
|
||||
return updateToPrereleases;
|
||||
return checkForPrereleases;
|
||||
}
|
||||
|
||||
public ReadOnlyBooleanProperty confirmDeletions() {
|
||||
|
@ -431,12 +431,8 @@ public class AppPrefs {
|
|||
Setting.of("closeBehaviour", closeBehaviourControl, closeBehaviour)),
|
||||
Group.of(
|
||||
"updates",
|
||||
Setting.of("automaticallyUpdate", automaticallyUpdateField, automaticallyUpdate)
|
||||
.applyVisibility(VisibilityProperty.of(new SimpleBooleanProperty(
|
||||
XPipeDistributionType.get().supportsUpdate()))),
|
||||
Setting.of("updateToPrereleases", updateToPrereleasesField, updateToPrereleases)
|
||||
.applyVisibility(VisibilityProperty.of(new SimpleBooleanProperty(
|
||||
XPipeDistributionType.get().supportsUpdate())))),
|
||||
Setting.of("automaticallyUpdate", automaticallyCheckForUpdatesField, automaticallyCheckForUpdates),
|
||||
Setting.of("updateToPrereleases", checkForPrereleasesField, checkForPrereleases)),
|
||||
Group.of(
|
||||
"advanced",
|
||||
Setting.of("storageDirectory", storageDirectoryControl, internalStorageDirectory),
|
||||
|
|
|
@ -8,6 +8,7 @@ import io.xpipe.core.impl.FileNames;
|
|||
import io.xpipe.core.impl.LocalStore;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
import io.xpipe.core.process.ShellDialects;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -64,7 +65,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
new SimpleType("gnomeTerminal", "gnome-terminal", "Gnome Terminal") {
|
||||
|
||||
@Override
|
||||
public void launch(String name, String file) throws Exception {
|
||||
public void launch(String name, String file, boolean elevated) throws Exception {
|
||||
try (ShellControl pc = LocalStore.getShell()) {
|
||||
ApplicationHelper.checkSupport(pc, executable, getDisplayName());
|
||||
|
||||
|
@ -144,7 +145,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
.orElse(null);
|
||||
}
|
||||
|
||||
public abstract void launch(String name, String file) throws Exception;
|
||||
public abstract void launch(String name, String file, boolean elevated) throws Exception;
|
||||
|
||||
static class MacOsTerminalType extends ExternalApplicationType.MacApplication implements ExternalTerminalType {
|
||||
|
||||
|
@ -153,7 +154,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void launch(String name, String file) throws Exception {
|
||||
public void launch(String name, String file, boolean elevated) throws Exception {
|
||||
try (ShellControl pc = LocalStore.getShell()) {
|
||||
var suffix = file.equals(pc.getShellDialect().getOpenCommand())
|
||||
? "\"\""
|
||||
|
@ -171,7 +172,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void launch(String name, String file) throws Exception {
|
||||
public void launch(String name, String file, boolean elevated) throws Exception {
|
||||
var custom = AppPrefs.get().customTerminalCommand().getValue();
|
||||
if (custom == null || custom.isBlank()) {
|
||||
throw new IllegalStateException("No custom terminal command specified");
|
||||
|
@ -207,7 +208,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void launch(String name, String file) throws Exception {
|
||||
public void launch(String name, String file, boolean elevated) throws Exception {
|
||||
try (ShellControl pc = LocalStore.getShell()) {
|
||||
var cmd = String.format(
|
||||
"""
|
||||
|
@ -241,7 +242,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void launch(String name, String file) throws Exception {
|
||||
public void launch(String name, String file, boolean elevated) throws Exception {
|
||||
if (!MacOsPermissions.waitForAccessibilityPermissions()) {
|
||||
return;
|
||||
}
|
||||
|
@ -279,7 +280,18 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void launch(String name, String file) throws Exception {
|
||||
public void launch(String name, String file, boolean elevated) throws Exception {
|
||||
if (elevated) {
|
||||
if (OsType.getLocal().equals(OsType.WINDOWS)) {
|
||||
try (ShellControl pc = LocalStore.getShell().subShell(ShellDialects.POWERSHELL).start()) {
|
||||
ApplicationHelper.checkSupport(pc, executable, displayName);
|
||||
var toExecute = "Start-Process \"" + executable + "\" -Verb RunAs -ArgumentList \"" + toCommand(name, file).replaceAll("\"", "`\"") + "\"";
|
||||
pc.executeSimpleCommand(toExecute);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try (ShellControl pc = LocalStore.getShell()) {
|
||||
ApplicationHelper.checkSupport(pc, executable, displayName);
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ package io.xpipe.app.storage;
|
|||
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.core.util.XPipeSession;
|
||||
import io.xpipe.app.util.XPipeSession;
|
||||
import lombok.NonNull;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import io.xpipe.beacon.BeaconDaemonController;
|
|||
import io.xpipe.core.store.DataStore;
|
||||
import io.xpipe.core.util.JacksonMapper;
|
||||
import io.xpipe.core.util.XPipeDaemonMode;
|
||||
import io.xpipe.core.util.XPipeSession;
|
||||
import io.xpipe.app.util.XPipeSession;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ package io.xpipe.app.test;
|
|||
|
||||
import io.xpipe.app.ext.XPipeServiceProviders;
|
||||
import io.xpipe.core.util.JacksonMapper;
|
||||
import io.xpipe.core.util.XPipeSession;
|
||||
import io.xpipe.app.util.XPipeSession;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
|
||||
import java.util.UUID;
|
||||
|
|
|
@ -1,308 +0,0 @@
|
|||
package io.xpipe.app.update;
|
||||
|
||||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.app.core.AppProperties;
|
||||
import io.xpipe.app.core.mode.OperationMode;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.util.BusyProperty;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.app.util.XPipeDistributionType;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.Value;
|
||||
import lombok.With;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
import org.kohsuke.github.GHRelease;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
@Getter
|
||||
public class AppUpdater {
|
||||
|
||||
private static AppUpdater INSTANCE;
|
||||
private final Property<AvailableRelease> lastUpdateCheckResult = new SimpleObjectProperty<>();
|
||||
private final Property<DownloadedUpdate> downloadedUpdate = new SimpleObjectProperty<>();
|
||||
private final BooleanProperty busy = new SimpleBooleanProperty();
|
||||
private final PerformedUpdate performedUpdate;
|
||||
private final boolean updateSucceeded;
|
||||
|
||||
private AppUpdater() {
|
||||
performedUpdate = AppCache.get("performedUpdate", PerformedUpdate.class, () -> null);
|
||||
var hasUpdated = performedUpdate != null;
|
||||
event("Was updated is " + hasUpdated);
|
||||
if (hasUpdated) {
|
||||
AppCache.clear("performedUpdate");
|
||||
updateSucceeded = AppProperties.get().getVersion().equals(performedUpdate.getNewVersion());
|
||||
AppCache.clear("lastUpdateCheckResult");
|
||||
AppCache.clear("downloadedUpdate");
|
||||
event("Found information about recent update");
|
||||
} else {
|
||||
updateSucceeded = false;
|
||||
}
|
||||
|
||||
downloadedUpdate.setValue(AppCache.get("downloadedUpdate", DownloadedUpdate.class, () -> null));
|
||||
|
||||
// Check if the original version this was downloaded from is still the same
|
||||
if (downloadedUpdate.getValue() != null
|
||||
&& !downloadedUpdate
|
||||
.getValue()
|
||||
.getSourceVersion()
|
||||
.equals(AppProperties.get().getVersion())) {
|
||||
downloadedUpdate.setValue(null);
|
||||
}
|
||||
|
||||
// Check if somehow the downloaded version is equal to the current one
|
||||
if (downloadedUpdate.getValue() != null
|
||||
&& downloadedUpdate
|
||||
.getValue()
|
||||
.getVersion()
|
||||
.equals(AppProperties.get().getVersion())) {
|
||||
downloadedUpdate.setValue(null);
|
||||
}
|
||||
|
||||
if (!XPipeDistributionType.get().supportsUpdate()) {
|
||||
downloadedUpdate.setValue(null);
|
||||
}
|
||||
|
||||
downloadedUpdate.addListener((c, o, n) -> {
|
||||
AppCache.update("downloadedUpdate", n);
|
||||
});
|
||||
lastUpdateCheckResult.addListener((c, o, n) -> {
|
||||
if (n != null && downloadedUpdate.getValue() != null && n.isUpdate() && n.getVersion().equals(downloadedUpdate.getValue().getVersion())) {
|
||||
return;
|
||||
}
|
||||
|
||||
downloadedUpdate.setValue(null);
|
||||
});
|
||||
|
||||
if (XPipeDistributionType.get().checkForUpdateOnStartup()) {
|
||||
refreshUpdateCheckSilent();
|
||||
}
|
||||
}
|
||||
|
||||
private static void event(String msg) {
|
||||
TrackEvent.builder().category("installer").type("info").message(msg).handle();
|
||||
}
|
||||
|
||||
public static AppUpdater get() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
if (INSTANCE != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
INSTANCE = new AppUpdater();
|
||||
startBackgroundUpdater();
|
||||
}
|
||||
|
||||
private static void startBackgroundUpdater() {
|
||||
if (XPipeDistributionType.get().supportsUpdate()
|
||||
&& XPipeDistributionType.get() != XPipeDistributionType.DEVELOPMENT) {
|
||||
ThreadHelper.create("updater", true, () -> {
|
||||
ThreadHelper.sleep(Duration.ofMinutes(10).toMillis());
|
||||
event("Starting background updater thread");
|
||||
while (true) {
|
||||
var rel = INSTANCE.refreshUpdateCheckSilent();
|
||||
if (rel != null
|
||||
&& AppPrefs.get().automaticallyUpdate().get() && rel.isUpdate()) {
|
||||
event("Performing background update");
|
||||
INSTANCE.downloadUpdate();
|
||||
}
|
||||
|
||||
ThreadHelper.sleep(Duration.ofHours(1).toMillis());
|
||||
}
|
||||
})
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isUpdate(String releaseVersion) {
|
||||
if (AppPrefs.get() != null
|
||||
&& AppPrefs.get().developerMode().getValue()
|
||||
&& AppPrefs.get().developerDisableUpdateVersionCheck().get()) {
|
||||
event("Bypassing version check");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!AppProperties.get().getVersion().equals(releaseVersion)) {
|
||||
event("Release has a different version");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void downloadUpdateAsync() {
|
||||
ThreadHelper.runAsync(() -> downloadUpdate());
|
||||
}
|
||||
|
||||
public synchronized void downloadUpdate() {
|
||||
if (busy.getValue()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastUpdateCheckResult.getValue() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!XPipeDistributionType.get().supportsUpdate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lastUpdateCheckResult.getValue().isUpdate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (var ignored = new BusyProperty(busy)) {
|
||||
event("Performing update download ...");
|
||||
try {
|
||||
var downloadFile = AppDownloads.downloadInstaller(
|
||||
lastUpdateCheckResult.getValue().getAssetType(),
|
||||
lastUpdateCheckResult.getValue().version,
|
||||
false);
|
||||
if (downloadFile.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var changelogString = AppDownloads.downloadChangelog(lastUpdateCheckResult.getValue().version, false);
|
||||
var changelog = changelogString.orElse(null);
|
||||
var rel = new DownloadedUpdate(
|
||||
AppProperties.get().getVersion(),
|
||||
lastUpdateCheckResult.getValue().version,
|
||||
downloadFile.get(),
|
||||
changelog,
|
||||
lastUpdateCheckResult.getValue().getAssetType());
|
||||
downloadedUpdate.setValue(rel);
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).omit().handle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void executeUpdateAndClose() {
|
||||
if (busy.getValue()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (downloadedUpdate.getValue() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var downloadFile = downloadedUpdate.getValue().getFile();
|
||||
if (!Files.exists(downloadFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event("Executing update ...");
|
||||
OperationMode.executeAfterShutdown(() -> {
|
||||
try {
|
||||
AppInstaller.installFileLocal(downloadedUpdate.getValue().getAssetType(), downloadFile);
|
||||
} catch (Throwable ex) {
|
||||
ex.printStackTrace();
|
||||
} finally {
|
||||
var performedUpdate = new PerformedUpdate(
|
||||
downloadedUpdate.getValue().getVersion(),
|
||||
downloadedUpdate.getValue().getBody(),
|
||||
downloadedUpdate.getValue().getVersion());
|
||||
AppCache.update("performedUpdate", performedUpdate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void checkForUpdateAsync() {
|
||||
ThreadHelper.runAsync(() -> refreshUpdateCheckSilent());
|
||||
}
|
||||
|
||||
public synchronized AvailableRelease refreshUpdateCheckSilent() {
|
||||
try {
|
||||
return refreshUpdateCheck();
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).omit().handle();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized AvailableRelease refreshUpdateCheck() throws IOException {
|
||||
if (busy.getValue()) {
|
||||
return lastUpdateCheckResult.getValue();
|
||||
}
|
||||
|
||||
try (var ignored = new BusyProperty(busy)) {
|
||||
var rel = AppDownloads.getLatestSuitableRelease();
|
||||
event("Determined latest suitable release "
|
||||
+ rel.map(GHRelease::getName).orElse(null));
|
||||
|
||||
if (rel.isEmpty()) {
|
||||
lastUpdateCheckResult.setValue(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
var isUpdate = isUpdate(rel.get().getTagName());
|
||||
var assetType = AppInstaller.getSuitablePlatformAsset();
|
||||
var ghAsset = rel.orElseThrow().listAssets().toList().stream()
|
||||
.filter(g -> assetType.isCorrectAsset(g.getName()))
|
||||
.findAny();
|
||||
if (ghAsset.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
event("Selected asset " + ghAsset.get().getName());
|
||||
lastUpdateCheckResult.setValue(new AvailableRelease(
|
||||
AppProperties.get().getVersion(),
|
||||
rel.get().getTagName(),
|
||||
rel.get().getHtmlUrl().toString(),
|
||||
ghAsset.get().getBrowserDownloadUrl(),
|
||||
assetType,
|
||||
Instant.now(),
|
||||
isUpdate));
|
||||
}
|
||||
|
||||
return lastUpdateCheckResult.getValue();
|
||||
}
|
||||
|
||||
@Value
|
||||
@Builder
|
||||
@Jacksonized
|
||||
public static class PerformedUpdate {
|
||||
String name;
|
||||
String rawDescription;
|
||||
String newVersion;
|
||||
}
|
||||
|
||||
@Value
|
||||
@Builder
|
||||
@Jacksonized
|
||||
@With
|
||||
public static class AvailableRelease {
|
||||
String sourceVersion;
|
||||
String version;
|
||||
String releaseUrl;
|
||||
String downloadUrl;
|
||||
AppInstaller.InstallerAssetType assetType;
|
||||
Instant checkTime;
|
||||
boolean isUpdate;
|
||||
}
|
||||
|
||||
@Value
|
||||
@Builder
|
||||
@Jacksonized
|
||||
public static class DownloadedUpdate {
|
||||
String sourceVersion;
|
||||
String version;
|
||||
Path file;
|
||||
String body;
|
||||
AppInstaller.InstallerAssetType assetType;
|
||||
}
|
||||
}
|
49
app/src/main/java/io/xpipe/app/update/ChocoUpdater.java
Normal file
49
app/src/main/java/io/xpipe/app/update/ChocoUpdater.java
Normal file
|
@ -0,0 +1,49 @@
|
|||
package io.xpipe.app.update;
|
||||
|
||||
import io.xpipe.app.core.AppProperties;
|
||||
import io.xpipe.app.fxcomps.impl.CodeSnippet;
|
||||
import io.xpipe.app.fxcomps.impl.CodeSnippetComp;
|
||||
import io.xpipe.core.store.ShellStore;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public class ChocoUpdater extends UpdateHandler {
|
||||
|
||||
public ChocoUpdater() {
|
||||
super(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Region createInterface() {
|
||||
var snippet = CodeSnippet.builder()
|
||||
.keyword("choco")
|
||||
.space()
|
||||
.identifier("install")
|
||||
.space()
|
||||
.string("xpipe")
|
||||
.space()
|
||||
.keyword("--version=" + getLastUpdateCheckResult().getValue().getVersion())
|
||||
.build();
|
||||
return new CodeSnippetComp(false, new SimpleObjectProperty<>(snippet)).createRegion();
|
||||
}
|
||||
|
||||
public AvailableRelease refreshUpdateCheckImpl() throws Exception {
|
||||
try (var sc = ShellStore.createLocal().create().start()) {
|
||||
var latest = sc.executeStringSimpleCommand(
|
||||
"choco outdated -r --nocolor").lines().filter(s -> s.startsWith("xpipe")).findAny().orElseThrow().split("\\|")[2];
|
||||
var isUpdate = isUpdate(latest);
|
||||
var rel = new AvailableRelease(
|
||||
AppProperties.get().getVersion(),
|
||||
latest,
|
||||
"https://community.chocolatey.org/packages/xpipe/" + latest,
|
||||
null,
|
||||
null,
|
||||
Instant.now(),
|
||||
isUpdate);
|
||||
lastUpdateCheckResult.setValue(rel);
|
||||
return lastUpdateCheckResult.getValue();
|
||||
}
|
||||
}
|
||||
}
|
82
app/src/main/java/io/xpipe/app/update/GitHubUpdater.java
Normal file
82
app/src/main/java/io/xpipe/app/update/GitHubUpdater.java
Normal file
|
@ -0,0 +1,82 @@
|
|||
package io.xpipe.app.update;
|
||||
|
||||
import io.xpipe.app.core.AppProperties;
|
||||
import javafx.scene.layout.Region;
|
||||
import org.kohsuke.github.GHRelease;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.time.Instant;
|
||||
|
||||
public class GitHubUpdater extends UpdateHandler {
|
||||
|
||||
public GitHubUpdater(boolean startBackgroundThread) {
|
||||
super(startBackgroundThread);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Region createInterface() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void prepareUpdateImpl() {
|
||||
var downloadFile = AppDownloads.downloadInstaller(
|
||||
lastUpdateCheckResult.getValue().getAssetType(),
|
||||
lastUpdateCheckResult.getValue().getVersion(),
|
||||
false);
|
||||
if (downloadFile.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var changelogString = AppDownloads.downloadChangelog(
|
||||
lastUpdateCheckResult.getValue().getVersion(), false);
|
||||
var changelog = changelogString.orElse(null);
|
||||
var rel = new PreparedUpdate(
|
||||
AppProperties.get().getVersion(),
|
||||
lastUpdateCheckResult.getValue().getVersion(),
|
||||
lastUpdateCheckResult.getValue().getReleaseUrl(),
|
||||
downloadFile.get(),
|
||||
changelog,
|
||||
lastUpdateCheckResult.getValue().getAssetType());
|
||||
preparedUpdate.setValue(rel);
|
||||
}
|
||||
|
||||
public void executeUpdateAndCloseImpl() throws Exception {
|
||||
var downloadFile = preparedUpdate.getValue().getFile();
|
||||
if (!Files.exists(downloadFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AppInstaller.installFileLocal(preparedUpdate.getValue().getAssetType(), downloadFile);
|
||||
}
|
||||
|
||||
public synchronized AvailableRelease refreshUpdateCheckImpl() throws Exception {
|
||||
var rel = AppDownloads.getLatestSuitableRelease();
|
||||
event("Determined latest suitable release "
|
||||
+ rel.map(GHRelease::getName).orElse(null));
|
||||
|
||||
if (rel.isEmpty()) {
|
||||
lastUpdateCheckResult.setValue(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
var isUpdate = isUpdate(rel.get().getTagName());
|
||||
var assetType = AppInstaller.getSuitablePlatformAsset();
|
||||
var ghAsset = rel.orElseThrow().listAssets().toList().stream()
|
||||
.filter(g -> assetType.isCorrectAsset(g.getName()))
|
||||
.findAny();
|
||||
if (ghAsset.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
event("Selected asset " + ghAsset.get().getName());
|
||||
lastUpdateCheckResult.setValue(new AvailableRelease(
|
||||
AppProperties.get().getVersion(),
|
||||
rel.get().getTagName(),
|
||||
rel.get().getHtmlUrl().toString(),
|
||||
ghAsset.get().getBrowserDownloadUrl(),
|
||||
assetType,
|
||||
Instant.now(),
|
||||
isUpdate));
|
||||
return lastUpdateCheckResult.getValue();
|
||||
}
|
||||
}
|
54
app/src/main/java/io/xpipe/app/update/PortableUpdater.java
Normal file
54
app/src/main/java/io/xpipe/app/update/PortableUpdater.java
Normal file
|
@ -0,0 +1,54 @@
|
|||
package io.xpipe.app.update;
|
||||
|
||||
import io.xpipe.app.comp.base.ButtonComp;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.AppProperties;
|
||||
import io.xpipe.app.util.Hyperlinks;
|
||||
import javafx.scene.layout.Region;
|
||||
import org.kohsuke.github.GHRelease;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public class PortableUpdater extends UpdateHandler {
|
||||
|
||||
public PortableUpdater() {
|
||||
super(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Region createInterface() {
|
||||
return new ButtonComp(AppI18n.observable("checkOutUpdate"), () -> {
|
||||
Hyperlinks.open(XPipeDistributionType.get()
|
||||
.getUpdateHandler()
|
||||
.getPreparedUpdate()
|
||||
.getValue()
|
||||
.getReleaseUrl());
|
||||
}).createRegion();
|
||||
}
|
||||
|
||||
public void executeUpdateAndCloseImpl() throws Exception {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public synchronized AvailableRelease refreshUpdateCheckImpl() throws Exception {
|
||||
var rel = AppDownloads.getLatestSuitableRelease();
|
||||
event("Determined latest suitable release "
|
||||
+ rel.map(GHRelease::getName).orElse(null));
|
||||
|
||||
if (rel.isEmpty()) {
|
||||
lastUpdateCheckResult.setValue(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
var isUpdate = isUpdate(rel.get().getTagName());
|
||||
lastUpdateCheckResult.setValue(new AvailableRelease(
|
||||
AppProperties.get().getVersion(),
|
||||
rel.get().getTagName(),
|
||||
rel.get().getHtmlUrl().toString(),
|
||||
null,
|
||||
null,
|
||||
Instant.now(),
|
||||
isUpdate));
|
||||
return lastUpdateCheckResult.getValue();
|
||||
}
|
||||
}
|
|
@ -3,42 +3,51 @@ package io.xpipe.app.update;
|
|||
import io.xpipe.app.comp.base.MarkdownComp;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.AppWindowHelper;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonBar;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
public class UpdateAvailableAlert {
|
||||
|
||||
public static void showIfNeeded() {
|
||||
if (AppUpdater.get().getDownloadedUpdate().getValue() == null) {
|
||||
UpdateHandler uh = XPipeDistributionType.get().getUpdateHandler();
|
||||
if (uh.getPreparedUpdate().getValue() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var u = AppUpdater.get().getDownloadedUpdate().getValue();
|
||||
var u = uh.getPreparedUpdate().getValue();
|
||||
var update = AppWindowHelper.showBlockingAlert(alert -> {
|
||||
alert.setTitle(AppI18n.get("updateReadyAlertTitle"));
|
||||
alert.setAlertType(Alert.AlertType.NONE);
|
||||
|
||||
if (u.getBody() != null && !u.getBody().isBlank()) {
|
||||
var markdown = new MarkdownComp(u.getBody(), s -> {
|
||||
var header = "<h1>" + AppI18n.get("whatsNew", u.getVersion()) + "</h1>";
|
||||
return header + s;
|
||||
})
|
||||
.createRegion();
|
||||
alert.getDialogPane().setContent(markdown);
|
||||
var markdown = new MarkdownComp(u.getBody() != null ? u.getBody() : "", s -> {
|
||||
var header = "<h1>" + AppI18n.get("whatsNew", u.getVersion()) + "</h1>";
|
||||
return header + s;
|
||||
})
|
||||
.createRegion();
|
||||
alert.getButtonTypes().clear();
|
||||
var updaterContent = uh.createInterface();
|
||||
if (updaterContent != null) {
|
||||
var stack = new StackPane(updaterContent);
|
||||
stack.setPadding(new Insets(18));
|
||||
var box = new VBox(markdown, stack);
|
||||
box.setFillWidth(true);
|
||||
box.setPadding(Insets.EMPTY);
|
||||
alert.getDialogPane().setContent(box);
|
||||
} else {
|
||||
alert.getDialogPane()
|
||||
.setContent(AppWindowHelper.alertContentText(AppI18n.get("updateReadyAlertContent")));
|
||||
alert.getDialogPane().setContent(markdown);
|
||||
alert.getButtonTypes().add(new ButtonType(AppI18n.get("install"), ButtonBar.ButtonData.OK_DONE));
|
||||
}
|
||||
|
||||
alert.getButtonTypes().clear();
|
||||
alert.getButtonTypes().add(new ButtonType(AppI18n.get("install"), ButtonBar.ButtonData.OK_DONE));
|
||||
alert.getButtonTypes().add(new ButtonType(AppI18n.get("ignore"), ButtonBar.ButtonData.NO));
|
||||
})
|
||||
.map(buttonType -> buttonType.getButtonData().isDefaultButton())
|
||||
.orElse(false);
|
||||
if (update) {
|
||||
AppUpdater.get().executeUpdateAndClose();
|
||||
uh.executeUpdateAndClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,9 +12,8 @@ import javafx.stage.Modality;
|
|||
public class UpdateChangelogAlert {
|
||||
|
||||
public static void showIfNeeded() {
|
||||
var update = AppUpdater.get().getPerformedUpdate();
|
||||
|
||||
if (update != null && !AppUpdater.get().isUpdateSucceeded()) {
|
||||
var update = XPipeDistributionType.get().getUpdateHandler().getPerformedUpdate();
|
||||
if (update != null && !XPipeDistributionType.get().getUpdateHandler().isUpdateSucceeded()) {
|
||||
ErrorEvent.fromMessage("Update did not succeed").handle();
|
||||
return;
|
||||
}
|
||||
|
|
259
app/src/main/java/io/xpipe/app/update/UpdateHandler.java
Normal file
259
app/src/main/java/io/xpipe/app/update/UpdateHandler.java
Normal file
|
@ -0,0 +1,259 @@
|
|||
package io.xpipe.app.update;
|
||||
|
||||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.app.core.AppProperties;
|
||||
import io.xpipe.app.core.mode.OperationMode;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.util.BusyProperty;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.layout.Region;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.Value;
|
||||
import lombok.With;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
@Getter
|
||||
public abstract class UpdateHandler {
|
||||
|
||||
protected final Property<AvailableRelease> lastUpdateCheckResult = new SimpleObjectProperty<>();
|
||||
protected final Property<PreparedUpdate> preparedUpdate = new SimpleObjectProperty<>();
|
||||
protected final BooleanProperty busy = new SimpleBooleanProperty();
|
||||
protected final PerformedUpdate performedUpdate;
|
||||
protected final boolean updateSucceeded;
|
||||
|
||||
protected UpdateHandler(boolean startBackgroundThread) {
|
||||
performedUpdate = AppCache.get("performedUpdate", PerformedUpdate.class, () -> null);
|
||||
var hasUpdated = performedUpdate != null;
|
||||
event("Was updated is " + hasUpdated);
|
||||
if (hasUpdated) {
|
||||
AppCache.clear("performedUpdate");
|
||||
updateSucceeded = AppProperties.get().getVersion().equals(performedUpdate.getNewVersion());
|
||||
AppCache.clear("lastUpdateCheckResult");
|
||||
AppCache.clear("preparedUpdate");
|
||||
event("Found information about recent update");
|
||||
} else {
|
||||
updateSucceeded = false;
|
||||
}
|
||||
|
||||
preparedUpdate.setValue(AppCache.get("preparedUpdate", PreparedUpdate.class, () -> null));
|
||||
|
||||
// Check if the original version this was downloaded from is still the same
|
||||
if (preparedUpdate.getValue() != null
|
||||
&& !preparedUpdate
|
||||
.getValue()
|
||||
.getSourceVersion()
|
||||
.equals(AppProperties.get().getVersion())) {
|
||||
preparedUpdate.setValue(null);
|
||||
}
|
||||
|
||||
// Check if somehow the downloaded version is equal to the current one
|
||||
if (preparedUpdate.getValue() != null
|
||||
&& preparedUpdate
|
||||
.getValue()
|
||||
.getVersion()
|
||||
.equals(AppProperties.get().getVersion())) {
|
||||
preparedUpdate.setValue(null);
|
||||
}
|
||||
|
||||
preparedUpdate.addListener((c, o, n) -> {
|
||||
AppCache.update("preparedUpdate", n);
|
||||
});
|
||||
lastUpdateCheckResult.addListener((c, o, n) -> {
|
||||
if (n != null
|
||||
&& preparedUpdate.getValue() != null
|
||||
&& n.isUpdate()
|
||||
&& n.getVersion().equals(preparedUpdate.getValue().getVersion())) {
|
||||
return;
|
||||
}
|
||||
|
||||
preparedUpdate.setValue(null);
|
||||
});
|
||||
|
||||
if (startBackgroundThread) {
|
||||
startBackgroundUpdater();
|
||||
}
|
||||
}
|
||||
|
||||
private void startBackgroundUpdater() {
|
||||
ThreadHelper.create("updater", true, () -> {
|
||||
ThreadHelper.sleep(Duration.ofMinutes(5).toMillis());
|
||||
event("Starting background updater thread");
|
||||
while (true) {
|
||||
if (AppPrefs.get().automaticallyUpdate().get()) {
|
||||
event("Performing background update");
|
||||
refreshUpdateCheckSilent();
|
||||
prepareUpdate();
|
||||
}
|
||||
|
||||
ThreadHelper.sleep(Duration.ofHours(1).toMillis());
|
||||
}
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
protected void event(String msg) {
|
||||
TrackEvent.builder().category("updater").type("info").message(msg).handle();
|
||||
}
|
||||
|
||||
protected final boolean isUpdate(String releaseVersion) {
|
||||
if (AppPrefs.get() != null
|
||||
&& AppPrefs.get().developerMode().getValue()
|
||||
&& AppPrefs.get().developerDisableUpdateVersionCheck().get()) {
|
||||
event("Bypassing version check");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!AppProperties.get().getVersion().equals(releaseVersion)) {
|
||||
event("Release has a different version");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public final void prepareUpdateAsync() {
|
||||
ThreadHelper.runAsync(() -> prepareUpdate());
|
||||
}
|
||||
|
||||
public final void refreshUpdateCheckAsync() {
|
||||
ThreadHelper.runAsync(() -> refreshUpdateCheckSilent());
|
||||
}
|
||||
|
||||
public final AvailableRelease refreshUpdateCheckSilent() {
|
||||
try {
|
||||
return refreshUpdateCheck();
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).omit().handle();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public final void prepareUpdate() {
|
||||
if (busy.getValue()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastUpdateCheckResult.getValue() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lastUpdateCheckResult.getValue().isUpdate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (var ignored = new BusyProperty(busy)) {
|
||||
event("Performing update download ...");
|
||||
prepareUpdateImpl();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract Region createInterface();
|
||||
|
||||
public void prepareUpdateImpl() {
|
||||
var changelogString =
|
||||
AppDownloads.downloadChangelog(lastUpdateCheckResult.getValue().getVersion(), false);
|
||||
var changelog = changelogString.orElse(null);
|
||||
|
||||
var rel = new PreparedUpdate(
|
||||
AppProperties.get().getVersion(),
|
||||
lastUpdateCheckResult.getValue().getVersion(),
|
||||
lastUpdateCheckResult.getValue().getReleaseUrl(),
|
||||
null,
|
||||
changelog,
|
||||
lastUpdateCheckResult.getValue().getAssetType());
|
||||
preparedUpdate.setValue(rel);
|
||||
}
|
||||
|
||||
public final void executeUpdateAndClose() {
|
||||
if (busy.getValue()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (preparedUpdate.getValue() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var downloadFile = preparedUpdate.getValue().getFile();
|
||||
if (!Files.exists(downloadFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event("Executing update ...");
|
||||
OperationMode.executeAfterShutdown(() -> {
|
||||
try {
|
||||
executeUpdateAndCloseImpl();
|
||||
} catch (Throwable ex) {
|
||||
ex.printStackTrace();
|
||||
} finally {
|
||||
var performedUpdate = new PerformedUpdate(
|
||||
preparedUpdate.getValue().getVersion(),
|
||||
preparedUpdate.getValue().getBody(),
|
||||
preparedUpdate.getValue().getVersion());
|
||||
AppCache.update("performedUpdate", performedUpdate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void executeUpdateAndCloseImpl() throws Exception {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public final AvailableRelease refreshUpdateCheck() throws Exception {
|
||||
if (busy.getValue()) {
|
||||
return lastUpdateCheckResult.getValue();
|
||||
}
|
||||
|
||||
try (var ignored = new BusyProperty(busy)) {
|
||||
return refreshUpdateCheckImpl();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract AvailableRelease refreshUpdateCheckImpl() throws Exception;
|
||||
|
||||
@Value
|
||||
@Builder
|
||||
@Jacksonized
|
||||
public static class PerformedUpdate {
|
||||
String name;
|
||||
String rawDescription;
|
||||
String newVersion;
|
||||
}
|
||||
|
||||
@Value
|
||||
@Builder
|
||||
@Jacksonized
|
||||
@With
|
||||
public static class AvailableRelease {
|
||||
String sourceVersion;
|
||||
String version;
|
||||
String releaseUrl;
|
||||
String downloadUrl;
|
||||
AppInstaller.InstallerAssetType assetType;
|
||||
Instant checkTime;
|
||||
boolean isUpdate;
|
||||
}
|
||||
|
||||
@Value
|
||||
@Builder
|
||||
@Jacksonized
|
||||
public static class PreparedUpdate {
|
||||
String sourceVersion;
|
||||
String version;
|
||||
String releaseUrl;
|
||||
Path file;
|
||||
String body;
|
||||
AppInstaller.InstallerAssetType assetType;
|
||||
}
|
||||
}
|
112
app/src/main/java/io/xpipe/app/update/XPipeDistributionType.java
Normal file
112
app/src/main/java/io/xpipe/app/update/XPipeDistributionType.java
Normal file
|
@ -0,0 +1,112 @@
|
|||
package io.xpipe.app.update;
|
||||
|
||||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.util.XPipeSession;
|
||||
import io.xpipe.core.impl.LocalStore;
|
||||
import io.xpipe.core.util.ModuleHelper;
|
||||
import io.xpipe.core.util.XPipeInstallation;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public enum XPipeDistributionType {
|
||||
DEVELOPMENT("development", () -> new GitHubUpdater(false)) {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "development";
|
||||
}
|
||||
},
|
||||
PORTABLE("portable", () -> new PortableUpdater()) {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "portable";
|
||||
}
|
||||
},
|
||||
INSTALLATION("install", () -> new GitHubUpdater(true)) {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "install";
|
||||
}
|
||||
},
|
||||
CHOCO("choco", () -> new ChocoUpdater()) {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "choco";
|
||||
}
|
||||
};
|
||||
|
||||
private static XPipeDistributionType type;
|
||||
|
||||
XPipeDistributionType(String id, Supplier<UpdateHandler> updateHandlerSupplier) {
|
||||
this.id = id;
|
||||
this.updateHandlerSupplier = updateHandlerSupplier;
|
||||
}
|
||||
|
||||
public static XPipeDistributionType get() {
|
||||
if (type != null) {
|
||||
return type;
|
||||
}
|
||||
|
||||
if (!ModuleHelper.isImage()) {
|
||||
return (type = DEVELOPMENT);
|
||||
}
|
||||
|
||||
if (!XPipeSession.get().isNewBuildSession()) {
|
||||
var cached = AppCache.get("dist", String.class, () -> null);
|
||||
var cachedType = Arrays.stream(values())
|
||||
.filter(xPipeDistributionType ->
|
||||
xPipeDistributionType.getId().equals(cached))
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
if (cachedType != null) {
|
||||
return (type = cachedType);
|
||||
}
|
||||
}
|
||||
|
||||
type = determine();
|
||||
AppCache.update("dist", type.getId());
|
||||
return type;
|
||||
}
|
||||
|
||||
public static XPipeDistributionType determine() {
|
||||
if (!XPipeInstallation.isInstallationDistribution()) {
|
||||
return (type = PORTABLE);
|
||||
}
|
||||
|
||||
try (var sc = LocalStore.getShell()) {
|
||||
try (var chocoOut = sc.command("choco search --local-only -r xpipe").start()) {
|
||||
var out = chocoOut.readStdoutDiscardErr();
|
||||
if (chocoOut.getExitCode() == 0) {
|
||||
var split = out.split("\\|");
|
||||
if (split.length == 2) {
|
||||
return CHOCO;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
}
|
||||
|
||||
return XPipeDistributionType.INSTALLATION;
|
||||
}
|
||||
|
||||
@Getter
|
||||
private final String id;
|
||||
private UpdateHandler updateHandler;
|
||||
private final Supplier<UpdateHandler> updateHandlerSupplier;
|
||||
|
||||
public UpdateHandler getUpdateHandler() {
|
||||
if (updateHandler == null) {
|
||||
updateHandler = updateHandlerSupplier.get();
|
||||
}
|
||||
return updateHandler;
|
||||
}
|
||||
|
||||
public abstract String getName();
|
||||
}
|
|
@ -21,6 +21,6 @@ public class TerminalHelper {
|
|||
throw new IllegalStateException(AppI18n.get("noTerminalSet"));
|
||||
}
|
||||
|
||||
type.launch(title, command);
|
||||
type.launch(title, command, false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
package io.xpipe.app.util;
|
||||
|
||||
import io.xpipe.core.util.ModuleHelper;
|
||||
import io.xpipe.core.util.XPipeInstallation;
|
||||
|
||||
public interface XPipeDistributionType {
|
||||
|
||||
XPipeDistributionType DEVELOPMENT = new XPipeDistributionType() {
|
||||
|
||||
@Override
|
||||
public boolean checkForUpdateOnStartup() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsUpdate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "development";
|
||||
}
|
||||
};
|
||||
XPipeDistributionType PORTABLE = new XPipeDistributionType() {
|
||||
|
||||
@Override
|
||||
public boolean checkForUpdateOnStartup() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "portable";
|
||||
}
|
||||
};
|
||||
XPipeDistributionType INSTALLATION = new XPipeDistributionType() {
|
||||
|
||||
@Override
|
||||
public boolean checkForUpdateOnStartup() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsUpdate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "install";
|
||||
}
|
||||
};
|
||||
|
||||
static XPipeDistributionType get() {
|
||||
if (!ModuleHelper.isImage()) {
|
||||
return DEVELOPMENT;
|
||||
}
|
||||
|
||||
if (XPipeInstallation.isInstallationDistribution()) {
|
||||
return INSTALLATION;
|
||||
} else {
|
||||
return PORTABLE;
|
||||
}
|
||||
}
|
||||
|
||||
boolean checkForUpdateOnStartup();
|
||||
|
||||
boolean supportsUpdate();
|
||||
|
||||
String getName();
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
package io.xpipe.core.util;
|
||||
package io.xpipe.app.util;
|
||||
|
||||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.app.core.AppProperties;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.util.UuidHelper;
|
||||
import lombok.Value;
|
||||
|
||||
import java.nio.file.Files;
|
||||
|
@ -13,6 +16,8 @@ public class XPipeSession {
|
|||
|
||||
boolean isNewSystemSession;
|
||||
|
||||
boolean isNewBuildSession;
|
||||
|
||||
/**
|
||||
* Unique identifier that resets on every X-Pipe restart.
|
||||
*/
|
||||
|
@ -61,7 +66,11 @@ public class XPipeSession {
|
|||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
INSTANCE = new XPipeSession(isNewSystemSession, UUID.randomUUID(), buildSessionId, systemSessionId);
|
||||
var s = AppCache.get("lastBuild", String.class, () -> buildSessionId.toString());
|
||||
var isBuildChanged = !buildSessionId.toString().equals(s);
|
||||
AppCache.update("lastBuild", AppProperties.get().getVersion());
|
||||
|
||||
INSTANCE = new XPipeSession(isNewSystemSession, isBuildChanged, UUID.randomUUID(), buildSessionId, systemSessionId);
|
||||
}
|
||||
|
||||
public static XPipeSession get() {
|
|
@ -16,7 +16,7 @@ saveWindowLocation=Save window location
|
|||
saveWindowLocationDescription=Controls whether the window coordinates should be saved and restored on restarts.
|
||||
startupShutdown=Startup / Shutdown
|
||||
system=System
|
||||
updateToPrereleases=Update to prereleases
|
||||
updateToPrereleases=Include prereleases
|
||||
updateToPrereleasesDescription=When enabled, the update check will also look for available prereleases in addition to full releases.
|
||||
storage=Storage
|
||||
runOnStartup=Run on startup
|
||||
|
@ -45,8 +45,8 @@ cancel=Cancel
|
|||
notAnAbsolutePath=Not an absolute path
|
||||
notADirectory=Not a directory
|
||||
notAnEmptyDirectory=Not an empty directory
|
||||
automaticallyUpdate=Automatically update
|
||||
automaticallyUpdateDescription=When enabled, new releases are automatically downloaded in the background while X-Pipe is running and installed on the next launch.
|
||||
automaticallyUpdate=Check for updates
|
||||
automaticallyUpdateDescription=When enabled, new releases are automatically fetched in the background while X-Pipe is running.
|
||||
sendAnonymousErrorReports=Send anonymous error reports
|
||||
sendUsageStatistics=Send anonymous usage statistics
|
||||
storageDirectory=Storage directory
|
||||
|
|
Loading…
Reference in a new issue