From a3b9eaa9f7f92544a15e29143f25cd2a43e293f2 Mon Sep 17 00:00:00 2001 From: crschnick Date: Fri, 22 Aug 2025 16:38:06 +0000 Subject: [PATCH] Browser fixes [stage] [arm] --- .../io/xpipe/app/beacon/mcp/McpTools.java | 4 +- .../app/browser/file/BrowserFileListComp.java | 17 +++++- .../browser/file/BrowserFileListNameCell.java | 20 +++---- .../file/BrowserFileSystemTabComp.java | 18 +++++- .../file/BrowserFileSystemTabModel.java | 38 +++++++++---- .../file/BrowserFileTransferOperation.java | 56 +++++++++++++------ .../browser/file/BrowserStatusBarComp.java | 2 +- .../java/io/xpipe/app/ext/FileSystem.java | 32 ++++++++--- .../io/xpipe/app/util/DocumentationLink.java | 1 + .../java/io/xpipe/app/util/ShellTemp.java | 2 +- dist/changelog/18.0.md | 19 +++++-- .../ext/base/script/SimpleScriptStore.java | 2 +- version | 2 +- 13 files changed, 151 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/io/xpipe/app/beacon/mcp/McpTools.java b/app/src/main/java/io/xpipe/app/beacon/mcp/McpTools.java index a72e133eb..651bc6d39 100644 --- a/app/src/main/java/io/xpipe/app/beacon/mcp/McpTools.java +++ b/app/src/main/java/io/xpipe/app/beacon/mcp/McpTools.java @@ -172,7 +172,7 @@ public final class McpTools { throw new BeaconClientException("Directory " + path + " does not exist"); } - try (var stream = recursive ? fs.listFilesRecursively(fs, path) : fs.listFiles(fs, path)) { + try (var stream = recursive ? fs.listFilesRecursively(fs, path).stream() : fs.listFiles(fs, path)) { var list = stream.toList(); var builder = McpSchema.CallToolResult.builder(); for (FileEntry e : list) { @@ -202,7 +202,7 @@ public final class McpTools { } var regex = Pattern.compile(DataStorageQuery.toRegex(pattern)); - try (var stream = recursive ? fs.listFilesRecursively(fs, path) : fs.listFiles(fs, path)) { + try (var stream = recursive ? fs.listFilesRecursively(fs, path).stream() : fs.listFiles(fs, path)) { var list = stream.toList(); var builder = McpSchema.CallToolResult.builder(); list.stream() diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java index f2ffa0be8..eee362f00 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java @@ -273,6 +273,14 @@ public final class BrowserFileListComp extends SimpleComp { return; } + if (event.isControlDown() || event.isShiftDown() || event.isAltDown() || event.isMetaDown()) { + return; + } + + if (typedSelection.get().isEmpty() && typed.equals(" ")) { + return; + } + var updated = typedSelection.get() + typed; var found = fileList.getShown().getValue().stream() .filter(browserEntry -> browserEntry.getFileName().toLowerCase().startsWith(updated.toLowerCase())) @@ -608,9 +616,14 @@ public final class BrowserFileListComp extends SimpleComp { updateHandler.accept(null, null); fileList.getShown().addListener((observable, oldValue, newValue) -> { // Delay to prevent internal tableview exceptions when sorting - Platform.runLater(() -> { + var isSortChange = oldValue.size() == newValue.size() && new HashSet<>(oldValue).containsAll(newValue); + if (isSortChange) { + Platform.runLater(() -> { + updateHandler.accept(oldValue, newValue); + }); + } else { updateHandler.accept(oldValue, newValue); - }); + } }); fileList.getFileSystemModel().getCurrentPath().addListener((observable, oldValue, newValue) -> { if (oldValue == null) { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListNameCell.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListNameCell.java index 5e1d651ab..841eb7181 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListNameCell.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListNameCell.java @@ -114,16 +114,16 @@ class BrowserFileListNameCell extends TableCell { return; } - var selection = typedSelection.get() + " "; - var found = fileList.getShown().getValue().stream() - .filter(browserEntry -> - browserEntry.getFileName().toLowerCase().startsWith(selection)) - .findFirst(); - // Ugly fix to prevent space from showing the menu when there is a file matching - // Due to the table view input map, these events always get sent and consumed, not allowing us to - // differentiate between these cases - if (found.isPresent()) { - return; + if (!typedSelection.get().isEmpty()) { + var selection = typedSelection.get() + " "; + var found = fileList.getShown().getValue().stream().filter( + browserEntry -> browserEntry.getFileName().toLowerCase().startsWith(selection)).findFirst(); + // Ugly fix to prevent space from showing the menu when there is a file matching + // Due to the table view input map, these events always get sent and consumed, not allowing us to + // differentiate between these cases + if (found.isPresent()) { + return; + } } var selected = fileList.getSelection(); diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabComp.java index 12e1d29e4..c559c63d0 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabComp.java @@ -9,11 +9,15 @@ import io.xpipe.app.comp.augment.ContextMenuAugment; import io.xpipe.app.comp.base.*; import io.xpipe.app.core.AppFontSizes; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.GlobalTimer; import io.xpipe.app.util.InputHelper; import io.xpipe.app.util.PlatformThread; import io.xpipe.core.FilePath; +import javafx.application.Platform; import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.value.ObservableValue; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.MenuButton; @@ -30,6 +34,7 @@ import javafx.scene.layout.VBox; import atlantafx.base.controls.Spacer; import org.kordamp.ikonli.javafx.FontIcon; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -220,13 +225,22 @@ public class BrowserFileSystemTabComp extends SimpleComp { }); }); + // Delay show to hide file list changes happening + // Not perfect, but covers most of the cases of small directories + var showOverview = new SimpleBooleanProperty(true); + model.getCurrentPath().subscribe(path -> { + GlobalTimer.delay(() -> { + showOverview.setValue(path == null); + }, Duration.ofMillis(250)); + }); + var home = new BrowserOverviewComp(model).styleClass("browser-overview"); var stack = new MultiContentComp( Map.of( home, - model.getCurrentPath().isNull(), + showOverview, fileList, - model.getCurrentPath().isNull().not()), + showOverview.not()), false); var r = stack.styleClass("browser-content-container").createRegion(); r.focusedProperty().addListener((observable, oldValue, newValue) -> { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java index 45aaaae79..95da248c8 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java @@ -30,6 +30,7 @@ import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.scene.control.SelectionMode; import lombok.Getter; import lombok.NonNull; @@ -37,10 +38,7 @@ import java.io.IOException; import java.nio.file.Path; import java.time.Duration; import java.time.Instant; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -84,6 +82,11 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab(); + all.addAll(entries); + for (BrowserEntry browserEntry : fileList.getAll().getValue()) { + var fe = browserEntry.getRawFileEntry(); + if (fe.getKind() == FileKind.LINK && entries.stream().anyMatch(o -> o.getPath().equals(fe.resolved().getPath()))) { + all.add(fe); + } + } + + for (FileEntry fileEntry : entries) { + if (fileEntry.getKind() == FileKind.LINK) { + all.add(fileEntry.resolved()); + } + } + try { - for (var e : entries) { + for (var e : all) { var refresh = fileSystem.getFileInfo(e.getPath()); fileList.updateEntry(e.getPath(), refresh.orElse(null)); } @@ -316,6 +334,11 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab list = stream.toList(); - for (FileEntry fileEntry : list) { - if (cancelled()) { - return; - } + var list = new ArrayList(); + source.getFileSystem().traverseFilesRecursively(source.getFileSystem(), source.getPath(), fileEntry -> { + if (cancelled()) { + progress.accept(BrowserTransferProgress.finished(source.getName() + " ...", totalSize.get())); + return false; + } - var rel = fileEntry - .getPath() - .relativize(baseRelative) - .toUnix() - .toString(); - flatFiles.put(fileEntry, rel); - if (fileEntry.getKind() == FileKind.FILE) { - // This one is up-to-date and does not need to be recalculated - // If we don't have a size, it doesn't matter that much as the total size is only for display - totalSize.addAndGet(fileEntry.getFileSizeLong().orElse(0)); - } + var rel = fileEntry + .getPath() + .relativize(baseRelative) + .toUnix() + .toString(); + flatFiles.put(fileEntry, rel); + if (fileEntry.getKind() == FileKind.FILE) { + // This one is up-to-date and does not need to be recalculated + // If we don't have a size, it doesn't matter that much as the total size is only for display + totalSize.addAndGet(fileEntry.getFileSizeLong().orElse(0)); + progress.accept(new BrowserTransferProgress(source.getName() + " ...", 0, totalSize.get())); + } + list.add(fileEntry); + return true; + }); + + for (FileEntry fileEntry : list) { + if (cancelled()) { + return; + } + + var rel = fileEntry + .getPath() + .relativize(baseRelative) + .toUnix() + .toString(); + flatFiles.put(fileEntry, rel); + if (fileEntry.getKind() == FileKind.FILE) { + // This one is up-to-date and does not need to be recalculated + // If we don't have a size, it doesn't matter that much as the total size is only for display + totalSize.addAndGet(fileEntry.getFileSizeLong().orElse(0)); + progress.accept(new BrowserTransferProgress(source.getName(), 0, totalSize.get())); } } } else { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java index df251774e..b687f126e 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java @@ -45,7 +45,7 @@ public class BrowserStatusBarComp extends SimpleComp { bar.apply(struc -> { struc.get().widthProperty().subscribe(value -> { var veryConstrained = value.doubleValue() < 600; - var somewhatConstrained = value.doubleValue() < 750; + var somewhatConstrained = value.doubleValue() < 710; struc.get().getChildren().get(2).setVisible(!somewhatConstrained); struc.get().getChildren().get(2).setManaged(!somewhatConstrained); struc.get().getChildren().get(4).setVisible(!veryConstrained); diff --git a/app/src/main/java/io/xpipe/app/ext/FileSystem.java b/app/src/main/java/io/xpipe/app/ext/FileSystem.java index c8c9ba294..fe1ea7d54 100644 --- a/app/src/main/java/io/xpipe/app/ext/FileSystem.java +++ b/app/src/main/java/io/xpipe/app/ext/FileSystem.java @@ -7,8 +7,11 @@ import io.xpipe.core.FilePath; import java.io.Closeable; import java.io.InputStream; import java.io.OutputStream; +import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Predicate; import java.util.stream.Stream; public interface FileSystem extends Closeable, AutoCloseable { @@ -49,18 +52,29 @@ public interface FileSystem extends Closeable, AutoCloseable { Stream listFiles(FileSystem system, FilePath file) throws Exception; - default Stream listFilesRecursively(FileSystem system, FilePath file) throws Exception { - return listFiles(system, file).flatMap(fileEntry -> { - if (fileEntry.getKind() != FileKind.DIRECTORY) { - return Stream.of(fileEntry); + default List listFilesRecursively(FileSystem system, FilePath file) throws Exception { + var all = new ArrayList(); + traverseFilesRecursively(system, file, all::add); + return all; + } + + default void traverseFilesRecursively(FileSystem system, FilePath file, Predicate visitor) throws Exception { + List base; + try (var filesStream = listFiles(system, file)) { + base = filesStream.toList(); + } + + for (FileEntry fileEntry : base) { + if (!visitor.test(fileEntry)) { + return; } - try { - return Stream.concat(Stream.of(fileEntry), listFilesRecursively(system, fileEntry.getPath())); - } catch (Exception e) { - throw new RuntimeException(e); + if (fileEntry.getKind() != FileKind.DIRECTORY) { + continue; } - }); + + traverseFilesRecursively(system, fileEntry.getPath(), visitor); + } } List listRoots() throws Exception; diff --git a/app/src/main/java/io/xpipe/app/util/DocumentationLink.java b/app/src/main/java/io/xpipe/app/util/DocumentationLink.java index 91f748390..a02fe52b9 100644 --- a/app/src/main/java/io/xpipe/app/util/DocumentationLink.java +++ b/app/src/main/java/io/xpipe/app/util/DocumentationLink.java @@ -40,6 +40,7 @@ public enum DocumentationLink { VNC("guide/vnc"), REAL_VNC("guide/vnc#realvnc-server"), SSH("guide/ssh"), + SSH_GATEWAYS("guide/ssh#gateways-and-jump-servers"), SSH_HOST_KEYS("troubleshoot/ssh#no-matching-host-key-type-found"), SSH_KEX("troubleshoot/ssh#no-matching-key-exchange-method"), SSH_IPV6("troubleshoot/ssh#ipv6-issues"), diff --git a/app/src/main/java/io/xpipe/app/util/ShellTemp.java b/app/src/main/java/io/xpipe/app/util/ShellTemp.java index c5a9317e1..73153d3df 100644 --- a/app/src/main/java/io/xpipe/app/util/ShellTemp.java +++ b/app/src/main/java/io/xpipe/app/util/ShellTemp.java @@ -101,7 +101,7 @@ public class ShellTemp { "-Force")) .executeAndCheck(); } else { - sc.command(CommandBuilder.of().add("rm", "-f").add("\"" + prefix.toString() + "\"*")) + sc.command(CommandBuilder.of().add("rm", "-f").add("\"" + prefix.toString() + "\"*").add("2>/dev/null")) .executeAndCheck(); } } diff --git a/dist/changelog/18.0.md b/dist/changelog/18.0.md index 0882fd572..9a8fc623c 100644 --- a/dist/changelog/18.0.md +++ b/dist/changelog/18.0.md @@ -51,6 +51,19 @@ The fonts on macOS have been updated with the goal of better readability. If you - Add support to launch VsCode Insiders and Trae as well in the VsCode SSH launch menu - The custom SSH agent setting now works differently. To use the agent socket specified in the settings menu, you will now have to select the custom SSH agent for a connection. The default OpenSSH agent will no longer use this option +## File browser + +- Files with trailing or leading spaces are now included in the file list +- The initial transfer size calculation for large directories is now dynamically updated and shown +- Fix execute action on Windows not being limited to certain file types +- Fix refresh of single files failing on Windows systems +- Fix execute action not showing confirmation dialog if enabled +- Fix not supporting dollar signs in directory names +- Fix conflict dialog being cut off +- Fix file listing not working on Solaris systems +- Fix renames not working in cmd +- Fix script arguments not being passed with file selection + ## Other - Tailscale connections are now a top-level entry and can be synced via git @@ -72,13 +85,7 @@ The fonts on macOS have been updated with the goal of better readability. If you - Fix small parts of the UI moving a bit on hover - Fix Bitwarden integration in some cases requiring the master password every time - Fix restart button not working on Linux -- Fix file browser execute action on Windows not being limited to certain file types -- Fix file browser refresh of single files failing on Windows systems -- Fix file browser execute action not showing confirmation dialog if enabled -- Fix file browser not supporting dollar signs in directory names -- Fix file browser conflict dialog being cut off - Fix license check becoming invalid if xpipe was left running for more than a week - Fix application freeze when having password manager identities while password manager was not running - Fix git vaultversion conflict not being solved automatically -- Fix file listing in file browser not working on Solaris systems - Fix BSD and Solaris systems not being recognized correctly diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStore.java b/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStore.java index 3382860c9..b97eef086 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStore.java @@ -60,7 +60,7 @@ public class SimpleScriptStore extends ScriptStore implements SelfReferentialSto shellControl.getShellDialect().getNewLine().getNewLineString())); var targetType = shellControl.getOriginalShellDialect(); var script = ScriptHelper.createExecScript(targetType, shellControl, fixedCommands); - return targetType.sourceScriptCommand(shellControl, script.toString()); + return targetType.sourceScriptCommand(shellControl, script.toString()) + " " + targetType.getCatchAllVariable(); } return null; diff --git a/version b/version index 6883bfeac..1d9b1c581 100644 --- a/version +++ b/version @@ -1 +1 @@ -18.0-32 +18.0-33