diff --git a/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java b/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java index 77cd6df55..494fa0e41 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java @@ -1,14 +1,16 @@ package io.xpipe.app.comp.base; +import atlantafx.base.theme.Styles; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.binding.Bindings; import javafx.beans.value.ObservableValue; -import javafx.scene.layout.Pane; +import javafx.geometry.Pos; import javafx.scene.layout.Region; import org.kordamp.ikonli.javafx.FontIcon; +import org.kordamp.ikonli.javafx.StackedFontIcon; public class SystemStateComp extends SimpleComp { @@ -19,8 +21,8 @@ public class SystemStateComp extends SimpleComp { } public static enum State { - STOPPED, - RUNNING, + FAILURE, + SUCCESS, OTHER } @@ -29,14 +31,45 @@ public class SystemStateComp extends SimpleComp { @Override protected Region createSimple() { - var icon = PlatformThread.sync(Bindings.createStringBinding(() -> { - return state.getValue() == State.STOPPED ? "mdmz-stop_circle" : state.getValue() == State.RUNNING ? "mdrmz-play_circle_outline" : "mdmz-remove_circle_outline"; - }, state)); + var icon = PlatformThread.sync(Bindings.createStringBinding( + () -> { + return state.getValue() == State.FAILURE + ? "mdi2l-lightning-bolt" + : state.getValue() == State.SUCCESS ? "mdal-check" : "mdsmz-remove"; + }, + state)); var fi = new FontIcon(); + fi.getStyleClass().add("inner-icon"); SimpleChangeListener.apply(icon, val -> fi.setIconLiteral(val)); - new FancyTooltipAugment<>(PlatformThread.sync(name)).augment(fi); - var pane = new Pane(fi); + var border = new FontIcon("mdi2c-circle-outline"); + border.getStyleClass().add("outer-icon"); + border.setOpacity(0.5); + + var success = Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-success-emphasis; }"); + var failure = Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-danger-emphasis; }"); + var other = Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-accent-emphasis; }"); + + var pane = new StackedFontIcon(); + pane.getChildren().addAll(fi, border); + pane.setAlignment(Pos.CENTER); + + var dataClass1 = """ + .stacked-ikonli-font-icon > .outer-icon { + -fx-icon-size: 22px; + } + .stacked-ikonli-font-icon > .inner-icon { + -fx-icon-size: 12px; + } + """; + pane.getStylesheets().add(Styles.toDataURI(dataClass1)); + + SimpleChangeListener.apply(PlatformThread.sync(state), val -> { + pane.getStylesheets().removeAll(success, failure, other); + pane.getStylesheets().add(val == State.SUCCESS ? success : val == State.FAILURE ? failure: other); + }); + + new FancyTooltipAugment<>(PlatformThread.sync(name)).augment(pane); return pane; } } diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java index 215a066c7..2e6f3c5cf 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java @@ -98,9 +98,14 @@ public abstract class StoreEntryComp extends SimpleComp { protected Label createInformation() { var information = new Label(); + information.setGraphicTextGap(7); information.textProperty().bind(PlatformThread.sync(entry.getInformation())); information.getStyleClass().add("information"); AppFont.header(information); + + var state = entry.getEntry().getProvider().stateDisplay(entry); + information.setGraphic(state.createRegion()); + return information; } diff --git a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java index 11752c285..83585e677 100644 --- a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java @@ -1,12 +1,14 @@ package io.xpipe.app.ext; import io.xpipe.app.comp.base.MarkdownComp; +import io.xpipe.app.comp.base.SystemStateComp; import io.xpipe.app.comp.storage.store.StandardStoreEntryComp; import io.xpipe.app.comp.storage.store.StoreEntrySectionComp; import io.xpipe.app.comp.storage.store.StoreEntryWrapper; import io.xpipe.app.comp.storage.store.StoreSection; import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.core.dialog.Dialog; import io.xpipe.core.store.*; import io.xpipe.core.util.JacksonizedValue; @@ -40,6 +42,28 @@ public interface DataStoreProvider { return new StoreEntrySectionComp(section); } + default String failureInfo() { + return null; + } + + default Comp stateDisplay(StoreEntryWrapper w) { + var state = Bindings.createObjectBinding( + () -> { + return w.getState().getValue() == DataStoreEntry.State.COMPLETE_BUT_INVALID + ? SystemStateComp.State.FAILURE + : w.getState().getValue() == DataStoreEntry.State.COMPLETE_AND_VALID + ? SystemStateComp.State.SUCCESS + : SystemStateComp.State.OTHER; + }, + w.getState()); + var name = Bindings.createStringBinding( + () -> { + return w.getState().getValue() == DataStoreEntry.State.COMPLETE_BUT_INVALID ? "stop" : "start"; + }, + w.getState()); + return new SystemStateComp(name, state); + } + default Comp createInsightsComp(ObservableValue store) { var content = Bindings.createStringBinding( () -> { diff --git a/app/src/main/java/io/xpipe/app/storage/DataStateProviderImpl.java b/app/src/main/java/io/xpipe/app/storage/DataStateProviderImpl.java index d0c73c39f..d1de0897f 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStateProviderImpl.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStateProviderImpl.java @@ -4,6 +4,7 @@ import io.xpipe.core.store.DataStore; import io.xpipe.core.util.DataStateProvider; import java.nio.file.Path; +import java.util.Objects; import java.util.UUID; import java.util.function.Supplier; @@ -20,8 +21,10 @@ public class DataStateProviderImpl extends DataStateProvider { return; } - entry.get().getElementState().put(key, value); - entry.get().simpleRefresh(); + var old = entry.get().getElementState().put(key, value); + if (!Objects.equals(old, value)) { + entry.get().simpleRefresh(); + } } @Override diff --git a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java index 01fb435c2..fb722db48 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java @@ -97,6 +97,13 @@ public class DataStoreEntry extends StorageElement { State state, Configuration configuration, boolean expanded) { + + // The validation must be stuck if that happens + var stateToUse = state; + if (state == State.VALIDATING) { + stateToUse = State.COMPLETE_BUT_INVALID; + } + var entry = new DataStoreEntry( directory, uuid, @@ -106,7 +113,7 @@ public class DataStoreEntry extends StorageElement { information, storeNode, false, - state, + stateToUse, configuration, expanded); return entry; @@ -216,12 +223,6 @@ public class DataStoreEntry extends StorageElement { TODO: Implement singular change functions */ public void refresh(boolean deep) throws Exception { - // Assume that refresh can't be called while validating. - // Therefore the validation must be stuck if that happens - if (state == State.VALIDATING) { - state = State.COMPLETE_BUT_INVALID; - } - var oldStore = store; DataStore newStore = DataStorageParser.storeFromNode(storeNode); if (newStore == null diff --git a/core/src/main/java/io/xpipe/core/process/OsType.java b/core/src/main/java/io/xpipe/core/process/OsType.java index 365bb3881..58488424c 100644 --- a/core/src/main/java/io/xpipe/core/process/OsType.java +++ b/core/src/main/java/io/xpipe/core/process/OsType.java @@ -31,7 +31,14 @@ public sealed interface OsType permits OsType.Windows, OsType.Linux, OsType.MacO } default String getSystemIdFile(ShellControl pc) throws Exception { - return FileNames.join(getXPipeHomeDirectory(pc), "system_id"); + var home = getXPipeHomeDirectory(pc); + + // Sometimes the home variable is not set or empty + if (home == null || home.isBlank()) { + return null; + } + + return FileNames.join(home, "system_id"); } List determineInterestingPaths(ShellControl pc) throws Exception; diff --git a/core/src/main/java/io/xpipe/core/process/ShellDialect.java b/core/src/main/java/io/xpipe/core/process/ShellDialect.java index 6bad4e042..d374cc134 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellDialect.java +++ b/core/src/main/java/io/xpipe/core/process/ShellDialect.java @@ -25,6 +25,8 @@ public interface ShellDialect { .collect(Collectors.joining(" ")); } + CommandControl queryVersion(ShellControl shellControl); + CommandControl prepareTempDirectory(ShellControl shellControl, String directory); String initFileName(ShellControl sc) throws Exception; diff --git a/core/src/main/java/io/xpipe/core/util/XPipeSystemId.java b/core/src/main/java/io/xpipe/core/util/XPipeSystemId.java index ad3bade38..0e0d2dc9a 100644 --- a/core/src/main/java/io/xpipe/core/util/XPipeSystemId.java +++ b/core/src/main/java/io/xpipe/core/util/XPipeSystemId.java @@ -30,6 +30,9 @@ public class XPipeSystemId { public static UUID getSystemId(ShellControl proc) throws Exception { var file = proc.getOsType().getSystemIdFile(proc); + if (file == null) { + return UUID.randomUUID(); + } if (!proc.getShellDialect().createFileExistsCommand(proc, file).executeAndCheck()) { return writeRandom(proc, file);