diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStore.java b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStore.java index ba94cbf97..c8e1bbcf5 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStore.java @@ -25,7 +25,7 @@ public abstract class AbstractServiceStore implements SingletonSessionStore { var desc = s.getLocalPort() != null - ? "localhost:" + s.getLocalPort() + " <- " + s.getRemotePort() + ? "localhost:" + s.getLocalPort() + " <- :" + s.getRemotePort() : s.isSessionRunning() - ? "localhost:" + s.getSession().getLocalPort() + " <- " + s.getRemotePort() + ? "localhost:" + s.getSession().getLocalPort() + " <- :" + s.getRemotePort() : AppI18n.get("remotePort", s.getRemotePort()); + var type = s.getServiceProtocolType() != null && !(s.getServiceProtocolType() instanceof ServiceProtocolType.None) ? + AppI18n.get(s.getServiceProtocolType().getTranslationKey()) : null; var state = !s.requiresTunnel() ? null : s.isSessionRunning() ? AppI18n.get("active") : s.isSessionEnabled() ? AppI18n.get("starting") : AppI18n.get("inactive"); - return new ShellStoreFormat(null, desc, state).format(); + return new ShellStoreFormat(null, desc, type, state).format(); }, section.getWrapper().getCache(), AppPrefs.get().language()); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStoreProvider.java index e5831275c..04ee2b2fb 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStoreProvider.java @@ -11,7 +11,6 @@ import io.xpipe.core.store.NetworkTunnelStore; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; import java.util.List; @@ -31,10 +30,9 @@ public class CustomServiceStoreProvider extends AbstractServiceStoreProvider { public GuiDialog guiDialog(DataStoreEntry entry, Property store) { CustomServiceStore st = store.getValue().asNeeded(); var host = new SimpleObjectProperty<>(st.getHost()); - var path = new SimpleStringProperty(st.getPath()); var localPort = new SimpleObjectProperty<>(st.getLocalPort()); var remotePort = new SimpleObjectProperty<>(st.getRemotePort()); - + var serviceProtocolType = new SimpleObjectProperty<>(st.getServiceProtocolType()); var q = new OptionsBuilder() .nameAndDescription("serviceHost") .addComp( @@ -50,15 +48,15 @@ public class CustomServiceStoreProvider extends AbstractServiceStoreProvider { .nonNull() .nameAndDescription("serviceLocalPort") .addInteger(localPort) - .nameAndDescription("servicePath") - .addString(path) + .sub(ServiceProtocolTypeHelper.choice(serviceProtocolType), serviceProtocolType) + .nonNull() .bind( () -> { return CustomServiceStore.builder() .host(host.get()) .localPort(localPort.get()) .remotePort(remotePort.get()) - .path(path.get()) + .serviceProtocolType(serviceProtocolType.get()) .build(); }, store); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStoreProvider.java index d08963968..2eb6c2469 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStoreProvider.java @@ -1,9 +1,17 @@ package io.xpipe.ext.base.service; +import io.xpipe.app.comp.store.StoreChoiceComp; import io.xpipe.app.comp.store.StoreSection; +import io.xpipe.app.comp.store.StoreViewState; +import io.xpipe.app.ext.GuiDialog; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.util.OptionsBuilder; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.NetworkTunnelStore; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; @@ -38,4 +46,39 @@ public class FixedServiceStoreProvider extends AbstractServiceStoreProvider { public List> getStoreClasses() { return List.of(FixedServiceStore.class); } + + @Override + public GuiDialog guiDialog(DataStoreEntry entry, Property store) { + FixedServiceStore st = store.getValue().asNeeded(); + var host = new SimpleObjectProperty<>(st.getHost()); + var localPort = new SimpleObjectProperty<>(st.getLocalPort()); + var serviceProtocolType = new SimpleObjectProperty<>(st.getServiceProtocolType()); + var q = new OptionsBuilder() + .nameAndDescription("serviceHost") + .addComp( + StoreChoiceComp.other( + host, + NetworkTunnelStore.class, + n -> n.getStore().isLocallyTunnelable(), + StoreViewState.get().getAllConnectionsCategory()), + host) + .nonNull() + .nameAndDescription("serviceLocalPort") + .addInteger(localPort) + .sub(ServiceProtocolTypeHelper.choice(serviceProtocolType), serviceProtocolType) + .nonNull() + .bind( + () -> { + return FixedServiceStore.builder() + .host(host.get()) + .displayParent(st.getDisplayParent()) + .localPort(localPort.get()) + .remotePort(st.getRemotePort()) + .serviceProtocolType(serviceProtocolType.get()) + .build(); + }, + store); + return q.buildDialog(); + } + } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceCopyUrlAction.java b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceCopyUrlAction.java index 58405379c..1b14bf136 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceCopyUrlAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceCopyUrlAction.java @@ -55,8 +55,10 @@ public class ServiceCopyUrlAction implements ActionProvider { @Override public void execute() throws Exception { serviceStore.startSessionIfNeeded(); - var l = serviceStore.getSession().getLocalPort(); - ClipboardHelper.copyUrl("localhost:" + l); + var l = serviceStore.requiresTunnel() ? serviceStore.getSession().getLocalPort() : serviceStore.getRemotePort(); + var base = "localhost:" + l; + var full = serviceStore.getServiceProtocolType().formatUrl(base); + ClipboardHelper.copyUrl(full); } } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceOpenAction.java b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceOpenAction.java index 2d44023ad..4de4167f7 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceOpenAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceOpenAction.java @@ -15,129 +15,32 @@ import java.util.List; public class ServiceOpenAction implements ActionProvider { @Override - public BranchDataStoreCallSite getBranchDataStoreCallSite() { - return new BranchDataStoreCallSite<>() { + public DefaultDataStoreCallSite getDefaultDataStoreCallSite() { + return new DefaultDataStoreCallSite() { @Override - public boolean isMajor(DataStoreEntryRef o) { - return true; - } - - @Override - public ObservableValue getName(DataStoreEntryRef store) { - return AppI18n.observable("openWebsite"); - } - - @Override - public String getIcon(DataStoreEntryRef store) { - return "mdi2s-search-web"; + public ActionProvider.Action createAction(DataStoreEntryRef store) { + return new Action(store.getStore()); } @Override public Class getApplicableClass() { return AbstractServiceStore.class; } - - @Override - public List getChildren(DataStoreEntryRef store) { - return List.of(new HttpAction(), new HttpsAction()); - } }; } - private static class HttpAction implements ActionProvider { - - @Override - public String getId() { - return "serviceOpenHttp"; - } - - @Override - public LeafDataStoreCallSite getLeafDataStoreCallSite() { - return new LeafDataStoreCallSite() { - - @Override - public boolean canLinkTo() { - return true; - } - - @Override - public ActionProvider.Action createAction(DataStoreEntryRef store) { - return new ServiceOpenAction.Action("http", store.getStore()); - } - - @Override - public Class getApplicableClass() { - return AbstractServiceStore.class; - } - - @Override - public ObservableValue getName(DataStoreEntryRef store) { - return AppI18n.observable("openHttp"); - } - - @Override - public String getIcon(DataStoreEntryRef store) { - return "mdi2s-shield-off-outline"; - } - }; - } - } - - private static class HttpsAction implements ActionProvider { - - @Override - public String getId() { - return "serviceOpenHttps"; - } - - @Override - public LeafDataStoreCallSite getLeafDataStoreCallSite() { - return new LeafDataStoreCallSite() { - - @Override - public boolean canLinkTo() { - return true; - } - - @Override - public ActionProvider.Action createAction(DataStoreEntryRef store) { - return new ServiceOpenAction.Action("https", store.getStore()); - } - - @Override - public Class getApplicableClass() { - return AbstractServiceStore.class; - } - - @Override - public ObservableValue getName(DataStoreEntryRef store) { - return AppI18n.observable("openHttps"); - } - - @Override - public String getIcon(DataStoreEntryRef store) { - return "mdi2s-shield-lock-outline"; - } - }; - } - } - @Value static class Action implements ActionProvider.Action { - String protocol; AbstractServiceStore serviceStore; @Override public void execute() throws Exception { serviceStore.startSessionIfNeeded(); - var l = serviceStore.getSession().getLocalPort(); - var path = - serviceStore.getPath() != null && !serviceStore.getPath().isEmpty() ? serviceStore.getPath() : ""; - if (!path.isEmpty() && !path.startsWith("/")) { - path = "/" + path; - } - Hyperlinks.open(protocol + "://localhost:" + l + path); + var l = serviceStore.requiresTunnel() ? serviceStore.getSession().getLocalPort() : serviceStore.getRemotePort(); + var base = "localhost:" + l; + var full = serviceStore.getServiceProtocolType().formatUrl(base); + serviceStore.getServiceProtocolType().open(full); } } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceProtocolType.java b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceProtocolType.java new file mode 100644 index 000000000..4cdbe1be8 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceProtocolType.java @@ -0,0 +1,102 @@ +package io.xpipe.ext.base.service; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.app.util.Hyperlinks; +import io.xpipe.ext.base.identity.SshIdentityStrategy; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = ServiceProtocolType.None.class), + @JsonSubTypes.Type(value = ServiceProtocolType.Http.class), + @JsonSubTypes.Type(value = ServiceProtocolType.Https.class) +}) +public interface ServiceProtocolType { + + public abstract String formatUrl(String base); + + public abstract void open(String url); + + public abstract String getTranslationKey(); + + + @JsonTypeName("none") + @Value + @Jacksonized + @Builder + public static class None implements ServiceProtocolType { + + @Override + public String formatUrl(String base) { + return base; + } + + @Override + public void open(String url) {} + + @Override + public String getTranslationKey() { + return "none"; + } + } + + @JsonTypeName("http") + @Value + @Jacksonized + @Builder + public static class Http implements ServiceProtocolType { + + String path; + + @Override + public String formatUrl(String base) { + var url = "http://" + base; + if (path != null && !path.isEmpty()) { + url += (!path.startsWith("/") ? "/" : "") + path; + } + return url; + } + + @Override + public void open(String url) { + Hyperlinks.open(url); + } + + @Override + public String getTranslationKey() { + return "http"; + } + } + + @JsonTypeName("https") + @Value + @Jacksonized + @Builder + public static class Https implements ServiceProtocolType { + + String path; + + @Override + public String formatUrl(String base) { + var url = "https://" + base; + if (path != null && !path.isEmpty()) { + url += (!path.startsWith("/") ? "/" : "") + path; + } + return url; + } + + @Override + public void open(String url) { + Hyperlinks.open(url); + } + + @Override + public String getTranslationKey() { + return "https"; + } + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceProtocolTypeHelper.java b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceProtocolTypeHelper.java new file mode 100644 index 000000000..76b65fa93 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceProtocolTypeHelper.java @@ -0,0 +1,61 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.OptionsBuilder; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ObservableValue; + +import java.util.LinkedHashMap; + +public class ServiceProtocolTypeHelper { + + private static OptionsBuilder http(Property p) { + var path = new SimpleStringProperty(p.getValue() != null ? p.getValue().getPath() : null); + return new OptionsBuilder() + .nameAndDescription("servicePath") + .addString(path) + .bind( + () -> { + return new ServiceProtocolType.Http(path.get()); + }, + p); + } + + private static OptionsBuilder https(Property p) { + var path = new SimpleStringProperty(p.getValue() != null ? p.getValue().getPath() : null); + return new OptionsBuilder() + .nameAndDescription("servicePath") + .addString(path) + .bind( + () -> { + return new ServiceProtocolType.Https(path.get()); + }, + p); + } + + public static OptionsBuilder choice(Property serviceProtocolType) { + var ex = serviceProtocolType.getValue(); + var http = new SimpleObjectProperty<>(ex instanceof ServiceProtocolType.Http h ? h : null); + var https = new SimpleObjectProperty<>(ex instanceof ServiceProtocolType.Https h ? h : null); + var selected = new SimpleIntegerProperty(ex instanceof ServiceProtocolType.None ? 0 : + ex instanceof ServiceProtocolType.Http ? 1 : ex instanceof ServiceProtocolType.Https ? 2 : -1); + var available = new LinkedHashMap, OptionsBuilder>(); + available.put(AppI18n.observable("none"), new OptionsBuilder()); + available.put(AppI18n.observable("http"), http(http)); + available.put(AppI18n.observable("https"), https(https)); + return new OptionsBuilder() + .nameAndDescription("serviceProtocolType") + .choice(selected, available) + .bindChoice(() -> { + return switch (selected.get()) { + case 0 -> new SimpleObjectProperty<>(new ServiceProtocolType.None()); + case 1 -> http; + case 2 -> https; + default -> new SimpleObjectProperty<>(); + }; + }, serviceProtocolType); + } +} diff --git a/lang/strings/translations_en.properties b/lang/strings/translations_en.properties index 0b74128c6..bce7cac1c 100644 --- a/lang/strings/translations_en.properties +++ b/lang/strings/translations_en.properties @@ -1267,3 +1267,7 @@ updateNag=You haven't updated XPipe in a while. You might be missing out on new updateNagTitle=Update reminder updateNagButton=See releases refreshServices=Refresh services +serviceProtocolType=Service protocol type +serviceProtocolTypeDescription=The protocol to use to open the service +http=HTTP +https=HTTPS