diff --git a/README.md b/README.md
index 4a2029422..5e7bd093e 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-
+
### A smart connection manager and remote file explorer
diff --git a/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java b/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java
index e7e856838..dd766e44c 100644
--- a/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java
@@ -42,8 +42,11 @@ public class AppLayoutComp extends Comp> {
private List 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> {
// 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);
diff --git a/app/src/main/java/io/xpipe/app/comp/about/UpdateCheckComp.java b/app/src/main/java/io/xpipe/app/comp/about/UpdateCheckComp.java
index e1594a125..0b69cbb37 100644
--- a/app/src/main/java/io/xpipe/app/comp/about/UpdateCheckComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/about/UpdateCheckComp.java
@@ -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 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 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();
diff --git a/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java b/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java
index 3aa662f89..65fe4164d 100644
--- a/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java
@@ -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> {
});
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);
}
diff --git a/app/src/main/java/io/xpipe/app/core/App.java b/app/src/main/java/io/xpipe/app/core/App.java
index 97b22c6f0..4d6f04426 100644
--- a/app/src/main/java/io/xpipe/app/core/App.java
+++ b/app/src/main/java/io/xpipe/app/core/App.java
@@ -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));
diff --git a/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java b/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java
index 49ebe5632..491882821 100644
--- a/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java
+++ b/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java
@@ -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");
}
diff --git a/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java b/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java
index e4522962a..52043962b 100644
--- a/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java
+++ b/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java
@@ -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;
diff --git a/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java b/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java
index d3228116a..67554c699 100644
--- a/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java
+++ b/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java
@@ -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;
diff --git a/app/src/main/java/io/xpipe/app/issue/TerminalErrorHandler.java b/app/src/main/java/io/xpipe/app/issue/TerminalErrorHandler.java
index c8f2878e9..893557954 100644
--- a/app/src/main/java/io/xpipe/app/issue/TerminalErrorHandler.java
+++ b/app/src/main/java/io/xpipe/app/issue/TerminalErrorHandler.java
@@ -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);
diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java
index d510c94d0..5ef078d87 100644
--- a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java
+++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java
@@ -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),
diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java
index d01a0a211..a28ed70c0 100644
--- a/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java
+++ b/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java
@@ -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);
diff --git a/app/src/main/java/io/xpipe/app/storage/StandardStorage.java b/app/src/main/java/io/xpipe/app/storage/StandardStorage.java
index 91544361e..88177ed95 100644
--- a/app/src/main/java/io/xpipe/app/storage/StandardStorage.java
+++ b/app/src/main/java/io/xpipe/app/storage/StandardStorage.java
@@ -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;
diff --git a/app/src/main/java/io/xpipe/app/test/DaemonExtensionTest.java b/app/src/main/java/io/xpipe/app/test/DaemonExtensionTest.java
index a96c496c4..20c9a7b3d 100644
--- a/app/src/main/java/io/xpipe/app/test/DaemonExtensionTest.java
+++ b/app/src/main/java/io/xpipe/app/test/DaemonExtensionTest.java
@@ -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;
diff --git a/app/src/main/java/io/xpipe/app/test/LocalExtensionTest.java b/app/src/main/java/io/xpipe/app/test/LocalExtensionTest.java
index cf9244663..022bfdf45 100644
--- a/app/src/main/java/io/xpipe/app/test/LocalExtensionTest.java
+++ b/app/src/main/java/io/xpipe/app/test/LocalExtensionTest.java
@@ -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;
diff --git a/app/src/main/java/io/xpipe/app/update/AppUpdater.java b/app/src/main/java/io/xpipe/app/update/AppUpdater.java
deleted file mode 100644
index f1bd13e4f..000000000
--- a/app/src/main/java/io/xpipe/app/update/AppUpdater.java
+++ /dev/null
@@ -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 lastUpdateCheckResult = new SimpleObjectProperty<>();
- private final Property 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;
- }
-}
diff --git a/app/src/main/java/io/xpipe/app/update/ChocoUpdater.java b/app/src/main/java/io/xpipe/app/update/ChocoUpdater.java
new file mode 100644
index 000000000..396e74f5d
--- /dev/null
+++ b/app/src/main/java/io/xpipe/app/update/ChocoUpdater.java
@@ -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();
+ }
+ }
+}
diff --git a/app/src/main/java/io/xpipe/app/update/GitHubUpdater.java b/app/src/main/java/io/xpipe/app/update/GitHubUpdater.java
new file mode 100644
index 000000000..fdfe28790
--- /dev/null
+++ b/app/src/main/java/io/xpipe/app/update/GitHubUpdater.java
@@ -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();
+ }
+}
diff --git a/app/src/main/java/io/xpipe/app/update/PortableUpdater.java b/app/src/main/java/io/xpipe/app/update/PortableUpdater.java
new file mode 100644
index 000000000..d74c882fd
--- /dev/null
+++ b/app/src/main/java/io/xpipe/app/update/PortableUpdater.java
@@ -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();
+ }
+}
diff --git a/app/src/main/java/io/xpipe/app/update/UpdateAvailableAlert.java b/app/src/main/java/io/xpipe/app/update/UpdateAvailableAlert.java
index 315d0dd60..1ffb187b2 100644
--- a/app/src/main/java/io/xpipe/app/update/UpdateAvailableAlert.java
+++ b/app/src/main/java/io/xpipe/app/update/UpdateAvailableAlert.java
@@ -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 = "" + AppI18n.get("whatsNew", u.getVersion()) + "
";
- return header + s;
- })
- .createRegion();
- alert.getDialogPane().setContent(markdown);
+ var markdown = new MarkdownComp(u.getBody() != null ? u.getBody() : "", s -> {
+ var header = "" + AppI18n.get("whatsNew", u.getVersion()) + "
";
+ 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();
}
}
}
diff --git a/app/src/main/java/io/xpipe/app/update/UpdateChangelogAlert.java b/app/src/main/java/io/xpipe/app/update/UpdateChangelogAlert.java
index c0e6f055b..de3691794 100644
--- a/app/src/main/java/io/xpipe/app/update/UpdateChangelogAlert.java
+++ b/app/src/main/java/io/xpipe/app/update/UpdateChangelogAlert.java
@@ -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;
}
diff --git a/app/src/main/java/io/xpipe/app/update/UpdateHandler.java b/app/src/main/java/io/xpipe/app/update/UpdateHandler.java
new file mode 100644
index 000000000..39681fd0d
--- /dev/null
+++ b/app/src/main/java/io/xpipe/app/update/UpdateHandler.java
@@ -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 lastUpdateCheckResult = new SimpleObjectProperty<>();
+ protected final Property 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;
+ }
+}
diff --git a/app/src/main/java/io/xpipe/app/update/XPipeDistributionType.java b/app/src/main/java/io/xpipe/app/update/XPipeDistributionType.java
new file mode 100644
index 000000000..ab950d782
--- /dev/null
+++ b/app/src/main/java/io/xpipe/app/update/XPipeDistributionType.java
@@ -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 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 updateHandlerSupplier;
+
+ public UpdateHandler getUpdateHandler() {
+ if (updateHandler == null) {
+ updateHandler = updateHandlerSupplier.get();
+ }
+ return updateHandler;
+ }
+
+ public abstract String getName();
+}
diff --git a/app/src/main/java/io/xpipe/app/util/TerminalHelper.java b/app/src/main/java/io/xpipe/app/util/TerminalHelper.java
index 8ecf2a968..53c971b3f 100644
--- a/app/src/main/java/io/xpipe/app/util/TerminalHelper.java
+++ b/app/src/main/java/io/xpipe/app/util/TerminalHelper.java
@@ -21,6 +21,6 @@ public class TerminalHelper {
throw new IllegalStateException(AppI18n.get("noTerminalSet"));
}
- type.launch(title, command);
+ type.launch(title, command, false);
}
}
diff --git a/app/src/main/java/io/xpipe/app/util/XPipeDistributionType.java b/app/src/main/java/io/xpipe/app/util/XPipeDistributionType.java
deleted file mode 100644
index 98a76e096..000000000
--- a/app/src/main/java/io/xpipe/app/util/XPipeDistributionType.java
+++ /dev/null
@@ -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();
-}
diff --git a/core/src/main/java/io/xpipe/core/util/XPipeSession.java b/app/src/main/java/io/xpipe/app/util/XPipeSession.java
similarity index 79%
rename from core/src/main/java/io/xpipe/core/util/XPipeSession.java
rename to app/src/main/java/io/xpipe/app/util/XPipeSession.java
index cf7118dca..783a1bd34 100644
--- a/core/src/main/java/io/xpipe/core/util/XPipeSession.java
+++ b/app/src/main/java/io/xpipe/app/util/XPipeSession.java
@@ -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() {
diff --git a/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties b/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties
index 72004a69f..5996dcf49 100644
--- a/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties
+++ b/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties
@@ -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