Implement more robust action system and desktop shortcuts

This commit is contained in:
crschnick 2023-02-05 15:04:18 +00:00
parent 6f78a357a6
commit 82ff5c7e0f
17 changed files with 331 additions and 87 deletions

View file

@ -136,7 +136,7 @@ run {
systemProperty 'io.xpipe.app.writeSysOut', "true"
systemProperty 'io.xpipe.app.developerMode', "true"
systemProperty 'io.xpipe.app.logLevel', "debug"
systemProperty "io.xpipe.beacon.port", "21724"
// systemProperty "io.xpipe.beacon.port", "21724"
// systemProperty "io.xpipe.beacon.printMessages", "true"
systemProperty "io.xpipe.app.extensions", extensionDirList
// systemProperty 'io.xpipe.app.debugPlatform', "true"

View file

@ -1,6 +1,6 @@
package io.xpipe.app.comp.about;
import io.xpipe.app.core.AppDistributionType;
import io.xpipe.extension.util.XPipeDistributionType;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.grid.AppUpdater;
import io.xpipe.app.util.Hyperlinks;
@ -101,7 +101,7 @@ public class UpdateCheckComp extends SimpleComp {
}
if (updateAvailable.getValue()) {
return AppDistributionType.get().supportsUpdate()
return XPipeDistributionType.get().supportsUpdate()
? I18n.get("downloadUpdate")
: I18n.get("checkOutUpdate");
} else {
@ -134,7 +134,7 @@ public class UpdateCheckComp extends SimpleComp {
return;
}
if (updateAvailable.getValue() && !AppDistributionType.get().supportsUpdate()) {
if (updateAvailable.getValue() && !XPipeDistributionType.get().supportsUpdate()) {
Hyperlinks.open(
AppUpdater.get().getLastUpdateCheckResult().getValue().getReleaseUrl());
} else if (updateAvailable.getValue()) {

View file

@ -163,7 +163,7 @@ public class StoreEntryComp extends SimpleComp {
private Comp<?> createButtonBar() {
var list = new ArrayList<Comp<?>>();
for (var p : entry.getActionProviders().entrySet()) {
var actionProvider = p.getKey();
var actionProvider = p.getKey().getDataStoreCallSite();
if (!actionProvider.isMajor()) {
continue;
}
@ -171,7 +171,8 @@ public class StoreEntryComp extends SimpleComp {
var button = new IconButtonComp(
actionProvider.getIcon(entry.getEntry().getStore().asNeeded()), () -> {
ThreadHelper.runFailableAsync(() -> {
actionProvider.execute(entry.getEntry().getStore().asNeeded());
var action = actionProvider.createAction(entry.getEntry().getStore().asNeeded());
action.execute();
});
});
button.apply(new FancyTooltipAugment<>(
@ -216,7 +217,7 @@ public class StoreEntryComp extends SimpleComp {
AppFont.normal(contextMenu.getStyleableNode());
for (var p : entry.getActionProviders().entrySet()) {
var actionProvider = p.getKey();
var actionProvider = p.getKey().getDataStoreCallSite();
if (actionProvider.isMajor()) {
continue;
}
@ -226,7 +227,8 @@ public class StoreEntryComp extends SimpleComp {
var item = new MenuItem(null, new FontIcon(icon));
item.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
actionProvider.execute(entry.getEntry().getStore().asNeeded());
var action = actionProvider.createAction(entry.getEntry().getStore().asNeeded());
action.execute();
});
});
item.textProperty().bind(name);

View file

@ -5,9 +5,9 @@ import io.xpipe.app.comp.storage.StorageFilter;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.extension.DataStoreActionProvider;
import io.xpipe.extension.event.ErrorEvent;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import io.xpipe.extension.util.ActionProvider;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableBooleanValue;
@ -29,7 +29,7 @@ public class StoreEntryWrapper implements StorageFilter.Filterable {
private final Property<DataStoreEntry.State> state = new SimpleObjectProperty<>();
private final StringProperty information = new SimpleStringProperty();
private final StringProperty summary = new SimpleStringProperty();
private final Map<DataStoreActionProvider<?>, ObservableBooleanValue> actionProviders;
private final Map<ActionProvider, ObservableBooleanValue> actionProviders;
private final BooleanProperty editable = new SimpleBooleanProperty();
private final BooleanProperty renamable = new SimpleBooleanProperty();
private final BooleanProperty refreshable = new SimpleBooleanProperty();
@ -40,10 +40,10 @@ public class StoreEntryWrapper implements StorageFilter.Filterable {
this.name = new SimpleStringProperty(entry.getName());
this.lastAccess = new SimpleObjectProperty<>(entry.getLastAccess().minus(Duration.ofMillis(500)));
this.actionProviders = new LinkedHashMap<>();
DataStoreActionProvider.ALL.stream()
ActionProvider.ALL.stream()
.filter(dataStoreActionProvider -> {
return !entry.isDisabled()
&& dataStoreActionProvider
&& dataStoreActionProvider.getDataStoreCallSite() != null && dataStoreActionProvider.getDataStoreCallSite()
.getApplicableClass()
.isAssignableFrom(entry.getStore().getClass());
})
@ -54,7 +54,7 @@ public class StoreEntryWrapper implements StorageFilter.Filterable {
return false;
}
return dataStoreActionProvider.isApplicable(
return dataStoreActionProvider.getDataStoreCallSite().isApplicable(
entry.getStore().asNeeded());
},
disabledProperty(),

View file

@ -1,7 +1,7 @@
package io.xpipe.app.grid;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppDistributionType;
import io.xpipe.extension.util.XPipeDistributionType;
import io.xpipe.app.core.AppExtensionManager;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.mode.OperationMode;
@ -54,7 +54,7 @@ public class AppUpdater {
.equals(AppProperties.get().getVersion())) {
downloadedUpdate.setValue(null);
}
if (!AppDistributionType.get().supportsUpdate()) {
if (!XPipeDistributionType.get().supportsUpdate()) {
downloadedUpdate.setValue(null);
}
@ -124,8 +124,8 @@ public class AppUpdater {
INSTANCE = new AppUpdater();
if (AppDistributionType.get().supportsUpdate()
&& AppDistributionType.get() != AppDistributionType.DEVELOPMENT) {
if (XPipeDistributionType.get().supportsUpdate()
&& XPipeDistributionType.get() != XPipeDistributionType.DEVELOPMENT) {
ThreadHelper.create("updater", true, () -> {
ThreadHelper.sleep(Duration.ofMinutes(10).toMillis());
event("Starting background updater thread");
@ -182,7 +182,7 @@ public class AppUpdater {
return;
}
if (!AppDistributionType.get().supportsUpdate()) {
if (!XPipeDistributionType.get().supportsUpdate()) {
return;
}

View file

@ -4,7 +4,7 @@ import io.sentry.*;
import io.sentry.protocol.SentryId;
import io.sentry.protocol.User;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppDistributionType;
import io.xpipe.extension.util.XPipeDistributionType;
import io.xpipe.app.core.AppProperties;
import io.xpipe.extension.event.ErrorEvent;
import io.xpipe.extension.event.TrackEvent;
@ -22,7 +22,7 @@ public class SentryErrorHandler {
options.setEnableUncaughtExceptionHandler(false);
options.setAttachServerName(false);
// options.setDebug(true);
options.setDist(AppDistributionType.get().getName());
options.setDist(XPipeDistributionType.get().getName());
options.setRelease(AppProperties.get().getVersion());
options.setEnableShutdownHook(false);
options.setProguardUuid(AppProperties.get().getBuildUuid().toString());

View file

@ -1,17 +1,11 @@
package io.xpipe.app.launcher;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.xpipe.app.comp.source.GuiDsCreatorMultiStep;
import io.xpipe.app.comp.source.store.GuiDsStoreCreator;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.impl.FileStore;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.JacksonMapper;
import io.xpipe.core.util.SecretValue;
import io.xpipe.extension.DataSourceProvider;
import io.xpipe.extension.event.ErrorEvent;
import lombok.EqualsAndHashCode;
import io.xpipe.extension.util.ActionProvider;
import lombok.Getter;
import lombok.Value;
@ -22,12 +16,11 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
public abstract class LauncherInput {
public static void handle(List<String> arguments) {
var all = new ArrayList<LauncherInput>();
var all = new ArrayList<ActionProvider.Action>();
arguments.forEach(s -> {
try {
all.addAll(of(s));
@ -55,11 +48,7 @@ public abstract class LauncherInput {
});
}
public abstract void execute() throws Exception;
public abstract boolean requiresPlatform();
public static List<LauncherInput> of(String input) {
public static List<ActionProvider.Action> of(String input) {
if (input.startsWith("\"") && input.endsWith("\"")) {
input = input.substring(1, input.length() - 1);
}
@ -76,13 +65,22 @@ public abstract class LauncherInput {
if (scheme.equalsIgnoreCase("xpipe")) {
var action = uri.getAuthority();
var args = Arrays.asList(uri.getPath().split("/"));
var a = switch (action.toLowerCase()) {
case "add" -> new AddActionInput(args);
default -> throw new IllegalStateException("Unexpected value: " + action);
};
return List.of(a);
var found = ActionProvider.ALL.stream()
.filter(actionProvider -> actionProvider.getLauncherCallSite() != null
&& actionProvider
.getLauncherCallSite()
.getId()
.equalsIgnoreCase(action))
.findFirst();
if (found.isPresent()) {
ActionProvider.Action a = null;
try {
a = found.get().getLauncherCallSite().createAction(args);
} catch (Exception e) {
return List.of();
}
return List.of(a);
}
}
}
} catch (IllegalArgumentException ignored) {
@ -96,13 +94,11 @@ public abstract class LauncherInput {
} catch (InvalidPathException ignored) {
}
return List.of();
}
@Value
@EqualsAndHashCode(callSuper = true)
public static class LocalFileInput extends LauncherInput {
public static class LocalFileInput implements ActionProvider.Action {
Path file;
@ -125,7 +121,7 @@ public abstract class LauncherInput {
}
}
public static abstract class ActionInput extends LauncherInput {
public abstract static class ActionInput extends LauncherInput {
@Getter
private final List<String> args;
@ -134,31 +130,4 @@ public abstract class LauncherInput {
this.args = args;
}
}
@Value
@EqualsAndHashCode(callSuper = true)
public static class AddActionInput extends ActionInput {
public AddActionInput(List<String> args) {
super(args);
}
@Override
public void execute() throws JsonProcessingException {
var type = getArgs().get(1);
var storeString = SecretValue.ofSecret(getArgs().get(2));
var store = JacksonMapper.parse(storeString.getSecretValue(), DataStore.class);
if (store == null) {
return;
}
var entry = DataStoreEntry.createNew(UUID.randomUUID(),"", store);
GuiDsStoreCreator.showEdit(entry);
}
@Override
public boolean requiresPlatform() {
return true;
}
}
}

View file

@ -8,7 +8,7 @@ import com.dlsc.preferencesfx.model.Category;
import com.dlsc.preferencesfx.model.Group;
import com.dlsc.preferencesfx.model.Setting;
import com.dlsc.preferencesfx.util.VisibilityProperty;
import io.xpipe.app.core.AppDistributionType;
import io.xpipe.extension.util.XPipeDistributionType;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.AppStyle;
import io.xpipe.extension.event.ErrorEvent;
@ -116,9 +116,9 @@ public class AppPrefs {
// Automatically update
// ====================
private final BooleanProperty automaticallyUpdate =
typed(new SimpleBooleanProperty(AppDistributionType.get().supportsUpdate()), Boolean.class);
typed(new SimpleBooleanProperty(XPipeDistributionType.get().supportsUpdate()), Boolean.class);
private final BooleanField automaticallyUpdateField = BooleanField.ofBooleanType(automaticallyUpdate)
.editable(AppDistributionType.get().supportsUpdate())
.editable(XPipeDistributionType.get().supportsUpdate())
.render(() -> new ToggleControl());
private final BooleanProperty updateToPrereleases = typed(new SimpleBooleanProperty(true), Boolean.class);
private final BooleanProperty confirmDeletions = typed(new SimpleBooleanProperty(true), Boolean.class);

View file

@ -177,6 +177,31 @@ public class XPipeInstallation {
}
}
public static Path getLocalDefaultInstallationIcon() {
Path path = getLocalInstallationBasePath();
// Check for development environment
if (!ModuleHelper.isImage()) {
if (OsType.getLocal().equals(OsType.WINDOWS)) {
return path.resolve("dist").resolve("logo").resolve("logo.ico");
} else if (OsType.getLocal().equals(OsType.LINUX)) {
return path.resolve("dist").resolve("logo").resolve("logo.png");
} else {
return path.resolve("dist").resolve("logo").resolve("logo.icns");
}
}
if (OsType.getLocal().equals(OsType.WINDOWS)) {
return path.resolve("app").resolve("logo.ico");
} else if (OsType.getLocal().equals(OsType.LINUX)) {
return path.resolve("logo.png");
} else {
return path.resolve("Contents")
.resolve("Resources")
.resolve("logo.icns");
}
}
public static String getLocalDefaultInstallationBasePath(boolean acceptCustomHome) {
var customHome = System.getenv("XPIPE_HOME");
if (customHome != null && !customHome.isEmpty() && acceptCustomHome) {

13
dist/base.gradle vendored
View file

@ -22,6 +22,10 @@ if (org.gradle.internal.os.OperatingSystem.current().isWindows()) {
from "$distDir/jpackage/xpiped"
into "$distDir/base/app"
}
copy {
from "$projectDir/logo/logo.ico"
into "$distDir/base/app"
}
file("$distDir/base/app/xpiped.exe").writable = true
exec {
@ -86,7 +90,12 @@ if (org.gradle.internal.os.OperatingSystem.current().isWindows()) {
from "$distDir/jpackage/xpiped"
into "$distDir/base/app"
}
copy {
from "$projectDir/logo/logo.png"
into "$distDir/base/"
}
// Fixes a JPackage bug
copy {
from "$distDir/base/app/lib/app/xpiped.cfg"
into "$distDir/base/app"
@ -135,6 +144,10 @@ if (org.gradle.internal.os.OperatingSystem.current().isWindows()) {
from "$distDir/jpackage/xpiped.app/Contents"
into "$distDir/X-Pipe.app/Contents/"
}
copy {
from "$projectDir/logo/logo.icns"
into "$distDir/X-Pipe.app/Contents/Resources/"
}
copy {
from "$distDir/cli/xpipe"
into "$distDir/X-Pipe.app/Contents/MacOS/"

View file

@ -0,0 +1,51 @@
package io.xpipe.ext.base.actions;
import io.xpipe.app.comp.source.store.GuiDsStoreCreator;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.JacksonMapper;
import io.xpipe.core.util.SecretValue;
import io.xpipe.extension.util.ActionProvider;
import lombok.Value;
import java.util.List;
import java.util.UUID;
public class AddStoreAction implements ActionProvider {
@Value
static class Action implements ActionProvider.Action {
DataStore store;
@Override
public boolean requiresPlatform() {
return true;
}
@Override
public void execute() throws Exception {
if (store == null) {
return;
}
var entry = DataStoreEntry.createNew(UUID.randomUUID(), "", store);
GuiDsStoreCreator.showEdit(entry);
}
}
@Override
public LauncherCallSite getLauncherCallSite() {
return new LauncherCallSite() {
@Override
public String getId() {
return "addStore";
}
@Override
public Action createAction(List<String> args) throws Exception {
var storeString = SecretValue.ofSecret(args.get(1));
var store = JacksonMapper.parse(storeString.getSecretValue(), DataStore.class);
return new Action(store);
}
};
}
}

View file

@ -1,13 +1,11 @@
import io.xpipe.ext.base.*;
import io.xpipe.ext.base.actions.FileBrowseAction;
import io.xpipe.ext.base.actions.FileEditAction;
import io.xpipe.ext.base.actions.ShareStoreAction;
import io.xpipe.ext.base.actions.StreamExportAction;
import io.xpipe.ext.base.actions.*;
import io.xpipe.ext.base.apps.*;
import io.xpipe.extension.DataSourceProvider;
import io.xpipe.extension.DataStoreActionProvider;
import io.xpipe.extension.DataStoreProvider;
import io.xpipe.extension.DataSourceTarget;
import io.xpipe.extension.util.ActionProvider;
open module io.xpipe.ext.base {
exports io.xpipe.ext.base;
@ -24,6 +22,7 @@ open module io.xpipe.ext.base {
requires static net.synedra.validatorfx;
requires static io.xpipe.app;
provides ActionProvider with AddStoreAction;
provides DataStoreActionProvider with
StreamExportAction,
ShareStoreAction,

View file

@ -6,6 +6,7 @@ import io.xpipe.core.util.JacksonMapper;
import io.xpipe.core.util.ProxyFunction;
import io.xpipe.extension.event.TrackEvent;
import io.xpipe.extension.prefs.PrefsProvider;
import io.xpipe.extension.util.ActionProvider;
import io.xpipe.extension.util.ModuleLayerLoader;
import io.xpipe.extension.util.XPipeDaemon;
@ -37,7 +38,7 @@ public class XPipeServiceProviders {
ModuleLayerLoader.loadAll(layer, hasDaemon);
if (hasDaemon) {
DataStoreActionProvider.init(layer);
ActionProvider.init(layer);
DataSourceActionProvider.init(layer);
ProxyFunction.init(layer);
PrefsProvider.init(layer);

View file

@ -0,0 +1,79 @@
package io.xpipe.extension.util;
import io.xpipe.core.store.DataStore;
import io.xpipe.extension.event.ErrorEvent;
import javafx.beans.value.ObservableValue;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
public interface ActionProvider {
static List<ActionProvider> ALL = new ArrayList<>();
public static void init(ModuleLayer layer) {
if (ALL.size() == 0) {
ALL.addAll(ServiceLoader.load(layer, ActionProvider.class).stream()
.map(p -> (ActionProvider) p.get())
.filter(provider -> {
try {
return provider.isActive();
} catch (Throwable e) {
ErrorEvent.fromThrowable(e).handle();
return false;
}
})
.toList());
}
}
interface Action {
boolean requiresPlatform();
void execute() throws Exception;
}
default boolean isActive() throws Exception {
return true;
}
interface LauncherCallSite {
String getId();
Action createAction(List<String> args) throws Exception;
}
default LauncherCallSite getLauncherCallSite() {
return null;
}
default DataStoreCallSite<?> getDataStoreCallSite() {
return null;
}
public static interface DataStoreCallSite<T extends DataStore> {
Action createAction(T store);
Class<T> getApplicableClass();
default boolean isMajor() {
return false;
}
default boolean isApplicable(T o) throws Exception {
return true;
}
ObservableValue<String> getName(T store);
String getIcon(T store);
default boolean showIfDisabled() {
return true;
}
}
}

View file

@ -0,0 +1,83 @@
package io.xpipe.extension.util;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.ShellStore;
import io.xpipe.core.util.XPipeInstallation;
import java.nio.file.Files;
import java.nio.file.Path;
public class DesktopShortcuts {
private static void createWindowsShortcut(String target, String name) throws Exception {
var icon = XPipeInstallation.getLocalDefaultInstallationIcon();
var content = String.format(
"""
set "TARGET=%s"
set "SHORTCUT=%%HOMEDRIVE%%%%HOMEPATH%%\\Desktop\\%s.lnk"
set PWS=powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile
%%PWS%% -Command "$ws = New-Object -ComObject WScript.Shell; $s = $ws.CreateShortcut('%%SHORTCUT%%'); $S.IconLocation='%s'; $S.TargetPath = '%%TARGET%%'; $S.Save()"
""",
target, name, icon.toString());
ShellStore.local().create().executeSimpleCommand(content);
}
private static void createLinuxShortcut(String target, String name) throws Exception {
var icon = XPipeInstallation.getLocalDefaultInstallationIcon();
var content = String.format(
"""
[Desktop Entry]
Type=Application
Name=%s
Comment=Open with X-Pipe
TryExec=/opt/xpipe/app/bin/xpiped
Exec=/opt/xpipe/cli/bin/xpipe open %s
Icon=%s
Terminal=false
Categories=Utility;Development;Office;
""",
name, target, icon.toString());
var file = Path.of("~/Desktop/" + name + ".desktop").toRealPath();
Files.writeString(file, content);
file.toFile().setExecutable(true);
}
private static void createMacOSShortcut(String target, String name) throws Exception {
var icon = XPipeInstallation.getLocalDefaultInstallationIcon();
var content = String.format(
"""
#!/bin/bash
open %s
""",
target);
var file = Path.of("~/Desktop/" + name + ".command").toRealPath();
Files.writeString(file, content);
file.toFile().setExecutable(true);
var iconScriptContent = String.format(
"""
iconSource="%s"
iconDestination="%s"
icon=/tmp/`basename $iconSource`
rsrc=/tmp/icon.rsrc
cp $iconSource $icon
sips -i $icon
DeRez -only icns $icon > $rsrc
SetFile -a C $iconDestination
Rez -append $rsrc -o $iconDestination
""",
icon, target);
ShellStore.local().create().executeSimpleCommand(iconScriptContent);
}
public static void create(String target, String name) throws Exception {
if (OsType.getLocal().equals(OsType.WINDOWS)) {
createWindowsShortcut(target, name);
} else if (OsType.getLocal().equals(OsType.LINUX)) {
createLinuxShortcut(target, name);
} else {
createMacOSShortcut(target, name);
}
}
}

View file

@ -1,11 +1,13 @@
package io.xpipe.app.core;
package io.xpipe.extension.util;
import io.xpipe.core.process.OsType;
import io.xpipe.core.util.ModuleHelper;
import io.xpipe.core.util.XPipeInstallation;
import io.xpipe.extension.event.TrackEvent;
public interface AppDistributionType {
public interface XPipeDistributionType {
AppDistributionType DEVELOPMENT = new AppDistributionType() {
XPipeDistributionType DEVELOPMENT = new XPipeDistributionType() {
@Override
public boolean supportsUpdate() {
@ -17,12 +19,18 @@ public interface AppDistributionType {
TrackEvent.info("Development mode update executed");
}
@Override
public boolean supportsURLs() {
// Enabled for testing
return true;
}
@Override
public String getName() {
return "development";
}
};
AppDistributionType PORTABLE = new AppDistributionType() {
XPipeDistributionType PORTABLE = new XPipeDistributionType() {
@Override
public boolean supportsUpdate() {
@ -32,12 +40,17 @@ public interface AppDistributionType {
@Override
public void performUpdateAction() {}
@Override
public boolean supportsURLs() {
return OsType.getLocal().equals(OsType.MAC);
}
@Override
public String getName() {
return "portable";
}
};
AppDistributionType INSTALLATION = new AppDistributionType() {
XPipeDistributionType INSTALLATION = new XPipeDistributionType() {
@Override
public boolean supportsUpdate() {
@ -49,14 +62,19 @@ public interface AppDistributionType {
TrackEvent.info("Update action called");
}
@Override
public boolean supportsURLs() {
return true;
}
@Override
public String getName() {
return "install";
}
};
static AppDistributionType get() {
if (!AppProperties.get().isImage()) {
static XPipeDistributionType get() {
if (!ModuleHelper.isImage()) {
return DEVELOPMENT;
}
@ -71,5 +89,7 @@ public interface AppDistributionType {
void performUpdateAction();
boolean supportsURLs();
String getName();
}

View file

@ -2,6 +2,7 @@ import io.xpipe.core.util.ProxyFunction;
import io.xpipe.extension.DataSourceProvider;
import io.xpipe.extension.DataStoreActionProvider;
import io.xpipe.extension.DataSourceTarget;
import io.xpipe.extension.util.ActionProvider;
import io.xpipe.extension.util.ModuleLayerLoader;
import io.xpipe.extension.util.XPipeDaemon;
@ -52,6 +53,7 @@ open module io.xpipe.extension {
uses io.xpipe.extension.Cache;
uses io.xpipe.extension.DataSourceActionProvider;
uses ProxyFunction;
uses ActionProvider;
uses io.xpipe.extension.util.ModuleLayerLoader;
provides ModuleLayerLoader with DataSourceTarget.Loader;