diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserComp.java index 0cb75c85a..35be1d8d0 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserComp.java @@ -14,6 +14,7 @@ import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.SimpleChangeListener; +import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.ThreadHelper; import javafx.application.Platform; @@ -262,13 +263,11 @@ public class BrowserComp extends SimpleComp { PlatformThread.sync(model.getBusy()))); tab.setText(model.getName()); - // new FancyTooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(tab); tab.setContent(new OpenFileSystemComp(model).createSimple()); var id = UUID.randomUUID().toString(); tab.setId(id); - var found = tabs.lookupAll("tab-header-area"); SimpleChangeListener.apply(tabs.skinProperty(), newValue -> { if (newValue != null) { Platform.runLater(() -> { @@ -281,6 +280,11 @@ public class BrowserComp extends SimpleComp { close.setPrefWidth(30); StackPane c = (StackPane) tabs.lookup("#" + id + " .tab-container"); + c.getStyleClass().add("color-box"); + var color = DataStorage.get().getRootForEntry(model.getEntry().get()).getColor(); + if (color != null) { + c.getStyleClass().add(color.getId()); + } new FancyTooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(c); c.addEventHandler( DragEvent.DRAG_ENTERED, diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java index 48c6ef9e2..1cd9efe75 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java @@ -68,7 +68,7 @@ public class BrowserWelcomeComp extends SimpleComp { var view = PrettyImageHelper.ofFixedSquare(graphic, 45); view.padding(new Insets(2, 8, 2, 8)); var tile = new Tile( - DataStorage.get().getStoreBrowserDisplayName(entry.get()), + DataStorage.get().getStoreDisplayName(entry.get()), e.getPath(), view.createRegion()); tile.setActionHandler(() -> { diff --git a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java index 1c83a1795..65e56c611 100644 --- a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java +++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java @@ -45,7 +45,7 @@ public final class OpenFileSystemModel { public OpenFileSystemModel(BrowserModel browserModel, DataStoreEntryRef entry) { this.browserModel = browserModel; this.entry = entry; - this.name = DataStorage.get().getStoreBrowserDisplayName(entry.get()); + this.name = DataStorage.get().getStoreDisplayName(entry.get()); this.tooltip = DataStorage.get().getId(entry.getEntry()).toString(); this.inOverview.bind(Bindings.createBooleanBinding( () -> { @@ -363,7 +363,7 @@ public final class OpenFileSystemModel { fs.open(); this.fileSystem = fs; this.local = - fs.getShell().map(shellControl -> shellControl.isLocal()).orElse(false); + fs.getShell().map(shellControl -> shellControl.hasLocalSystemAccess()).orElse(false); this.cache.init(); }); } 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 78fea6c7d..447a5e02e 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 @@ -17,6 +17,7 @@ import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.update.XPipeDistributionType; import io.xpipe.app.util.DataStoreFormatter; @@ -355,7 +356,7 @@ public abstract class StoreEntryComp extends SimpleComp { contextMenu.getItems().add(move); } - { + if (DataStorage.get().isRootEntry(wrapper.getEntry())) { var color = new Menu(AppI18n.get("color"), new FontIcon("mdi2f-format-color-fill")); var none = new MenuItem("None"); none.setOnAction(event -> { diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSection.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSection.java index e4f3c6dcc..04abdee11 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSection.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSection.java @@ -108,18 +108,11 @@ public class StoreSection { var topLevel = BindingsHelper.filteredContentBinding( ordered, section -> { - var noParent = DataStorage.get() - .getDisplayParent(section.getWrapper().getEntry()) - .isEmpty(); var sameCategory = category.getValue().contains(section.getWrapper().getEntry()); - var diffParentCategory = DataStorage.get() - .getDisplayParent(section.getWrapper().getEntry()) - .map(entry -> !category.getValue().contains(entry)) - .orElse(false); var showFilter = section.shouldShow(filterString.get()); var matchesSelector = section.anyMatches(entryFilter); - return (noParent || diffParentCategory) && showFilter && sameCategory && matchesSelector; + return DataStorage.get().isRootEntry(section.getWrapper().getEntry()) && showFilter && sameCategory && matchesSelector; }, category, filterString); diff --git a/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java b/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java index b064b34fa..09f08ddda 100644 --- a/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java +++ b/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java @@ -6,7 +6,7 @@ import io.xpipe.app.comp.DeveloperTabComp; import io.xpipe.app.comp.storage.store.StoreLayoutComp; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.prefs.PrefsComp; -import io.xpipe.app.util.FeatureProvider; +import io.xpipe.app.util.LicenseProvider; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; @@ -70,7 +70,7 @@ public class AppLayoutModel { l.add(new Entry( AppI18n.observable("explorePlans"), "mdi2p-professional-hexagon", - FeatureProvider.get().overviewPage())); + LicenseProvider.get().overviewPage())); return l; } diff --git a/app/src/main/java/io/xpipe/app/core/AppMainWindow.java b/app/src/main/java/io/xpipe/app/core/AppMainWindow.java index 704d763ce..ce5813f85 100644 --- a/app/src/main/java/io/xpipe/app/core/AppMainWindow.java +++ b/app/src/main/java/io/xpipe/app/core/AppMainWindow.java @@ -244,7 +244,7 @@ public class AppMainWindow { var contentR = content.createRegion(); contentR.requestFocus(); stage.getScene().setRoot(contentR); - AppTheme.initTheme(stage); + AppTheme.initThemeHandlers(stage); TrackEvent.debug("Set content scene"); contentR.prefWidthProperty().bind(stage.getScene().widthProperty()); @@ -263,7 +263,7 @@ public class AppMainWindow { if (AppProperties.get().isDeveloperMode() && event.getCode().equals(KeyCode.F6)) { var newR = content.createRegion(); stage.getScene().setRoot(newR); - AppTheme.initTheme(stage); + AppTheme.initThemeHandlers(stage); newR.requestFocus(); TrackEvent.debug("Rebuilt content"); diff --git a/app/src/main/java/io/xpipe/app/core/AppStyle.java b/app/src/main/java/io/xpipe/app/core/AppStyle.java index 7cd5cd518..194448fe0 100644 --- a/app/src/main/java/io/xpipe/app/core/AppStyle.java +++ b/app/src/main/java/io/xpipe/app/core/AppStyle.java @@ -15,7 +15,7 @@ import java.util.*; public class AppStyle { - private static final Map STYLESHEET_CONTENTS = new HashMap<>(); + private static final Map STYLESHEET_CONTENTS = new LinkedHashMap<>(); private static final List scenes = new ArrayList<>(); private static String FONT_CONTENTS = ""; diff --git a/app/src/main/java/io/xpipe/app/core/AppTheme.java b/app/src/main/java/io/xpipe/app/core/AppTheme.java index 70554b791..c4fd4e8d9 100644 --- a/app/src/main/java/io/xpipe/app/core/AppTheme.java +++ b/app/src/main/java/io/xpipe/app/core/AppTheme.java @@ -35,17 +35,18 @@ public class AppTheme { private static final PseudoClass PRETTY = PseudoClass.getPseudoClass("pretty"); private static final PseudoClass PERFORMANCE = PseudoClass.getPseudoClass("performance"); - public static void initTheme(Window stage) { - var t = AppPrefs.get().theme.getValue(); - if (t == null) { - return; - } + public static void initThemeHandlers(Window stage) { + SimpleChangeListener.apply(AppPrefs.get().theme, t -> { + Theme.ALL.forEach(theme -> stage.getScene().getRoot().getStyleClass().remove(theme.getCssId())); + if (t == null) { + return; + } - Theme.ALL.forEach(theme -> stage.getScene().getRoot().getStyleClass().remove(theme.getCssId())); - stage.getScene().getRoot().getStyleClass().add(t.getCssId()); + stage.getScene().getRoot().getStyleClass().add(t.getCssId()); + stage.getScene().getRoot().pseudoClassStateChanged(LIGHT, !t.isDark()); + stage.getScene().getRoot().pseudoClassStateChanged(DARK, t.isDark()); + }); - stage.getScene().getRoot().pseudoClassStateChanged(LIGHT, !t.isDark()); - stage.getScene().getRoot().pseudoClassStateChanged(DARK, t.isDark()); SimpleChangeListener.apply(AppPrefs.get().performanceMode(),val -> { stage.getScene().getRoot().pseudoClassStateChanged(PRETTY, !val); stage.getScene().getRoot().pseudoClassStateChanged(PERFORMANCE, val); @@ -106,7 +107,6 @@ public class AppTheme { for (Window window : Window.getWindows()) { var scene = window.getScene(); Image snapshot = scene.snapshot(null); - initTheme(window); Pane root = (Pane) scene.getRoot(); ImageView imageView = new ImageView(snapshot); diff --git a/app/src/main/java/io/xpipe/app/core/AppWindowHelper.java b/app/src/main/java/io/xpipe/app/core/AppWindowHelper.java index 1db6d1fc8..cbcd5a31d 100644 --- a/app/src/main/java/io/xpipe/app/core/AppWindowHelper.java +++ b/app/src/main/java/io/xpipe/app/core/AppWindowHelper.java @@ -62,6 +62,7 @@ public class AppWindowHelper { stage.setTitle(title); addIcons(stage); setupContent(stage, contentFunc, bindSize, loading); + AppTheme.initThemeHandlers(stage); setupStylesheets(stage.getScene()); stage.setOnShown(e -> { 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 d1cb53018..228cbbeda 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 @@ -6,7 +6,7 @@ import io.xpipe.app.core.*; import io.xpipe.app.issue.*; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; -import io.xpipe.app.util.FeatureProvider; +import io.xpipe.app.util.LicenseProvider; import io.xpipe.app.util.FileBridge; import io.xpipe.app.util.LockedSecretValue; import io.xpipe.core.store.LocalStore; @@ -41,7 +41,7 @@ public class BaseMode extends OperationMode { // Load translations before storage initialization to localize store error messages // Also loaded before antivirus alert to localize that AppI18n.init(); - FeatureProvider.get().init(); + LicenseProvider.get().init(); AppAntivirusAlert.showIfNeeded(); LocalStore.init(); AppPrefs.init(); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java index f9c00208b..215c1e20d 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java @@ -94,7 +94,7 @@ public class StoreCategoryComp extends SimpleComp { })); h.apply(new ContextMenuAugment<>( mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY, () -> createContextMenu(name))); - h.padding(new Insets(0, 10, 0, (category.getDepth() * 8))); + h.padding(new Insets(0, 10, 0, (category.getDepth() * 10))); h.styleClass("category-button"); var l = category.getChildren() .sorted(Comparator.comparing( @@ -102,7 +102,7 @@ public class StoreCategoryComp extends SimpleComp { var children = new ListBoxViewComp<>(l, l, storeCategoryWrapper -> new StoreCategoryComp(storeCategoryWrapper)); var emptyBinding = Bindings.isEmpty(category.getChildren()); - var v = new VerticalComp(List.of(h, Comp.separator().hide(emptyBinding), children.hide(emptyBinding))); + var v = new VerticalComp(List.of(h, Comp.separator().hide(emptyBinding), Comp.vspacer(5).hide(emptyBinding), children.hide(emptyBinding))); v.styleClass("category"); v.apply(struc -> { SimpleChangeListener.apply(StoreViewState.get().getActiveCategory(), val -> { diff --git a/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java b/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java index 91e8db597..bcb7204f0 100644 --- a/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java +++ b/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java @@ -10,7 +10,7 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.util.JfxHelper; -import io.xpipe.app.util.LicenseException; +import io.xpipe.app.util.LicenseRequiredException; import io.xpipe.app.util.PlatformState; import javafx.application.Platform; import javafx.beans.property.Property; @@ -194,7 +194,7 @@ public class ErrorHandlerComp extends SimpleComp { actionBox.getStyleClass().add("actions"); actionBox.setFillWidth(true); - if (event.getThrowable() instanceof LicenseException) { + if (event.getThrowable() instanceof LicenseRequiredException) { event.getCustomActions().add(new ErrorAction() { @Override public String getName() { diff --git a/app/src/main/java/io/xpipe/app/issue/GuiErrorHandler.java b/app/src/main/java/io/xpipe/app/issue/GuiErrorHandler.java index cffe65633..ec8fc6e47 100644 --- a/app/src/main/java/io/xpipe/app/issue/GuiErrorHandler.java +++ b/app/src/main/java/io/xpipe/app/issue/GuiErrorHandler.java @@ -1,6 +1,8 @@ package io.xpipe.app.issue; import io.xpipe.app.core.mode.OperationMode; +import io.xpipe.app.util.LicenseProvider; +import io.xpipe.app.util.LicenseRequiredException; public class GuiErrorHandler extends GuiErrorHandlerBase implements ErrorHandler { @@ -26,6 +28,11 @@ public class GuiErrorHandler extends GuiErrorHandlerBase implements ErrorHandler return; } - ErrorHandlerComp.showAndTryWait(event, true); + + if (event.getThrowable() instanceof LicenseRequiredException lex) { + LicenseProvider.get().showLicenseAlert(lex); + } else { + ErrorHandlerComp.showAndTryWait(event, true); + } } } diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorage.java b/app/src/main/java/io/xpipe/app/storage/DataStorage.java index 5b3c706d0..98022acbe 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -200,6 +200,34 @@ public abstract class DataStorage { saveAsync(); } + public boolean isRootEntry(DataStoreEntry entry) { + var noParent = DataStorage.get() + .getDisplayParent(entry) + .isEmpty(); + var diffParentCategory = DataStorage.get() + .getDisplayParent(entry) + .map(p -> !p.getCategoryUuid().equals(entry.getCategoryUuid())) + .orElse(false); + return noParent || diffParentCategory; + } + + public DataStoreEntry getRootForEntry(DataStoreEntry entry) { + if (isRootEntry(entry)) { + return entry; + } + + var current = entry; + Optional parent; + while ((parent = getDisplayParent(current)).isPresent()) { + current = parent.get(); + if (isRootEntry(current)) { + break; + } + } + + return current; + } + public Optional getDisplayParent(DataStoreEntry entry) { if (entry.getValidity() == DataStoreEntry.Validity.LOAD_FAILED) { return Optional.empty(); @@ -555,7 +583,7 @@ public abstract class DataStorage { return findEntry(store).map(dataStoreEntry -> dataStoreEntry.getName()); } - public String getStoreBrowserDisplayName(DataStoreEntry store) { + public String getStoreDisplayName(DataStoreEntry store) { if (store == null) { return "?"; } diff --git a/app/src/main/java/io/xpipe/app/storage/GitStorageHandler.java b/app/src/main/java/io/xpipe/app/storage/GitStorageHandler.java index d2f4ebcaf..987c606ae 100644 --- a/app/src/main/java/io/xpipe/app/storage/GitStorageHandler.java +++ b/app/src/main/java/io/xpipe/app/storage/GitStorageHandler.java @@ -1,13 +1,13 @@ package io.xpipe.app.storage; -import io.xpipe.app.util.FeatureProvider; +import io.xpipe.app.util.LicenseProvider; import java.nio.file.Path; public interface GitStorageHandler { static GitStorageHandler getInstance() { - return FeatureProvider.get().createStorageHandler(); + return LicenseProvider.get().createStorageHandler(); } boolean supportsShare(); 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 8869481de..56cb2e495 100644 --- a/app/src/main/java/io/xpipe/app/storage/StandardStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/StandardStorage.java @@ -4,7 +4,7 @@ import io.xpipe.app.comp.storage.store.StoreSortMode; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.prefs.AppPrefs; -import io.xpipe.app.util.FeatureProvider; +import io.xpipe.app.util.LicenseProvider; import io.xpipe.app.util.XPipeSession; import io.xpipe.core.store.LocalStore; import lombok.Getter; @@ -28,7 +28,7 @@ public class StandardStorage extends DataStorage { private final GitStorageHandler gitStorageHandler; StandardStorage() { - this.gitStorageHandler = FeatureProvider.get().createStorageHandler(); + this.gitStorageHandler = LicenseProvider.get().createStorageHandler(); this.gitStorageHandler.init(dir); } @@ -257,6 +257,7 @@ public class StandardStorage extends DataStorage { e.setDirectory(getStoresDir().resolve(LOCAL_ID.toString())); e.setConfiguration( StorageElement.Configuration.builder().deletable(false).build()); + e.setColor(DataStoreColor.BLUE); storeEntries.add(e); e.validate(); } diff --git a/app/src/main/java/io/xpipe/app/update/AppInstaller.java b/app/src/main/java/io/xpipe/app/update/AppInstaller.java index 60902d590..3260f66bc 100644 --- a/app/src/main/java/io/xpipe/app/update/AppInstaller.java +++ b/app/src/main/java/io/xpipe/app/update/AppInstaller.java @@ -38,7 +38,7 @@ public class AppInstaller { public static void installFile(ShellControl s, InstallerAssetType asset, Path localFile) throws Exception { String targetFile; - if (s.isLocal()) { + if (s.hasLocalSystemAccess()) { targetFile = localFile.toString(); } else { targetFile = FileNames.join( diff --git a/app/src/main/java/io/xpipe/app/util/LicenseException.java b/app/src/main/java/io/xpipe/app/util/LicenseException.java deleted file mode 100644 index 3c6f5fb8d..000000000 --- a/app/src/main/java/io/xpipe/app/util/LicenseException.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.xpipe.app.util; - -public class LicenseException extends RuntimeException { - - public LicenseException() { - } - - public LicenseException(String message) { - super(message); - } - - public LicenseException(String message, Throwable cause) { - super(message, cause); - } - - public LicenseException(Throwable cause) { - super(cause); - } - - public LicenseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } - - public LicenseException(String featureName, LicenseType min) { - this(featureName + " are only supported with a " + min.name().toLowerCase() + " license"); - } -} diff --git a/app/src/main/java/io/xpipe/app/util/FeatureProvider.java b/app/src/main/java/io/xpipe/app/util/LicenseProvider.java similarity index 70% rename from app/src/main/java/io/xpipe/app/util/FeatureProvider.java rename to app/src/main/java/io/xpipe/app/util/LicenseProvider.java index 462df882a..dbc6fb10c 100644 --- a/app/src/main/java/io/xpipe/app/util/FeatureProvider.java +++ b/app/src/main/java/io/xpipe/app/util/LicenseProvider.java @@ -2,15 +2,16 @@ package io.xpipe.app.util; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.storage.GitStorageHandler; +import io.xpipe.core.process.ShellControl; import io.xpipe.core.util.ModuleLayerLoader; import java.util.ServiceLoader; -public abstract class FeatureProvider { +public abstract class LicenseProvider { - private static FeatureProvider INSTANCE = null; + private static LicenseProvider INSTANCE = null; - public static FeatureProvider get() { + public static LicenseProvider get() { return INSTANCE; } @@ -18,7 +19,7 @@ public abstract class FeatureProvider { @Override public void init(ModuleLayer layer) { - INSTANCE = ServiceLoader.load(layer, FeatureProvider.class).stream() + INSTANCE = ServiceLoader.load(layer, LicenseProvider.class).stream() .map(ServiceLoader.Provider::get) .findFirst().orElseThrow(); } @@ -34,6 +35,10 @@ public abstract class FeatureProvider { } } + public abstract void handleShellControl(ShellControl sc); + + public abstract void showLicenseAlert(LicenseRequiredException ex); + public abstract LicenseType getLicenseType(); public abstract void init(); diff --git a/app/src/main/java/io/xpipe/app/util/LicenseRequiredException.java b/app/src/main/java/io/xpipe/app/util/LicenseRequiredException.java new file mode 100644 index 000000000..cdf77c38c --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/LicenseRequiredException.java @@ -0,0 +1,20 @@ +package io.xpipe.app.util; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@EqualsAndHashCode(callSuper = true) +@Value +public class LicenseRequiredException extends RuntimeException { + + String featureName; + boolean plural; + LicenseType minLicense; + + public LicenseRequiredException(String featureName, boolean plural, LicenseType minLicense) { + super(featureName + " are only supported with a " + minLicense.name().toLowerCase() + " license"); + this.featureName = featureName; + this.plural = plural; + this.minLicense = minLicense; + } +} 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 8f5ae7dc3..bb18174bf 100644 --- a/app/src/main/java/io/xpipe/app/util/TerminalHelper.java +++ b/app/src/main/java/io/xpipe/app/util/TerminalHelper.java @@ -4,6 +4,7 @@ import io.xpipe.app.core.AppI18n; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.ExternalTerminalType; +import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.core.process.ProcessControl; @@ -22,12 +23,13 @@ public class TerminalHelper { throw ErrorEvent.unreportable(new IllegalStateException(AppI18n.get("noTerminalSet"))); } - var prefix = entry != null && entry.getColor() != null && type.supportsColoredTitle() - ? entry.getColor().getEmoji() + " " + var color = DataStorage.get().getRootForEntry(entry).getColor(); + var prefix = entry != null && color != null && type.supportsColoredTitle() + ? color.getEmoji() + " " : ""; var fixedTitle = prefix + (title != null ? title : entry != null ? entry.getName() : "?"); var file = ScriptHelper.createLocalExecScript(cc.prepareTerminalOpen(fixedTitle)); - var config = new ExternalTerminalType.LaunchConfiguration(entry != null ? entry.getColor() : null, title, file); + var config = new ExternalTerminalType.LaunchConfiguration(entry != null ? color : null, title, file); try { type.launch(config); } catch (Exception ex) { diff --git a/app/src/main/java/module-info.java b/app/src/main/java/module-info.java index 166b839c2..b80623bb4 100644 --- a/app/src/main/java/module-info.java +++ b/app/src/main/java/module-info.java @@ -8,7 +8,7 @@ import io.xpipe.app.issue.EventHandler; import io.xpipe.app.issue.EventHandlerImpl; import io.xpipe.app.storage.DataStateProviderImpl; import io.xpipe.app.storage.StorageJacksonModule; -import io.xpipe.app.util.FeatureProvider; +import io.xpipe.app.util.LicenseProvider; import io.xpipe.app.util.ProxyManagerProviderImpl; import io.xpipe.app.util.TerminalHelper; import io.xpipe.core.util.DataStateProvider; @@ -119,14 +119,14 @@ open module io.xpipe.app { uses ModuleLayerLoader; uses ScanProvider; uses BrowserAction; - uses io.xpipe.app.util.FeatureProvider; + uses LicenseProvider; provides Module with StorageJacksonModule; provides ModuleLayerLoader with ActionProvider.Loader, PrefsProvider.Loader, BrowserAction.Loader, - FeatureProvider.Loader, + LicenseProvider.Loader, ScanProvider.Loader; provides DataStateProvider with DataStateProviderImpl; diff --git a/app/src/main/resources/io/xpipe/app/resources/style/color-box.css b/app/src/main/resources/io/xpipe/app/resources/style/color-box.css new file mode 100644 index 000000000..f73012d9a --- /dev/null +++ b/app/src/main/resources/io/xpipe/app/resources/style/color-box.css @@ -0,0 +1,71 @@ +.root:light .color-box.blue { + -fx-background-color: linear-gradient(from 100% 0% to 0% 100% , rgb(130, 130, 250, 0.2) 40%, rgb(57, 57, 200, 0.2) 50%, rgb(137, 137, 250, 0.2) 100%); + -fx-border-color: rgba(80, 100, 150, 0.3); +} + +.root:light .color-box.blue > .separator .line { + -fx-border-color: rgba(80, 100, 150, 0.4); +} + +.root:dark .color-box.blue { + -fx-background-color: linear-gradient(from 100% 0% to 0% 100% , rgb(30, 30, 80, 0.8) 40%, rgb(27, 27, 65, 0.8) 50%, rgb(37, 37, 100, 0.8) 100%); + -fx-border-color: rgba(80, 100, 150, 0.7); +} + +.root:dark .color-box.blue > .separator .line { + -fx-border-color: rgba(80, 100, 150, 0.7); +} + +.root:light .color-box.red { + -fx-background-color: linear-gradient(from 100% 0% to 0% 100% , rgb(220, 100, 100, 0.15) 40%, rgb(205, 50, 50, 0.15) 50%, rgb(200, 90, 90, 0.15) 100%); + -fx-border-color: rgba(150, 100, 80, 0.4); +} + +.root:light .color-box.red > .separator .line { + -fx-border-color: rgba(150, 100, 80, 0.4); +} + +.root:dark .color-box.red { + -fx-background-color: linear-gradient(from 100% 0% to 0% 100% , rgb(80, 30, 30, 0.4) 40%, rgb(65, 27, 27, 0.4) 50%, rgb(100, 37, 37, 0.4) 100%); + -fx-border-color: rgba(150, 100, 80, 0.4); +} + +.root:dark .color-box.red > .separator .line { + -fx-border-color: rgba(150, 100, 80, 0.4); +} + +.root:light .color-box.yellow { + -fx-background-color: linear-gradient(from 100% 0% to 0% 100% , rgb(180, 180, 30, 0.2) 40%, rgb(135, 135, 27, 0.2) 50%, rgb(200, 200, 37, 0.2) 100%); + -fx-border-color: rgba(170, 170, 80, 0.3); +} + +.root:light .color-box.yellow > .separator .line { + -fx-border-color: rgba(170, 170, 80, 0.5); +} + +.root:dark .color-box.yellow { + -fx-background-color: linear-gradient(from 100% 0% to 0% 100% , rgb(80, 80, 30, 0.4) 40%, rgb(65, 65, 27, 0.4) 50%, rgb(100, 100, 37, 0.4) 100%); + -fx-border-color: rgba(150, 150, 80, 0.4); +} + +.root:dark .color-box.yellow > .separator .line { + -fx-border-color: rgba(170, 170, 80, 0.3); +} + +.root:light .color-box.green { + -fx-background-color: linear-gradient(from 100% 0% to 0% 100% , rgb(30, 180, 30, 0.1) 40%, rgb(20, 120, 20, 0.15) 50%, rgb(37, 200, 37, 0.1) 100%); + -fx-border-color: rgba(100, 150, 80, 0.2); +} + +.root:light .color-box.green > .separator .line { + -fx-border-color: rgba(100, 150, 80, 0.4); +} + +.root:dark .color-box.green { + -fx-background-color: linear-gradient(from 100% 0% to 0% 100% , rgb(30, 80, 30, 0.3) 40%, rgb(20, 60, 20, 0.3) 50%, rgb(37, 100, 37, 0.3) 100%); + -fx-border-color: rgba(100, 190, 80, 0.3); +} + +.root:dark .color-box.green > .separator .line { + -fx-border-color: rgba(100, 190, 80, 0.2); +} \ No newline at end of file diff --git a/app/src/main/resources/io/xpipe/app/resources/style/license-required-alert.css b/app/src/main/resources/io/xpipe/app/resources/style/license-required-alert.css new file mode 100644 index 000000000..cab94511f --- /dev/null +++ b/app/src/main/resources/io/xpipe/app/resources/style/license-required-alert.css @@ -0,0 +1,11 @@ +.license-required { + -fx-padding: 1em; + -fx-font-family: Roboto; +} + +.license-required .message { +-fx-border-width: 1px; +-fx-border-radius: 15px; +-fx-background-radius: 15px; +-fx-padding: 1em; +} diff --git a/core/src/main/java/io/xpipe/core/process/ShellControl.java b/core/src/main/java/io/xpipe/core/process/ShellControl.java index 7eb89c30f..24549d77a 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellControl.java +++ b/core/src/main/java/io/xpipe/core/process/ShellControl.java @@ -15,10 +15,16 @@ import java.util.function.Predicate; public interface ShellControl extends ProcessControl { - default boolean isLocal() { + default boolean hasLocalSystemAccess() { return getSystemId().equals(XPipeSystemId.getLocal()); } + boolean isLocal(); + + ShellControl changesHosts(); + + ShellControl getMachineRootSession(); + String getOsName(); UUID getSystemId(); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchAction.java index 3696f09c7..2431dcc18 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchAction.java @@ -25,7 +25,7 @@ public class LaunchAction implements ActionProvider { @Override public void execute() throws Exception { - var storeName = entry.getName(); + var storeName = DataStorage.get().getStoreDisplayName(entry); if (entry.getStore() instanceof ShellStore s) { TerminalHelper.open(entry, storeName, ScriptStore.controlWithDefaultScripts(s.control())); return;