diff --git a/app/src/main/java/io/xpipe/app/action/QuickConnectProvider.java b/app/src/main/java/io/xpipe/app/action/QuickConnectProvider.java index 334f43e38..270722813 100644 --- a/app/src/main/java/io/xpipe/app/action/QuickConnectProvider.java +++ b/app/src/main/java/io/xpipe/app/action/QuickConnectProvider.java @@ -1,6 +1,8 @@ package io.xpipe.app.action; import io.xpipe.app.ext.DataStore; +import io.xpipe.app.ext.DataStoreProvider; +import io.xpipe.app.ext.DataStoreProviders; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; @@ -28,4 +30,10 @@ public interface QuickConnectProvider extends ActionProvider { DataStore createStore(String arguments, DataStore existing); String getPlaceholder(); + + boolean skipDialogIfPossible(); + + default void open(DataStoreEntry e) throws Exception { + e.getProvider().launch(e).run(); + } } diff --git a/app/src/main/java/io/xpipe/app/action/XPipeUrlProvider.java b/app/src/main/java/io/xpipe/app/action/XPipeUrlProvider.java index 8e60c27eb..f6ae326a7 100644 --- a/app/src/main/java/io/xpipe/app/action/XPipeUrlProvider.java +++ b/app/src/main/java/io/xpipe/app/action/XPipeUrlProvider.java @@ -11,7 +11,7 @@ public class XPipeUrlProvider implements LauncherUrlProvider { @Override public String getPlaceholder() { - return "xpipe://action?[¶m=value]"; + return "xpipe://action?"; } @Override diff --git a/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java b/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java index 96120c07d..aba10904d 100644 --- a/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java +++ b/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java @@ -2,8 +2,10 @@ package io.xpipe.app.beacon; import io.xpipe.app.beacon.mcp.AppMcpServer; import io.xpipe.app.core.AppLocalTemp; +import io.xpipe.app.core.AppProperties; import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.issue.TrackEvent; +import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.DocumentationLink; import io.xpipe.beacon.BeaconConfig; import io.xpipe.beacon.BeaconInterface; @@ -170,13 +172,15 @@ public class AppBeaconServer { } private boolean handleCorsHeaders(HttpExchange exchange) throws IOException { - exchange.getResponseHeaders() - .add("Origin", "http://localhost:" + AppBeaconServer.get().getPort()); - exchange.getResponseHeaders().add("Vary", "Origin"); - exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*"); - exchange.getResponseHeaders().add("Access-Control-Allow-Credentials", "true"); - exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "*"); - exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "*"); + if (AppPrefs.get().enableHttpApi().get()) { + exchange.getResponseHeaders().add("Origin", "http://localhost:" + AppBeaconServer.get().getPort()); + exchange.getResponseHeaders().add("Vary", "Origin"); + exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*"); + exchange.getResponseHeaders().add("Access-Control-Allow-Credentials", "true"); + exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "*"); + exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "*"); + } + if (exchange.getRequestMethod().equals("OPTIONS")) { exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, -1); return true; diff --git a/app/src/main/java/io/xpipe/app/core/mode/AppBaseMode.java b/app/src/main/java/io/xpipe/app/core/mode/AppBaseMode.java index 167ebe626..74187a624 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/AppBaseMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/AppBaseMode.java @@ -213,6 +213,7 @@ public class AppBaseMode extends AppOperationMode { AppBeaconServer.reset(); KeePassXcPasswordManager.reset(); StoreViewState.reset(); + StoreFilterState.reset(); AppLayoutModel.reset(); AppTheme.reset(); PlatformState.teardown(); diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreFilterFieldComp.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreFilterFieldComp.java index ad42e0781..41efdd454 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreFilterFieldComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreFilterFieldComp.java @@ -15,6 +15,7 @@ import io.xpipe.app.platform.PlatformThread; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.ObservableSubscriber; import javafx.beans.binding.Bindings; +import javafx.geometry.Bounds; import javafx.scene.Cursor; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; @@ -51,6 +52,10 @@ public class StoreFilterFieldComp extends SimpleRegionBuilder { field.focusedProperty().subscribe(focus -> { if (focus) { popover.hide(); + } else { + if (state.getIsSearchString().get()) { + state.open(); + } } }); @@ -114,9 +119,18 @@ public class StoreFilterFieldComp extends SimpleRegionBuilder { state.getRawText().subscribe(val -> { PlatformThread.runLaterIfNeeded(() -> { + var wasFocused = field.isFocused(); + if (!wasFocused) { + field.requestFocus(); + } + if (!Objects.equals(field.getText(), val) && !(val == null && "".equals(field.getText()))) { field.setText(val); } + + if (!wasFocused) { + field.end(); + } }); }); @@ -125,7 +139,8 @@ public class StoreFilterFieldComp extends SimpleRegionBuilder { }); var menuButton = new IconButtonComp("mdi2a-animation-play", () -> { - popover.show(field); + Bounds bounds = field.localToScreen(field.getBoundsInLocal()); + popover.show(field, bounds.getMinX() + (field.getWidth() / 1.6), bounds.getMaxY() - 4.0); }); menuButton.describe(d -> d.nameKey("quickConnect")); menuButton.style("quick-connect-button"); diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreFilterState.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreFilterState.java index 1b2654523..4b5a31ad5 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreFilterState.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreFilterState.java @@ -84,10 +84,10 @@ public class StoreFilterState { INSTANCE.recentUrls.setContent(recentUrls); } - public void reset() { - AppCache.update("recentSearches", recentSearches); - AppCache.update("recentQuickConnections", recentQuickConnections); - AppCache.update("recentUrls", recentUrls); + public static void reset() { + AppCache.update("recentSearches", INSTANCE.recentSearches.getList()); + AppCache.update("recentQuickConnections", INSTANCE.recentQuickConnections.getList()); + AppCache.update("recentUrls", INSTANCE.recentUrls.getList()); INSTANCE = null; } @@ -135,6 +135,10 @@ public class StoreFilterState { } public boolean open() { + if (rawText.getValue() == null) { + return false; + } + if (isSearchString.getValue()) { putFilter(rawText.getValue()); return false; diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreFilterStateComp.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreFilterStateComp.java index 44fe227c9..96d1c8873 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreFilterStateComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreFilterStateComp.java @@ -38,7 +38,7 @@ public class StoreFilterStateComp extends SimpleRegionBuilder { var searches = state.getRecentSearches().getList(); var searchesEmpty = Bindings.isEmpty(searches); - var searchesList = new ListBoxViewComp(searches, searches, s -> createButton(s, null), false); + var searchesList = new ListBoxViewComp(searches, searches, s -> createButton(s, s), false); var searchesPlaceholders = FXCollections.observableList(List.of(AppI18n.get("recentSearchesDescriptionNames"), AppI18n.get("recentSearchesDescriptionTags"), AppI18n.get("recentSearchesDescriptionTypes"))); @@ -46,16 +46,16 @@ public class StoreFilterStateComp extends SimpleRegionBuilder { var quickConnections = state.getRecentQuickConnections().getList(); var quickConnectionsEmpty = Bindings.isEmpty(quickConnections); - var quickConnectionsList = new ListBoxViewComp(quickConnections, quickConnections, s -> createButton(s, null), false); + var quickConnectionsList = new ListBoxViewComp(quickConnections, quickConnections, s -> createButton(s, s), false); var quickConnectionsPlaceholders = FXCollections.observableArrayList(QuickConnectProvider.getAll().stream() .map(p -> p.getPlaceholder()) .toList()); - var quickConnectionsEmptyList = new ListBoxViewComp(quickConnectionsPlaceholders, quickConnectionsPlaceholders, s -> createButton(s, null), false); + var quickConnectionsEmptyList = new ListBoxViewComp(quickConnectionsPlaceholders, quickConnectionsPlaceholders, s -> createButton(s, s.split(" ")[0] + " "), false); var urls = state.getRecentUrls().getList(); var urlsEmpty = Bindings.isEmpty(urls); - var urlList = new ListBoxViewComp(urls, urls, s -> createButton(s, null), false); + var urlList = new ListBoxViewComp(urls, urls, s -> createButton(s, s), false); var urlPlaceholders = FXCollections.observableArrayList(LauncherUrlProvider.getAll().stream() .map(p -> p.getPlaceholder()) diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreQuickConnect.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreQuickConnect.java index 0f5f4141b..febe6ff91 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreQuickConnect.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreQuickConnect.java @@ -71,23 +71,39 @@ public class StoreQuickConnect { if (newStore.isComplete()) { var existing = provider.get().findExisting(newStore); if (existing.isPresent()) { - try { - existing.get().getProvider().launch(existing.get()).run(); - return true; - } catch (Exception e) { - ErrorEventFactory.fromThrowable(e).handle(); - return false; - } + ThreadHelper.runAsync(() -> { + try { + provider.get().open(existing.get()); + } catch (Exception e) { + ErrorEventFactory.fromThrowable(e).handle(); + } + }); + return true; } } DataStorage.get().updateEntryStore(quickConnectEntry, newStore); + if (provider.get().skipDialogIfPossible() && newStore.isComplete()) { + update(newStore); + ThreadHelper.runAsync(() -> { + try { + DataStorage.get().addStoreEntryInProgress(quickConnectEntry); + provider.get().open(quickConnectEntry); + } catch (Exception e) { + ErrorEventFactory.fromThrowable(e).handle(); + } finally { + DataStorage.get().removeStoreEntryInProgress(quickConnectEntry); + } + }); + return true; + } + var model = StoreCreationDialog.showEdit(quickConnectEntry, newStore, false, finished -> { update(finished.getStore()); ThreadHelper.runAsync(() -> { try { DataStorage.get().addStoreEntryInProgress(quickConnectEntry); - quickConnectEntry.getProvider().launch(quickConnectEntry).run(); + provider.get().open(quickConnectEntry); } catch (Exception e) { ErrorEventFactory.fromThrowable(e).handle(); } finally { 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 616949ed0..cabcd3cf5 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -336,7 +336,7 @@ public abstract class DataStorage { } catch (Exception e) { return false; } - } while ((c = DataStorage.get().getDefaultDisplayParent(c).orElse(null)) != null); + } while ((c = getDefaultDisplayParent(c).orElse(null)) != null); return true; } @@ -359,8 +359,8 @@ public abstract class DataStorage { newEntry.setIcon(icon, true); } - var oldParent = DataStorage.get().getDefaultDisplayParent(entry); - var newParent = DataStorage.get().getDefaultDisplayParent(newEntry); + var oldParent = getDefaultDisplayParent(entry); + var newParent = getDefaultDisplayParent(newEntry); var sameParent = Objects.equals(oldParent, newParent); finalizeWithDependencies(entry); @@ -457,7 +457,9 @@ public abstract class DataStorage { } entry.setStoreInternal(store, false); - saveAsync(); + if (storeEntries.containsKey(entry)) { + saveAsync(); + } } public void updateCategory(DataStoreCategory category, DataStoreCategory newCategory) { @@ -470,7 +472,7 @@ public abstract class DataStorage { public void updateCategoryConfig(DataStoreCategory category, DataStoreCategoryConfig config) { if (category.setConfig(config)) { // Update git remote if needed - DataStorage.get().saveAsync(); + saveAsync(); } } diff --git a/app/src/main/java/io/xpipe/app/util/HostHelper.java b/app/src/main/java/io/xpipe/app/util/HostHelper.java index c74c5a83c..eceed4a6c 100644 --- a/app/src/main/java/io/xpipe/app/util/HostHelper.java +++ b/app/src/main/java/io/xpipe/app/util/HostHelper.java @@ -22,6 +22,23 @@ public class HostHelper { } } + public static boolean isLocalhost(String host) { + host = host.strip(); + if (host.equalsIgnoreCase("localhost")) { + return true; + } + + if (host.equals("127.0.0.1")) { + return true; + } + + if (host.equals("::1") || host.equals("[::1]")) { + return true; + } + + return false; + } + public static boolean isLocalNetworkAddress(String host) { Inet4Address inet4Address; try { diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/MultiIdentityStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/MultiIdentityStoreProvider.java index b724efb50..f4cf55bb1 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/identity/MultiIdentityStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/MultiIdentityStoreProvider.java @@ -34,6 +34,7 @@ public class MultiIdentityStoreProvider extends IdentityStoreProvider { public GuiDialog guiDialog(DataStoreEntry entry, Property store) { MultiIdentityStore st = (MultiIdentityStore) store.getValue(); + var initialIdentities = new ArrayList<>(st.getAvailableIdentities()); var identities = new SimpleListProperty<>(FXCollections.observableArrayList(st.getAvailableIdentities())); return new OptionsBuilder() @@ -47,7 +48,14 @@ public class MultiIdentityStoreProvider extends IdentityStoreProvider { for (DataStoreEntryRef identity : identities) { uuids.add(identity.get().getUuid()); } - uuids.addAll(st.getIdentities()); + for (UUID storeIdentity : st.getIdentities()) { + if (initialIdentities.stream().anyMatch(ref -> ref.get().getUuid().equals(storeIdentity))) { + var wasRemoved = identities.stream().noneMatch(ref -> ref.get().getUuid().equals(storeIdentity)); + if (!wasRemoved) { + uuids.add(storeIdentity); + } + } + } return MultiIdentityStore.builder() .identities(new ArrayList<>(uuids)) diff --git a/lang/strings/translations_en.properties b/lang/strings/translations_en.properties index 2b622f6b1..1e18550aa 100644 --- a/lang/strings/translations_en.properties +++ b/lang/strings/translations_en.properties @@ -802,7 +802,8 @@ identityChoice=User identity identityChoiceDescription=Choose a predefined identity or specify login details just for this connection defineNewIdentityOrSelect=Enter new or choose existing localIdentity.displayName=Local identity -localIdentity.displayDescription=Create a reusable identity for this local desktop +#force +localIdentity.displayDescription=Create a reusable identity for this local system syncedIdentity.displayName=Synced identity syncedIdentity.displayDescription=Create a reusable identity that is synced across systems localIdentity=Local identity @@ -2045,6 +2046,6 @@ recentSearchesDescriptionNames=, e.g. "Local Machine" recentSearchesDescriptionTags=, e.g. "my-custom-tag" recentSearchesDescriptionTypes=, e.g. "Docker container" recentQuickConnections=Recent quick connections -recentQuickConnectionsDescription=Quick connect via connection string +recentQuickConnectionsDescription=Quick connect via string recentUrls=Recent URLs -recentUrlsDescription=Quick connect via connection URLs +recentUrlsDescription=Quick connect via URLs