mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-21 23:20:23 +00:00
Small fixes and improvements [release]
This commit is contained in:
parent
b881e548fc
commit
71e9539efd
16 changed files with 177 additions and 44 deletions
|
@ -0,0 +1,51 @@
|
|||
package io.xpipe.app.browser;
|
||||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.AppWindowHelper;
|
||||
import io.xpipe.core.store.FileSystem;
|
||||
import javafx.scene.control.Alert;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class FileBrowserAlerts {
|
||||
|
||||
public static boolean showMoveAlert(List<FileSystem.FileEntry> source, FileSystem.FileEntry target) {
|
||||
if (source.stream().noneMatch(entry -> entry.isDirectory())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return AppWindowHelper.showBlockingAlert(alert -> {
|
||||
alert.setTitle(AppI18n.get("moveAlertTitle"));
|
||||
alert.setHeaderText(AppI18n.get("moveAlertHeader", source.size(), target.getPath()));
|
||||
alert.getDialogPane().setContent(AppWindowHelper.alertContentText(getSelectedElementsString(source)));
|
||||
alert.setAlertType(Alert.AlertType.CONFIRMATION);
|
||||
})
|
||||
.map(b -> b.getButtonData().isDefaultButton())
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
public static boolean showDeleteAlert(List<FileSystem.FileEntry> source) {
|
||||
if (source.stream().noneMatch(entry -> entry.isDirectory())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return AppWindowHelper.showBlockingAlert(alert -> {
|
||||
alert.setTitle(AppI18n.get("deleteAlertTitle"));
|
||||
alert.setHeaderText(AppI18n.get("deleteAlertHeader", source.size()));
|
||||
alert.getDialogPane().setContent(AppWindowHelper.alertContentText(getSelectedElementsString(source)));
|
||||
alert.setAlertType(Alert.AlertType.CONFIRMATION);
|
||||
})
|
||||
.map(b -> b.getButtonData().isDefaultButton())
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
private static String getSelectedElementsString(List<FileSystem.FileEntry> source) {
|
||||
var namesHeader = AppI18n.get("selectedElements");
|
||||
var names = namesHeader + "\n" + source.stream().limit(10).map(entry -> "- " + entry.getPath()).collect(Collectors.joining("\n"));
|
||||
if (source.size() > 10) {
|
||||
names += "\n+ " + (source.size() - 10) + " ...";
|
||||
}
|
||||
return names;
|
||||
}
|
||||
}
|
|
@ -147,8 +147,8 @@ final class FileContextMenu extends ContextMenu {
|
|||
|
||||
var delete = new MenuItem("Delete");
|
||||
delete.setOnAction(event -> {
|
||||
model.deleteSelectionAsync();
|
||||
event.consume();
|
||||
model.deleteAsync(entry.getPath());
|
||||
});
|
||||
|
||||
var rename = new MenuItem("Rename");
|
||||
|
|
|
@ -23,6 +23,7 @@ public class FileListCompEntry {
|
|||
|
||||
private Point2D lastOver = new Point2D(-1, -1);
|
||||
private TimerTask activeTask;
|
||||
private FileContextMenu currentContextMenu;
|
||||
|
||||
public FileListCompEntry(Node row, FileSystem.FileEntry item, FileListModel model) {
|
||||
this.row = row;
|
||||
|
@ -36,9 +37,15 @@ public class FileListCompEntry {
|
|||
return;
|
||||
}
|
||||
|
||||
var cm = new FileContextMenu(model.getFileSystemModel(), item, model.getEditing());
|
||||
if (currentContextMenu != null) {
|
||||
currentContextMenu.hide();
|
||||
currentContextMenu = null;
|
||||
}
|
||||
|
||||
if (t.getButton() == MouseButton.SECONDARY) {
|
||||
var cm = new FileContextMenu(model.getFileSystemModel(), item, model.getEditing());
|
||||
cm.show(row, t.getScreenX(), t.getScreenY());
|
||||
currentContextMenu = cm;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,16 +23,18 @@ public class FileSystemHelper {
|
|||
ConnectionFileSystem fileSystem = (ConnectionFileSystem) model.getFileSystem();
|
||||
var current = !(model.getStore().getValue() instanceof LocalStore)
|
||||
? fileSystem
|
||||
.getShellControl()
|
||||
.executeStringSimpleCommand(fileSystem
|
||||
.getShellControl()
|
||||
.getShellDialect()
|
||||
.getPrintWorkingDirectoryCommand())
|
||||
: fileSystem.getShell().get().getOsType().getHomeDirectory(fileSystem.getShell().get());
|
||||
return FileSystemHelper.normalizeDirectoryPath(model, current);
|
||||
.getShellControl()
|
||||
.executeStringSimpleCommand(
|
||||
fileSystem.getShellControl().getShellDialect().getPrintWorkingDirectoryCommand())
|
||||
: fileSystem
|
||||
.getShell()
|
||||
.get()
|
||||
.getOsType()
|
||||
.getHomeDirectory(fileSystem.getShell().get());
|
||||
return FileSystemHelper.resolveDirectoryPath(model, current);
|
||||
}
|
||||
|
||||
public static String normalizeDirectoryPath(OpenFileSystemModel model, String path) throws Exception {
|
||||
public static String resolveDirectoryPath(OpenFileSystemModel model, String path) throws Exception {
|
||||
if (path == null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -65,6 +67,8 @@ public class FileSystemHelper {
|
|||
throw new IllegalArgumentException(String.format("Directory %s does not exist", normalized));
|
||||
}
|
||||
|
||||
model.getFileSystem().directoryAccessible(normalized);
|
||||
|
||||
return FileNames.toDirectory(normalized);
|
||||
}
|
||||
|
||||
|
@ -96,6 +100,20 @@ public class FileSystemHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public static void delete(List<FileSystem.FileEntry> files) throws Exception {
|
||||
if (files.size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var file : files) {
|
||||
try {
|
||||
file.getFileSystem().delete(file.getPath());
|
||||
} catch (Throwable t) {
|
||||
ErrorEvent.fromThrowable(t).handle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void dropFilesInto(
|
||||
FileSystem.FileEntry target, List<FileSystem.FileEntry> files, boolean explicitCopy) throws Exception {
|
||||
if (files.size() == 0) {
|
||||
|
|
|
@ -2,7 +2,9 @@ package io.xpipe.app.browser;
|
|||
|
||||
import atlantafx.base.controls.Spacer;
|
||||
import io.xpipe.app.fxcomps.SimpleComp;
|
||||
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
|
||||
import io.xpipe.app.fxcomps.impl.TextFieldComp;
|
||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||
import io.xpipe.core.impl.FileNames;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
|
@ -52,11 +54,13 @@ public class OpenFileSystemComp extends SimpleComp {
|
|||
|
||||
var terminalBtn = new Button(null, new FontIcon("mdi2c-code-greater-than"));
|
||||
terminalBtn.setOnAction(e -> model.openTerminalAsync(model.getCurrentPath().get()));
|
||||
terminalBtn.disableProperty().bind(PlatformThread.sync(model.getNoDirectory()));
|
||||
|
||||
var addBtn = new Button(null, new FontIcon("mdmz-plus"));
|
||||
addBtn.setOnAction(e -> {
|
||||
creatingProperty.set(true);
|
||||
});
|
||||
addBtn.disableProperty().bind(PlatformThread.sync(model.getNoDirectory()));
|
||||
|
||||
var filter = new FileFilterComp(model.getFilter()).createRegion();
|
||||
|
||||
|
@ -85,14 +89,14 @@ public class OpenFileSystemComp extends SimpleComp {
|
|||
pane.getChildren().add(root);
|
||||
|
||||
var creation = createCreationWindow(creatingProperty);
|
||||
var creationPain = new StackPane(creation);
|
||||
creationPain.setAlignment(Pos.CENTER);
|
||||
creationPain.setOnMouseClicked(event -> {
|
||||
var creationPane = new StackPane(creation);
|
||||
creationPane.setAlignment(Pos.CENTER);
|
||||
creationPane.setOnMouseClicked(event -> {
|
||||
creatingProperty.set(false);
|
||||
});
|
||||
pane.getChildren().add(creationPain);
|
||||
creationPain.visibleProperty().bind(creatingProperty);
|
||||
creationPain.managedProperty().bind(creatingProperty);
|
||||
pane.getChildren().add(creationPane);
|
||||
creationPane.visibleProperty().bind(creatingProperty);
|
||||
creationPane.managedProperty().bind(creatingProperty);
|
||||
|
||||
return pane;
|
||||
}
|
||||
|
@ -104,25 +108,32 @@ public class OpenFileSystemComp extends SimpleComp {
|
|||
creationName.setText("");
|
||||
}
|
||||
});
|
||||
var createFileButton = new Button("Create file");
|
||||
var createFileButton = new Button("File", new PrettyImageComp(new SimpleStringProperty("file_drag_icon.png"), 20, 20).createRegion());
|
||||
createFileButton.setOnAction(event -> {
|
||||
model.createFileAsync(FileNames.join(model.getCurrentPath().get(), creationName.getText()));
|
||||
model.createFileAsync(creationName.getText());
|
||||
creating.set(false);
|
||||
});
|
||||
var createDirectoryButton = new Button("Create directory");
|
||||
var createDirectoryButton = new Button("Directory", new PrettyImageComp(new SimpleStringProperty("folder_closed.svg"), 20, 20).createRegion());
|
||||
createDirectoryButton.setOnAction(event -> {
|
||||
model.createDirectoryAsync(FileNames.join(model.getCurrentPath().get(), creationName.getText()));
|
||||
model.createDirectoryAsync(creationName.getText());
|
||||
creating.set(false);
|
||||
});
|
||||
var buttonBar = new ButtonBar();
|
||||
buttonBar.getButtons().addAll(createFileButton, createDirectoryButton);
|
||||
var creationContent = new VBox(creationName, buttonBar);
|
||||
creationContent.setSpacing(15);
|
||||
var creation = new TitledPane("New", creationContent);
|
||||
var creation = new TitledPane("New ...", creationContent);
|
||||
creation.setMaxWidth(400);
|
||||
creation.setCollapsible(false);
|
||||
creationContent.setPadding(new Insets(15));
|
||||
creation.getStyleClass().add("elevated-3");
|
||||
|
||||
creating.addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue) {
|
||||
creationName.requestFocus();
|
||||
}
|
||||
});
|
||||
|
||||
return creation;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ final class OpenFileSystemModel {
|
|||
private final FileBrowserNavigationHistory history = new FileBrowserNavigationHistory();
|
||||
private final BooleanProperty busy = new SimpleBooleanProperty();
|
||||
private final FileBrowserModel browserModel;
|
||||
private final BooleanProperty noDirectory = new SimpleBooleanProperty();
|
||||
|
||||
public OpenFileSystemModel(FileBrowserModel browserModel) {
|
||||
this.browserModel = browserModel;
|
||||
|
@ -77,7 +78,7 @@ final class OpenFileSystemModel {
|
|||
public Optional<String> cd(String path) {
|
||||
String newPath = null;
|
||||
try {
|
||||
newPath = FileSystemHelper.normalizeDirectoryPath(this, path);
|
||||
newPath = FileSystemHelper.resolveDirectoryPath(this, path);
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
return Optional.of(currentPath.get());
|
||||
|
@ -116,10 +117,12 @@ final class OpenFileSystemModel {
|
|||
List<FileSystem.FileEntry> newList;
|
||||
if (dir != null) {
|
||||
newList = getFileSystem().listFiles(dir).collect(Collectors.toCollection(ArrayList::new));
|
||||
noDirectory.set(false);
|
||||
} else {
|
||||
newList = getFileSystem().listRoots().stream()
|
||||
.map(s -> new FileSystem.FileEntry(getFileSystem(), s, Instant.now(), true, false, false, 0))
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
noDirectory.set(true);
|
||||
}
|
||||
fileList.setAll(newList);
|
||||
return true;
|
||||
|
@ -151,14 +154,25 @@ final class OpenFileSystemModel {
|
|||
return;
|
||||
}
|
||||
|
||||
var same = files.get(0).getFileSystem().equals(target.getFileSystem());
|
||||
if (same) {
|
||||
if (!FileBrowserAlerts.showMoveAlert(files, target)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
FileSystemHelper.dropFilesInto(target, files, explicitCopy);
|
||||
refreshInternal();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void createDirectoryAsync(String path) {
|
||||
if (path.isBlank()) {
|
||||
public void createDirectoryAsync(String name) {
|
||||
if (name.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getCurrentDirectory() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -168,14 +182,23 @@ final class OpenFileSystemModel {
|
|||
return;
|
||||
}
|
||||
|
||||
fileSystem.mkdirs(path);
|
||||
var abs = FileNames.join(getCurrentDirectory().getPath(), name);
|
||||
if (fileSystem.directoryExists(abs)) {
|
||||
throw new IllegalStateException(String.format("Directory %s already exists", abs));
|
||||
}
|
||||
|
||||
fileSystem.mkdirs(abs);
|
||||
refreshInternal();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void createFileAsync(String path) {
|
||||
if (path.isBlank()) {
|
||||
public void createFileAsync(String name) {
|
||||
if (name.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getCurrentDirectory() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -185,20 +208,25 @@ final class OpenFileSystemModel {
|
|||
return;
|
||||
}
|
||||
|
||||
fileSystem.touch(path);
|
||||
var abs = FileNames.join(getCurrentDirectory().getPath(), name);
|
||||
fileSystem.touch(abs);
|
||||
refreshInternal();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void deleteAsync(String path) {
|
||||
public void deleteSelectionAsync() {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
BusyProperty.execute(busy, () -> {
|
||||
if (fileSystem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
fileSystem.delete(path);
|
||||
if (!FileBrowserAlerts.showDeleteAlert(fileList.getSelected())) {
|
||||
return;
|
||||
}
|
||||
|
||||
FileSystemHelper.delete(fileList.getSelected());
|
||||
refreshInternal();
|
||||
});
|
||||
});
|
||||
|
@ -249,7 +277,7 @@ final class OpenFileSystemModel {
|
|||
var command = s.create()
|
||||
.initWith(List.of(connection.getShellDialect().getCdCommand(directory)))
|
||||
.prepareTerminalOpen();
|
||||
TerminalHelper.open("", command);
|
||||
TerminalHelper.open(directory, command);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -48,7 +48,7 @@ public class StoreCreationBarComp extends SimpleComp {
|
|||
.shortcut(new KeyCodeCombination(KeyCode.D, KeyCombination.SHORTCUT_DOWN))
|
||||
.apply(new FancyTooltipAugment<>("addDatabase"));
|
||||
|
||||
var box = new VerticalComp(List.of(newShellStore, newDbStore, newStreamStore, newOtherStore));
|
||||
var box = new VerticalComp(List.of(newShellStore, newDbStore, newStreamStore));
|
||||
box.apply(s -> AppFont.medium(s.get()));
|
||||
var bar = box.createRegion();
|
||||
bar.getStyleClass().add("bar");
|
||||
|
|
|
@ -7,6 +7,7 @@ import io.xpipe.app.util.ThreadHelper;
|
|||
import javafx.application.ConditionalFeature;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.SceneAntialiasing;
|
||||
|
@ -35,7 +36,9 @@ public class AppWindowHelper {
|
|||
var text = new Text(s);
|
||||
text.setWrappingWidth(450);
|
||||
AppFont.medium(text);
|
||||
return new StackPane(text);
|
||||
var sp = new StackPane(text);
|
||||
sp.setPadding(new Insets(5));
|
||||
return sp;
|
||||
}
|
||||
|
||||
public static Stage sideWindow(
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package io.xpipe.app.issue;
|
||||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.core.process.ProcessOutputException;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
|
@ -14,6 +15,7 @@ public class ExceptionConverter {
|
|||
}
|
||||
|
||||
return switch (ex) {
|
||||
case ProcessOutputException e -> e.getOutput();
|
||||
case StackOverflowError e -> AppI18n.get("app.stackOverflow");
|
||||
case OutOfMemoryError e -> AppI18n.get("app.outOfMemory");
|
||||
case FileNotFoundException e -> AppI18n.get("app.fileNotFound", msg);
|
||||
|
|
|
@ -4,6 +4,7 @@ import io.xpipe.app.ext.PrefsChoiceValue;
|
|||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.util.ApplicationHelper;
|
||||
import io.xpipe.app.util.MacOsPermissions;
|
||||
import io.xpipe.core.impl.FileNames;
|
||||
import io.xpipe.core.impl.LocalStore;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
|
@ -45,7 +46,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
|
||||
@Override
|
||||
protected String toCommand(String name, String command) {
|
||||
return "-w 1 nt --title \"" + name + "\" " + command;
|
||||
// A weird behavior in Windows Terminal causes the trailing
|
||||
// backslash of a filepath to escape the closing quote in the title argument
|
||||
// So just remove that slash
|
||||
var fixedName = FileNames.removeTrailingSlash(name);
|
||||
return "-w 1 nt --title \"" + fixedName + "\" " + command;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -11,7 +11,7 @@ public class TerminalHelper {
|
|||
}
|
||||
|
||||
public static void open(String title, String command) throws Exception {
|
||||
if (command.contains("\n") || !command.strip().equals(command)) {
|
||||
if (command.contains("\n") || command.contains(" ")) {
|
||||
command = ScriptHelper.createLocalExecScript(command);
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,11 @@ common=Common
|
|||
other=Other
|
||||
askpassAlertTitle=Askpass
|
||||
nullPointer=Null Pointer
|
||||
moveAlertTitle=Confirm move
|
||||
moveAlertHeader=Do you want to move the ($COUNT$) selected elements into $TARGET$?
|
||||
deleteAlertTitle=Confirm deletion
|
||||
deleteAlertHeader=Do you want to delete the ($COUNT$) selected elements?
|
||||
selectedElements=Selected elements:
|
||||
mustNotBeEmpty=$NAME$ must not be empty
|
||||
null=$VALUE$ must be not null
|
||||
hostFeatureUnsupported=Host does not support the feature $FEATURE$
|
||||
|
|
|
@ -10,7 +10,6 @@ import lombok.Getter;
|
|||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Path;
|
||||
|
@ -77,10 +76,7 @@ public class FileStore extends JacksonizedValue implements FilenameStore, Stream
|
|||
|
||||
@Override
|
||||
public OutputStream openOutput() throws Exception {
|
||||
if (!fileSystem.createFileSystem().open().mkdirs(getParent())) {
|
||||
throw new IOException("Unable to create directory: " + getParent());
|
||||
}
|
||||
|
||||
fileSystem.createFileSystem().open().mkdirs(getParent());
|
||||
return fileSystem.createFileSystem().open().openOutput(path);
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,11 @@ public class ConnectionFileSystem implements FileSystem {
|
|||
return shellControl.getShellDialect().directoryExists(shellControl, file).executeAndCheck();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void directoryAccessible(String file) throws Exception {
|
||||
shellControl.executeSimpleCommand(shellControl.getShellDialect().getCdCommand(file));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<FileEntry> listFiles(String file) throws Exception {
|
||||
return shellControl.getShellDialect().listFiles(this, shellControl, file);
|
||||
|
@ -107,11 +112,11 @@ public class ConnectionFileSystem implements FileSystem {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean mkdirs(String file) throws Exception {
|
||||
public void mkdirs(String file) throws Exception {
|
||||
try (var pc = shellControl.command(proc -> proc.getShellDialect()
|
||||
.getMkdirsCommand(file)).complex()
|
||||
.start()) {
|
||||
return pc.discardAndCheckExit();
|
||||
pc.discardOrThrow();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -59,12 +59,14 @@ public interface FileSystem extends Closeable, AutoCloseable {
|
|||
|
||||
void move(String file, String newFile) throws Exception;
|
||||
|
||||
boolean mkdirs(String file) throws Exception;
|
||||
void mkdirs(String file) throws Exception;
|
||||
|
||||
void touch(String file) throws Exception;
|
||||
|
||||
boolean directoryExists(String file) throws Exception;
|
||||
|
||||
void directoryAccessible(String file) throws Exception;
|
||||
|
||||
Stream<FileEntry> listFiles(String file) throws Exception;
|
||||
|
||||
default Stream<FileEntry> listFilesRecursively(String file) throws Exception {
|
||||
|
|
2
version
2
version
|
@ -1 +1 @@
|
|||
0.5.17
|
||||
0.5.18
|
Loading…
Reference in a new issue