diff --git a/README.md b/README.md index 4a2029422..5e7bd093e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -drawing +drawing ### 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