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.writeSysOut', "true"
systemProperty 'io.xpipe.app.developerMode', "true" systemProperty 'io.xpipe.app.developerMode', "true"
systemProperty 'io.xpipe.app.logLevel', "debug" 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.beacon.printMessages", "true"
systemProperty "io.xpipe.app.extensions", extensionDirList systemProperty "io.xpipe.app.extensions", extensionDirList
// systemProperty 'io.xpipe.app.debugPlatform', "true" // systemProperty 'io.xpipe.app.debugPlatform', "true"

View file

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

View file

@ -163,7 +163,7 @@ public class StoreEntryComp extends SimpleComp {
private Comp<?> createButtonBar() { private Comp<?> createButtonBar() {
var list = new ArrayList<Comp<?>>(); var list = new ArrayList<Comp<?>>();
for (var p : entry.getActionProviders().entrySet()) { for (var p : entry.getActionProviders().entrySet()) {
var actionProvider = p.getKey(); var actionProvider = p.getKey().getDataStoreCallSite();
if (!actionProvider.isMajor()) { if (!actionProvider.isMajor()) {
continue; continue;
} }
@ -171,7 +171,8 @@ public class StoreEntryComp extends SimpleComp {
var button = new IconButtonComp( var button = new IconButtonComp(
actionProvider.getIcon(entry.getEntry().getStore().asNeeded()), () -> { actionProvider.getIcon(entry.getEntry().getStore().asNeeded()), () -> {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
actionProvider.execute(entry.getEntry().getStore().asNeeded()); var action = actionProvider.createAction(entry.getEntry().getStore().asNeeded());
action.execute();
}); });
}); });
button.apply(new FancyTooltipAugment<>( button.apply(new FancyTooltipAugment<>(
@ -216,7 +217,7 @@ public class StoreEntryComp extends SimpleComp {
AppFont.normal(contextMenu.getStyleableNode()); AppFont.normal(contextMenu.getStyleableNode());
for (var p : entry.getActionProviders().entrySet()) { for (var p : entry.getActionProviders().entrySet()) {
var actionProvider = p.getKey(); var actionProvider = p.getKey().getDataStoreCallSite();
if (actionProvider.isMajor()) { if (actionProvider.isMajor()) {
continue; continue;
} }
@ -226,7 +227,8 @@ public class StoreEntryComp extends SimpleComp {
var item = new MenuItem(null, new FontIcon(icon)); var item = new MenuItem(null, new FontIcon(icon));
item.setOnAction(event -> { item.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
actionProvider.execute(entry.getEntry().getStore().asNeeded()); var action = actionProvider.createAction(entry.getEntry().getStore().asNeeded());
action.execute();
}); });
}); });
item.textProperty().bind(name); 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.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.extension.DataStoreActionProvider;
import io.xpipe.extension.event.ErrorEvent; import io.xpipe.extension.event.ErrorEvent;
import io.xpipe.extension.fxcomps.util.PlatformThread; import io.xpipe.extension.fxcomps.util.PlatformThread;
import io.xpipe.extension.util.ActionProvider;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.*; import javafx.beans.property.*;
import javafx.beans.value.ObservableBooleanValue; 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 Property<DataStoreEntry.State> state = new SimpleObjectProperty<>();
private final StringProperty information = new SimpleStringProperty(); private final StringProperty information = new SimpleStringProperty();
private final StringProperty summary = 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 editable = new SimpleBooleanProperty();
private final BooleanProperty renamable = new SimpleBooleanProperty(); private final BooleanProperty renamable = new SimpleBooleanProperty();
private final BooleanProperty refreshable = 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.name = new SimpleStringProperty(entry.getName());
this.lastAccess = new SimpleObjectProperty<>(entry.getLastAccess().minus(Duration.ofMillis(500))); this.lastAccess = new SimpleObjectProperty<>(entry.getLastAccess().minus(Duration.ofMillis(500)));
this.actionProviders = new LinkedHashMap<>(); this.actionProviders = new LinkedHashMap<>();
DataStoreActionProvider.ALL.stream() ActionProvider.ALL.stream()
.filter(dataStoreActionProvider -> { .filter(dataStoreActionProvider -> {
return !entry.isDisabled() return !entry.isDisabled()
&& dataStoreActionProvider && dataStoreActionProvider.getDataStoreCallSite() != null && dataStoreActionProvider.getDataStoreCallSite()
.getApplicableClass() .getApplicableClass()
.isAssignableFrom(entry.getStore().getClass()); .isAssignableFrom(entry.getStore().getClass());
}) })
@ -54,7 +54,7 @@ public class StoreEntryWrapper implements StorageFilter.Filterable {
return false; return false;
} }
return dataStoreActionProvider.isApplicable( return dataStoreActionProvider.getDataStoreCallSite().isApplicable(
entry.getStore().asNeeded()); entry.getStore().asNeeded());
}, },
disabledProperty(), disabledProperty(),

View file

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

View file

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

View file

@ -1,17 +1,11 @@
package io.xpipe.app.launcher; package io.xpipe.app.launcher;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.xpipe.app.comp.source.GuiDsCreatorMultiStep; 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.core.mode.OperationMode;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.impl.FileStore; 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.DataSourceProvider;
import io.xpipe.extension.event.ErrorEvent; import io.xpipe.extension.event.ErrorEvent;
import lombok.EqualsAndHashCode; import io.xpipe.extension.util.ActionProvider;
import lombok.Getter; import lombok.Getter;
import lombok.Value; import lombok.Value;
@ -22,12 +16,11 @@ import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.UUID;
public abstract class LauncherInput { public abstract class LauncherInput {
public static void handle(List<String> arguments) { public static void handle(List<String> arguments) {
var all = new ArrayList<LauncherInput>(); var all = new ArrayList<ActionProvider.Action>();
arguments.forEach(s -> { arguments.forEach(s -> {
try { try {
all.addAll(of(s)); all.addAll(of(s));
@ -55,11 +48,7 @@ public abstract class LauncherInput {
}); });
} }
public abstract void execute() throws Exception; public static List<ActionProvider.Action> of(String input) {
public abstract boolean requiresPlatform();
public static List<LauncherInput> of(String input) {
if (input.startsWith("\"") && input.endsWith("\"")) { if (input.startsWith("\"") && input.endsWith("\"")) {
input = input.substring(1, input.length() - 1); input = input.substring(1, input.length() - 1);
} }
@ -76,13 +65,22 @@ public abstract class LauncherInput {
if (scheme.equalsIgnoreCase("xpipe")) { if (scheme.equalsIgnoreCase("xpipe")) {
var action = uri.getAuthority(); var action = uri.getAuthority();
var args = Arrays.asList(uri.getPath().split("/")); var args = Arrays.asList(uri.getPath().split("/"));
var found = ActionProvider.ALL.stream()
var a = switch (action.toLowerCase()) { .filter(actionProvider -> actionProvider.getLauncherCallSite() != null
case "add" -> new AddActionInput(args); && actionProvider
default -> throw new IllegalStateException("Unexpected value: " + action); .getLauncherCallSite()
}; .getId()
.equalsIgnoreCase(action))
return List.of(a); .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) { } catch (IllegalArgumentException ignored) {
@ -96,13 +94,11 @@ public abstract class LauncherInput {
} catch (InvalidPathException ignored) { } catch (InvalidPathException ignored) {
} }
return List.of(); return List.of();
} }
@Value @Value
@EqualsAndHashCode(callSuper = true) public static class LocalFileInput implements ActionProvider.Action {
public static class LocalFileInput extends LauncherInput {
Path file; 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 @Getter
private final List<String> args; private final List<String> args;
@ -134,31 +130,4 @@ public abstract class LauncherInput {
this.args = args; 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.Group;
import com.dlsc.preferencesfx.model.Setting; import com.dlsc.preferencesfx.model.Setting;
import com.dlsc.preferencesfx.util.VisibilityProperty; 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.AppProperties;
import io.xpipe.app.core.AppStyle; import io.xpipe.app.core.AppStyle;
import io.xpipe.extension.event.ErrorEvent; import io.xpipe.extension.event.ErrorEvent;
@ -116,9 +116,9 @@ public class AppPrefs {
// Automatically update // Automatically update
// ==================== // ====================
private final BooleanProperty automaticallyUpdate = 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) private final BooleanField automaticallyUpdateField = BooleanField.ofBooleanType(automaticallyUpdate)
.editable(AppDistributionType.get().supportsUpdate()) .editable(XPipeDistributionType.get().supportsUpdate())
.render(() -> new ToggleControl()); .render(() -> new ToggleControl());
private final BooleanProperty updateToPrereleases = typed(new SimpleBooleanProperty(true), Boolean.class); private final BooleanProperty updateToPrereleases = typed(new SimpleBooleanProperty(true), Boolean.class);
private final BooleanProperty confirmDeletions = 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) { public static String getLocalDefaultInstallationBasePath(boolean acceptCustomHome) {
var customHome = System.getenv("XPIPE_HOME"); var customHome = System.getenv("XPIPE_HOME");
if (customHome != null && !customHome.isEmpty() && acceptCustomHome) { 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" from "$distDir/jpackage/xpiped"
into "$distDir/base/app" into "$distDir/base/app"
} }
copy {
from "$projectDir/logo/logo.ico"
into "$distDir/base/app"
}
file("$distDir/base/app/xpiped.exe").writable = true file("$distDir/base/app/xpiped.exe").writable = true
exec { exec {
@ -86,7 +90,12 @@ if (org.gradle.internal.os.OperatingSystem.current().isWindows()) {
from "$distDir/jpackage/xpiped" from "$distDir/jpackage/xpiped"
into "$distDir/base/app" into "$distDir/base/app"
} }
copy {
from "$projectDir/logo/logo.png"
into "$distDir/base/"
}
// Fixes a JPackage bug
copy { copy {
from "$distDir/base/app/lib/app/xpiped.cfg" from "$distDir/base/app/lib/app/xpiped.cfg"
into "$distDir/base/app" into "$distDir/base/app"
@ -135,6 +144,10 @@ if (org.gradle.internal.os.OperatingSystem.current().isWindows()) {
from "$distDir/jpackage/xpiped.app/Contents" from "$distDir/jpackage/xpiped.app/Contents"
into "$distDir/X-Pipe.app/Contents/" into "$distDir/X-Pipe.app/Contents/"
} }
copy {
from "$projectDir/logo/logo.icns"
into "$distDir/X-Pipe.app/Contents/Resources/"
}
copy { copy {
from "$distDir/cli/xpipe" from "$distDir/cli/xpipe"
into "$distDir/X-Pipe.app/Contents/MacOS/" 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.*;
import io.xpipe.ext.base.actions.FileBrowseAction; import io.xpipe.ext.base.actions.*;
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.apps.*; import io.xpipe.ext.base.apps.*;
import io.xpipe.extension.DataSourceProvider; import io.xpipe.extension.DataSourceProvider;
import io.xpipe.extension.DataStoreActionProvider; import io.xpipe.extension.DataStoreActionProvider;
import io.xpipe.extension.DataStoreProvider; import io.xpipe.extension.DataStoreProvider;
import io.xpipe.extension.DataSourceTarget; import io.xpipe.extension.DataSourceTarget;
import io.xpipe.extension.util.ActionProvider;
open module io.xpipe.ext.base { open module io.xpipe.ext.base {
exports 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 net.synedra.validatorfx;
requires static io.xpipe.app; requires static io.xpipe.app;
provides ActionProvider with AddStoreAction;
provides DataStoreActionProvider with provides DataStoreActionProvider with
StreamExportAction, StreamExportAction,
ShareStoreAction, ShareStoreAction,

View file

@ -6,6 +6,7 @@ import io.xpipe.core.util.JacksonMapper;
import io.xpipe.core.util.ProxyFunction; import io.xpipe.core.util.ProxyFunction;
import io.xpipe.extension.event.TrackEvent; import io.xpipe.extension.event.TrackEvent;
import io.xpipe.extension.prefs.PrefsProvider; import io.xpipe.extension.prefs.PrefsProvider;
import io.xpipe.extension.util.ActionProvider;
import io.xpipe.extension.util.ModuleLayerLoader; import io.xpipe.extension.util.ModuleLayerLoader;
import io.xpipe.extension.util.XPipeDaemon; import io.xpipe.extension.util.XPipeDaemon;
@ -37,7 +38,7 @@ public class XPipeServiceProviders {
ModuleLayerLoader.loadAll(layer, hasDaemon); ModuleLayerLoader.loadAll(layer, hasDaemon);
if (hasDaemon) { if (hasDaemon) {
DataStoreActionProvider.init(layer); ActionProvider.init(layer);
DataSourceActionProvider.init(layer); DataSourceActionProvider.init(layer);
ProxyFunction.init(layer); ProxyFunction.init(layer);
PrefsProvider.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.core.util.XPipeInstallation;
import io.xpipe.extension.event.TrackEvent; import io.xpipe.extension.event.TrackEvent;
public interface AppDistributionType { public interface XPipeDistributionType {
AppDistributionType DEVELOPMENT = new AppDistributionType() { XPipeDistributionType DEVELOPMENT = new XPipeDistributionType() {
@Override @Override
public boolean supportsUpdate() { public boolean supportsUpdate() {
@ -17,12 +19,18 @@ public interface AppDistributionType {
TrackEvent.info("Development mode update executed"); TrackEvent.info("Development mode update executed");
} }
@Override
public boolean supportsURLs() {
// Enabled for testing
return true;
}
@Override @Override
public String getName() { public String getName() {
return "development"; return "development";
} }
}; };
AppDistributionType PORTABLE = new AppDistributionType() { XPipeDistributionType PORTABLE = new XPipeDistributionType() {
@Override @Override
public boolean supportsUpdate() { public boolean supportsUpdate() {
@ -32,12 +40,17 @@ public interface AppDistributionType {
@Override @Override
public void performUpdateAction() {} public void performUpdateAction() {}
@Override
public boolean supportsURLs() {
return OsType.getLocal().equals(OsType.MAC);
}
@Override @Override
public String getName() { public String getName() {
return "portable"; return "portable";
} }
}; };
AppDistributionType INSTALLATION = new AppDistributionType() { XPipeDistributionType INSTALLATION = new XPipeDistributionType() {
@Override @Override
public boolean supportsUpdate() { public boolean supportsUpdate() {
@ -49,14 +62,19 @@ public interface AppDistributionType {
TrackEvent.info("Update action called"); TrackEvent.info("Update action called");
} }
@Override
public boolean supportsURLs() {
return true;
}
@Override @Override
public String getName() { public String getName() {
return "install"; return "install";
} }
}; };
static AppDistributionType get() { static XPipeDistributionType get() {
if (!AppProperties.get().isImage()) { if (!ModuleHelper.isImage()) {
return DEVELOPMENT; return DEVELOPMENT;
} }
@ -71,5 +89,7 @@ public interface AppDistributionType {
void performUpdateAction(); void performUpdateAction();
boolean supportsURLs();
String getName(); String getName();
} }

View file

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