This commit is contained in:
crschnick
2025-06-21 18:54:50 +00:00
parent 0a40eedf18
commit fdc9bf47f3
706 changed files with 15763 additions and 11343 deletions

View File

@@ -86,10 +86,12 @@ Note that this is a desktop application that should be run on your local desktop
Installers are the easiest way to get started and come with an optional automatic update functionality:
- [Windows .msi Installer (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-windows-x86_64.msi)
- [Windows .msi Installer (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-windows-arm64.msi)
If you don't like installers, you can also use a portable version that is packaged as an archive:
- [Windows .zip Portable (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-windows-x86_64.zip)
- [Windows .zip Portable (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-windows-arm64.zip)
Alternatively, you can also use the following package managers:
- [choco](https://community.chocolatey.org/packages/xpipe) to install it with `choco install xpipe`.

View File

@@ -23,8 +23,8 @@ dependencies {
api project(':beacon')
compileOnly 'org.hamcrest:hamcrest:3.0'
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.11.4'
compileOnly 'org.junit.jupiter:junit-jupiter-params:5.11.4'
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.12.2'
compileOnly 'org.junit.jupiter:junit-jupiter-params:5.12.2'
api 'com.vladsch.flexmark:flexmark:0.64.8'
api 'com.vladsch.flexmark:flexmark-util:0.64.8'
@@ -49,21 +49,21 @@ dependencies {
api 'com.vladsch.flexmark:flexmark-ext-toc:0.64.8'
api("com.github.weisj:jsvg:1.7.1")
api files("$rootDir/gradle/gradle_scripts/vernacular-1.16.jar")
api 'io.xpipe:vernacular:1.15'
api 'org.bouncycastle:bcprov-jdk18on:1.80'
api 'info.picocli:picocli:4.7.6'
api 'org.apache.commons:commons-lang3:3.17.0'
api 'io.sentry:sentry:8.7.0'
api 'commons-io:commons-io:2.18.0'
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.18.3"
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.18.3"
api 'io.sentry:sentry:8.13.3'
api 'commons-io:commons-io:2.19.0'
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.19.1"
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.19.1"
api group: 'org.kordamp.ikonli', name: 'ikonli-material2-pack', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-materialdesign2-pack', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-material-pack', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-feather-pack', version: "12.2.0"
api group: 'org.slf4j', name: 'slf4j-api', version: '2.0.16'
api group: 'org.slf4j', name: 'slf4j-jdk-platform-logging', version: '2.0.16'
api group: 'org.slf4j', name: 'slf4j-api', version: '2.0.17'
api group: 'org.slf4j', name: 'slf4j-jdk-platform-logging', version: '2.0.17'
api 'io.xpipe:modulefs:0.1.6'
api 'net.synedra:validatorfx:0.4.2'
api files("$rootDir/gradle/gradle_scripts/atlantafx-base-2.0.2.jar")

View File

@@ -0,0 +1,191 @@
package io.xpipe.app.action;
import io.xpipe.app.comp.base.ModalButton;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.util.DataStoreFormatter;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.app.util.ThreadHelper;
import lombok.experimental.SuperBuilder;
import java.util.*;
import java.util.function.Consumer;
@SuperBuilder
public abstract class AbstractAction {
private static final Set<AbstractAction> active = new HashSet<>();
private static boolean closed;
private static Consumer<AbstractAction> pick;
private static final AppLayoutModel.QueueEntry queueEntry = new AppLayoutModel.QueueEntry(
AppI18n.observable("cancelActionPicker"), new LabelGraphic.IconGraphic("mdal-cancel_presentation"), () -> {
cancelPick();
});
public static synchronized void expectPick() {
if (pick != null) {
return;
}
var show = !AppCache.getBoolean("pickIntroductionShown", false);
if (show) {
var modal = ModalOverlay.of("actionPickerTitle", AppDialog.dialogTextKey("actionPickerDescription"));
modal.addButton(ModalButton.ok());
modal.showAndWait();
AppCache.update("pickIntroductionShown", true);
}
AppLayoutModel.get().getQueueEntries().add(queueEntry);
pick = action -> {
cancelPick();
var modal = ModalOverlay.of("actionShortcuts", new ActionPickComp(action).prefWidth(600));
modal.show();
};
}
public static synchronized void cancelPick() {
AppLayoutModel.get().getQueueEntries().remove(queueEntry);
pick = null;
}
public static void reset() {
closed = true;
for (int i = 10; i > 0; i--) {
synchronized (active) {
var count = active.size();
if (count == 0) {
break;
}
}
// Wait 10s max
ThreadHelper.sleep(1000);
}
synchronized (active) {
for (AbstractAction abstractAction : active) {
TrackEvent.trace("Action has not quit after timeout: " + abstractAction.toString());
}
}
}
public void executeSync() {
if (closed) {
return;
}
synchronized (AbstractAction.class) {
if (pick != null) {
TrackEvent.withTrace("Picked action").tags(toDisplayMap()).handle();
pick.accept(this);
pick = null;
return;
}
}
executeSyncImpl();
}
public void executeAsync() {
if (closed) {
return;
}
synchronized (AbstractAction.class) {
if (pick != null) {
TrackEvent.withTrace("Picked action").tags(toDisplayMap()).handle();
pick.accept(this);
pick = null;
return;
}
}
ThreadHelper.runAsync(() -> {
executeSyncImpl();
});
}
private void executeSyncImpl() {
if (!ActionConfirmation.confirmAction(this)) {
return;
}
if (closed) {
return;
}
synchronized (active) {
active.add(this);
}
TrackEvent.withTrace("Starting action execution").tags(toDisplayMap()).handle();
try {
if (!beforeExecute()) {
return;
}
} catch (Throwable t) {
ErrorEventFactory.fromThrowable(t).handle();
return;
}
try {
executeImpl();
} catch (Throwable t) {
ErrorEventFactory.fromThrowable(t).handle();
} finally {
afterExecute();
synchronized (active) {
active.remove(this);
}
TrackEvent.withTrace("Finished action execution").tag("id", getId()).handle();
}
}
public String getId() {
return getProvider().getId();
}
public String getDisplayName() {
var id = getId();
return id != null ? DataStoreFormatter.camelCaseToName(id) : "?";
}
public ActionProvider getProvider() {
var clazz = getClass();
var enc = clazz.getEnclosingClass();
if (enc == null) {
throw new IllegalStateException("No enclosing instance of " + clazz);
}
return ActionProvider.ALL.stream()
.filter(actionProvider -> actionProvider.getClass().equals(enc))
.findFirst()
.orElseThrow(IllegalStateException::new);
}
public String getShortcutName() {
return getDisplayName();
}
public abstract void executeImpl() throws Exception;
protected boolean beforeExecute() throws Exception {
return true;
}
public boolean isMutation() {
return false;
}
protected void afterExecute() {}
public abstract Map<String, String> toDisplayMap();
}

View File

@@ -0,0 +1,105 @@
package io.xpipe.app.action;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.hub.action.BatchStoreAction;
import io.xpipe.app.hub.action.MultiStoreAction;
import io.xpipe.app.hub.action.StoreAction;
import io.xpipe.app.hub.comp.StoreChoiceComp;
import io.xpipe.app.hub.comp.StoreListChoiceComp;
import io.xpipe.app.hub.comp.StoreViewState;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.*;
import io.xpipe.core.store.DataStore;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.scene.layout.Region;
public class ActionConfigComp extends SimpleComp {
private final Property<AbstractAction> action;
public ActionConfigComp(Property<AbstractAction> action) {
this.action = action;
}
@Override
protected Region createSimple() {
var options = new OptionsBuilder();
options.nameAndDescription("actionStore")
.addComp(createChooser())
.nameAndDescription("actionStores")
.addComp(createMultiChooser());
options.nameAndDescription("actionConfiguration").addComp(createTextArea());
return options.build();
}
@SuppressWarnings("unchecked")
private Comp<?> createMultiChooser() {
var listProp = new SimpleListProperty<DataStoreEntryRef<DataStore>>(FXCollections.observableArrayList());
if (action.getValue() instanceof BatchStoreAction<?> ba) {
listProp.setAll(((BatchStoreAction<DataStore>) ba).getRefs());
} else if (action.getValue() instanceof MultiStoreAction<?> ma) {
listProp.setAll(((MultiStoreAction<DataStore>) ma).getRefs());
} else {
listProp.clear();
}
listProp.addListener((obs, o, n) -> {
if (action.getValue() instanceof BatchStoreAction<?> ba) {
action.setValue(((BatchStoreAction<DataStore>) ba).withRefs(n));
} else if (action.getValue() instanceof MultiStoreAction<?> ma) {
action.setValue(((MultiStoreAction<DataStore>) ma).withRefs(n));
}
});
var choice = new StoreListChoiceComp<>(
listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory());
choice.hide(listProp.emptyProperty());
return choice;
}
@SuppressWarnings("unchecked")
private Comp<?> createChooser() {
var singleProp = new SimpleObjectProperty<DataStoreEntryRef<DataStore>>();
var s = action.getValue() instanceof StoreAction<?> sa ? sa.getRef() : null;
singleProp.set((DataStoreEntryRef<DataStore>) s);
singleProp.addListener((obs, o, n) -> {
if (action.getValue() instanceof StoreAction<?> sa) {
action.setValue(sa.withRef(n.asNeeded()));
}
});
var choice = new StoreChoiceComp<>(
StoreChoiceComp.Mode.OTHER,
null,
singleProp,
DataStore.class,
ref -> true,
StoreViewState.get().getAllConnectionsCategory());
choice.hide(singleProp.isNull());
return choice;
}
private Comp<?> createTextArea() {
var config = new SimpleStringProperty();
var s = action.getValue() instanceof SerializableAction sa ? sa.toConfigNode() : null;
config.set(s != null && s.size() > 0 ? s.toPrettyString() : null);
config.addListener((obs, o, n) -> {
if (action.getValue() instanceof SerializableAction aa && n != null) {
var with = aa.withConfigString(n);
if (with.isPresent()) {
action.setValue(with.get());
}
}
});
var area = new IntegratedTextAreaComp(config, false, "action", new SimpleStringProperty("json"));
area.hide(config.isNull());
return area;
}
}

View File

@@ -0,0 +1,79 @@
package io.xpipe.app.action;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.hub.action.BatchStoreAction;
import io.xpipe.app.hub.action.MultiStoreAction;
import io.xpipe.app.hub.action.StoreAction;
import io.xpipe.app.hub.comp.StoreListChoiceComp;
import io.xpipe.app.hub.comp.StoreViewState;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.OptionsBuilder;
import io.xpipe.core.store.DataStore;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Region;
import atlantafx.base.theme.Styles;
import java.util.List;
import java.util.Map;
public class ActionConfirmComp extends SimpleComp {
private final AbstractAction action;
public ActionConfirmComp(AbstractAction action) {
this.action = action;
}
@Override
protected Region createSimple() {
var options = new OptionsBuilder();
var plural = action instanceof BatchStoreAction<?> || action instanceof MultiStoreAction<?>;
options.nameAndDescription(plural ? "actionConnections" : "actionConnection")
.addComp(createList());
options.nameAndDescription("actionConfiguration").addComp(createTable());
return options.build();
}
@SuppressWarnings("unchecked")
private Comp<?> createList() {
var listProp = new SimpleListProperty<DataStoreEntryRef<DataStore>>(FXCollections.observableArrayList());
if (action instanceof BatchStoreAction<?> ba) {
listProp.setAll(((BatchStoreAction<DataStore>) ba).getRefs());
} else if (action instanceof MultiStoreAction<?> ma) {
listProp.setAll(((MultiStoreAction<DataStore>) ma).getRefs());
} else if (action instanceof StoreAction<?> sa) {
listProp.setAll(List.of(sa.getRef().asNeeded()));
}
var choice = new StoreListChoiceComp<>(
listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory());
choice.setEditable(false);
choice.hide(listProp.emptyProperty());
return choice;
}
private Comp<?> createTable() {
var map = action.toDisplayMap();
return Comp.of(() -> {
var grid = new GridPane();
grid.setHgap(11);
grid.setVgap(2);
var row = 0;
for (Map.Entry<String, String> e : map.entrySet()) {
var name = new Label(e.getKey());
var value = new Label(e.getValue());
value.getStyleClass().add(Styles.TEXT_BOLD);
grid.add(name, 0, row);
grid.add(value, 1, row);
row++;
}
return grid;
});
}
}

View File

@@ -0,0 +1,42 @@
package io.xpipe.app.action;
import io.xpipe.app.comp.base.ModalButton;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import javafx.beans.property.SimpleBooleanProperty;
import java.util.List;
public class ActionConfirmation {
public static boolean confirmAction(AbstractAction action) {
if (!action.isMutation() || !confirmAllModifications(action)) {
return true;
}
var ok = new SimpleBooleanProperty(false);
var modal = ModalOverlay.of("confirmAction", new ActionConfirmComp(action).prefWidth(550));
modal.addButton(ModalButton.cancel());
modal.addButton(ModalButton.ok(() -> ok.set(true)));
modal.showAndWait();
return ok.get();
}
private static boolean confirmAllModifications(AbstractAction action) {
var context = getContext(action);
return context.stream().anyMatch(dataStoreEntry -> {
var config = DataStorage.get().getEffectiveCategoryConfig(dataStoreEntry);
return config.getConfirmAllModifications() != null && config.getConfirmAllModifications();
});
}
private static List<DataStoreEntry> getContext(AbstractAction action) {
if (action instanceof StoreContextAction ca) {
return ca.getStoreEntryContext();
}
return List.of();
}
}

View File

@@ -0,0 +1,105 @@
package io.xpipe.app.action;
import io.xpipe.app.hub.action.BatchStoreAction;
import io.xpipe.app.hub.action.MultiStoreAction;
import io.xpipe.app.hub.action.StoreAction;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.JacksonMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.ArrayList;
public class ActionJacksonMapper {
@SuppressWarnings("unchecked")
public static <T extends AbstractAction> T parse(JsonNode tree) throws JsonProcessingException {
if (!tree.isObject()) {
return null;
}
var id = tree.get("id");
if (id == null || !id.isTextual()) {
return null;
}
var provider = ActionProvider.ALL.stream()
.filter(actionProvider -> id.textValue().equals(actionProvider.getId()))
.findFirst();
if (provider.isEmpty()) {
return null;
}
var clazz = provider.get().getActionClass();
if (clazz.isEmpty()) {
return null;
}
var object = (ObjectNode) tree;
var ref = tree.get("ref");
if (ref != null && !ref.isArray() && StoreAction.class.isAssignableFrom(clazz.get())) {
var action = JacksonMapper.getDefault().treeToValue(tree, clazz.get());
return (T) action;
}
var makeBatch = ref != null && ref.isArray() && !MultiStoreAction.class.isAssignableFrom(clazz.get());
if (makeBatch) {
if (ref.size() == 0) {
return null;
}
var batchActions = new ArrayList<StoreAction<DataStore>>();
object.remove("ref");
for (JsonNode batchRef : ref) {
object.set("ref", batchRef);
var action = JacksonMapper.getDefault().treeToValue(object, clazz.get());
batchActions.add((StoreAction<DataStore>) action);
}
return (T) BatchStoreAction.builder().actions(batchActions).build();
}
var makeMulti = ref != null && ref.isArray() && MultiStoreAction.class.isAssignableFrom(clazz.get());
if (makeMulti) {
object.remove("ref");
object.set("refs", ref);
var action = JacksonMapper.getDefault().treeToValue(object, clazz.get());
return (T) action;
}
return null;
}
public static ObjectNode write(AbstractAction value) {
if (value instanceof BatchStoreAction<?> b) {
var arrayNode = JsonNodeFactory.instance.arrayNode();
b.getActions().stream()
.map(a -> {
var tree = (ObjectNode) JacksonMapper.getDefault().valueToTree(a);
return tree.get("ref");
})
.forEach(n -> arrayNode.add(n));
var tree = (ObjectNode)
JacksonMapper.getDefault().valueToTree(b.getActions().getFirst());
tree.set("ref", arrayNode);
tree.put("id", b.getActions().getFirst().getId());
return tree;
}
var tree = (ObjectNode) JacksonMapper.getDefault().valueToTree(value);
if (value instanceof MultiStoreAction<?> m) {
var refs = tree.get("refs");
tree.remove("refs");
tree.set("ref", refs);
tree.put("id", m.getId());
return tree;
}
tree.put("id", value.getId());
return tree;
}
}

View File

@@ -0,0 +1,27 @@
package io.xpipe.app.action;
import io.xpipe.app.comp.base.ModalOverlayContentComp;
import io.xpipe.app.util.OptionsBuilder;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.layout.Region;
public class ActionPickComp extends ModalOverlayContentComp {
private final AbstractAction action;
public ActionPickComp(AbstractAction action) {
this.action = action;
}
@Override
protected Region createSimple() {
var prop = new SimpleObjectProperty<>(action);
var top = new ActionConfigComp(prop);
var bottom = new ActionShortcutComp(prop, () -> {
getModalOverlay().close();
});
var options = new OptionsBuilder().addComp(top).addComp(bottom);
return options.build();
}
}

View File

@@ -0,0 +1,57 @@
package io.xpipe.app.action;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.core.util.ModuleLayerLoader;
import java.util.*;
public interface ActionProvider {
List<ActionProvider> ALL = new ArrayList<>();
static void initProviders() {
TrackEvent.trace("Starting action provider initialization");
for (ActionProvider actionProvider : ALL) {
try {
actionProvider.init();
} catch (Throwable t) {
ErrorEventFactory.fromThrowable(t).handle();
}
}
TrackEvent.trace("Finished action provider initialization");
}
default void init() throws Exception {}
default String getLicensedFeatureId() {
return null;
}
default String getId() {
return null;
}
@SuppressWarnings("unchecked")
default Optional<Class<? extends AbstractAction>> getActionClass() {
var child = Arrays.stream(getClass().getDeclaredClasses())
.filter(aClass -> aClass.getSimpleName().equals("Action"))
.findFirst()
.map(aClass -> (Class<? extends AbstractAction>) aClass);
return child.isPresent() ? Optional.of(child.get()) : Optional.empty();
}
class Loader implements ModuleLayerLoader {
@Override
public void init(ModuleLayer layer) {
ALL.addAll(ServiceLoader.load(layer, ActionProvider.class).stream()
.map(p -> p.get())
.toList());
for (var p : DataStoreProviders.getAll()) {
ALL.addAll(p.getActionProviders());
}
}
}
}

View File

@@ -0,0 +1,88 @@
package io.xpipe.app.action;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.InputGroupComp;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.*;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.layout.Region;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class ActionShortcutComp extends SimpleComp {
private final Property<AbstractAction> action;
private final Runnable onCreateMacro;
public ActionShortcutComp(Property<AbstractAction> action, Runnable onCreateMacro) {
this.action = action;
this.onCreateMacro = onCreateMacro;
}
@Override
protected Region createSimple() {
var options = new OptionsBuilder();
options.nameAndDescription("actionDesktopShortcut").addComp(createDesktopComp());
options.nameAndDescription("actionUrlShortcut").addComp(createUrlComp());
// options.nameAndDescription("actionMacro")
// .addComp(createMacroComp());
return options.build();
}
private Comp<?> createUrlComp() {
var url = new SimpleStringProperty();
action.subscribe((v) -> {
var s = ActionUrls.toUrl(v);
PlatformThread.runLaterIfNeeded(() -> {
url.set(s);
});
});
var copyButton = new ButtonComp(null, new FontIcon("mdi2c-clipboard-multiple-outline"), () -> {
ClipboardHelper.copyUrl(url.getValue());
})
.grow(false, true)
.tooltipKey("createShortcut");
var field = new TextFieldComp(url);
field.grow(true, false);
field.apply(struc -> struc.get().setEditable(false));
var group = new InputGroupComp(List.of(field, copyButton));
return group;
}
private Comp<?> createDesktopComp() {
var url = BindingsHelper.map(action, abstractAction -> ActionUrls.toUrl(abstractAction));
var name = new SimpleStringProperty();
action.subscribe((v) -> {
var s = v.getShortcutName();
PlatformThread.runLaterIfNeeded(() -> {
name.set(s);
});
});
var copyButton = new ButtonComp(null, new FontIcon("mdi2f-file-move-outline"), () -> {
ThreadHelper.runFailableAsync(() -> {
var file = DesktopShortcuts.createCliOpen(url.getValue(), name.getValue());
DesktopHelper.browseFileInDirectory(file);
});
})
.grow(false, true)
.tooltipKey("createShortcut");
var field = new TextFieldComp(name);
field.grow(true, false);
var group = new InputGroupComp(List.of(field, copyButton));
return group;
}
private Comp<?> createMacroComp() {
var button = new ButtonComp(
AppI18n.observable("createMacro"), new FontIcon("mdi2c-clipboard-multiple-outline"), onCreateMacro);
return button;
}
}

View File

@@ -0,0 +1,146 @@
package io.xpipe.app.action;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.core.util.InPlaceSecretValue;
import io.xpipe.core.util.JacksonMapper;
import io.xpipe.core.util.UuidHelper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.SneakyThrows;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
public class ActionUrls {
private static String encodeValue(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}
private static List<String> nodeToString(JsonNode node) {
if (node.isTextual()) {
return List.of(encodeValue(node.asText()));
}
if (node.isArray()) {
var list = new ArrayList<String>();
for (JsonNode c : node) {
var r = nodeToString(c);
if (r.size() == 1) {
list.add(r.getFirst());
}
}
return list;
}
var enc = InPlaceSecretValue.of(node.toPrettyString()).getEncryptedValue();
return List.of(enc);
}
@SneakyThrows
public static String toUrl(AbstractAction action) {
if (!(action instanceof SerializableAction sa)) {
return null;
}
var json = sa.toNode();
var parsed =
JacksonMapper.getDefault().treeToValue(json, new TypeReference<LinkedHashMap<String, JsonNode>>() {});
Map<String, List<String>> requestParams = new LinkedHashMap<>();
for (Map.Entry<String, JsonNode> e : parsed.entrySet()) {
var value = nodeToString(e.getValue());
requestParams.put(e.getKey(), value);
}
String encodedURL = requestParams.keySet().stream()
.map(key -> {
var vals = requestParams.get(key);
return vals.stream().map(s -> key + "=" + s).collect(Collectors.joining("&"));
})
.collect(Collectors.joining("&", "xpipe://action?", ""));
return encodedURL;
}
public static Optional<AbstractAction> parse(String queryString) throws Exception {
var query = splitQuery(queryString);
var id = query.get("id");
if (id == null || id.size() != 1) {
return Optional.empty();
}
var provider = ActionProvider.ALL.stream()
.filter(actionProvider -> id.getFirst().equals(actionProvider.getId()))
.findFirst();
if (provider.isEmpty()) {
return Optional.empty();
}
var clazz = provider.get().getActionClass();
if (clazz.isEmpty()) {
return Optional.empty();
}
if (!SerializableAction.class.isAssignableFrom(clazz.get())) {
return Optional.empty();
}
var stores = query.get("ref");
if (stores == null || stores.isEmpty()) {
return Optional.empty();
}
for (String store : stores) {
var uuid = UuidHelper.parse(store);
if (uuid.isEmpty()) {
throw new IllegalArgumentException("Invalid store id: " + store);
}
var entry = DataStorage.get().getStoreEntryIfPresent(uuid.get());
if (entry.isEmpty()) {
throw new IllegalArgumentException("Store not found for id: " + store);
}
if (!entry.get().getValidity().isUsable()) {
throw new IllegalArgumentException(
"Store " + DataStorage.get().getStorePath(entry.get()) + " is incomplete");
}
}
var fixedMap = query.entrySet().stream()
.collect(Collectors.toMap(
entry -> entry.getKey(),
entry -> entry.getValue().size() == 1 ? entry.getValue().getFirst() : entry.getValue()));
var json = (ObjectNode) JacksonMapper.getDefault().valueToTree(fixedMap);
var instance = ActionJacksonMapper.parse(json);
return Optional.ofNullable(instance);
}
private static Map<String, List<String>> splitQuery(String query) {
if (query == null || query.isBlank()) {
return Collections.emptyMap();
}
return Arrays.stream(query.split("&"))
.map(ActionUrls::splitQueryParameter)
.collect(Collectors.groupingBy(
AbstractMap.SimpleImmutableEntry::getKey,
LinkedHashMap::new,
Collectors.mapping(Map.Entry::getValue, Collectors.toList())));
}
private static AbstractMap.SimpleImmutableEntry<String, String> splitQueryParameter(String it) {
final int idx = it.indexOf("=");
final String key = idx > 0 ? it.substring(0, idx) : it;
final String value = idx > 0 && it.length() > idx + 1 ? it.substring(idx + 1) : null;
return new AbstractMap.SimpleImmutableEntry<>(
URLDecoder.decode(key, StandardCharsets.UTF_8),
value != null ? URLDecoder.decode(value, StandardCharsets.UTF_8) : null);
}
}

View File

@@ -0,0 +1,10 @@
package io.xpipe.app.action;
import java.net.URI;
public interface LauncherUrlProvider extends ActionProvider {
String getScheme();
AbstractAction createAction(URI uri) throws Exception;
}

View File

@@ -0,0 +1,73 @@
package io.xpipe.app.action;
import io.xpipe.app.util.DataStoreFormatter;
import io.xpipe.core.util.JacksonMapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.experimental.SuperBuilder;
import java.util.*;
@SuperBuilder
public abstract class SerializableAction extends AbstractAction {
public String toString() {
return toNode().toPrettyString();
}
public ObjectNode toNode() {
var json = ActionJacksonMapper.write(this);
return json;
}
public ObjectNode toConfigNode() {
var json = toNode();
json.remove("ref");
json.remove("refs");
return json;
}
public Optional<? extends SerializableAction> withConfigString(String configString) {
try {
var tree = (ObjectNode) JacksonMapper.getDefault().readTree(configString);
tree.put("id", getId());
SerializableAction action = ActionJacksonMapper.parse(tree);
return Optional.ofNullable(action);
} catch (Exception ex) {
return Optional.empty();
}
}
@Override
public Map<String, String> toDisplayMap() {
var node = toConfigNode();
var map = new LinkedHashMap<String, String>();
map.put("Action", getDisplayName());
for (Map.Entry<String, JsonNode> property : node.properties()) {
if (property.getKey().equals("id")) {
continue;
}
var name = DataStoreFormatter.camelCaseToName(property.getKey());
var value = property.getValue().asText();
if (!value.isEmpty()) {
map.put(name, value);
} else if (property.getValue().isArray()) {
var list = new ArrayList<String>();
for (JsonNode jsonNode : property.getValue()) {
var s = jsonNode.asText();
if (!s.isEmpty()) {
list.add(s);
}
}
if (!list.isEmpty()) {
map.put(name, String.join("\n", list));
}
}
}
return map;
}
}

View File

@@ -0,0 +1,10 @@
package io.xpipe.app.action;
import io.xpipe.app.storage.DataStoreEntry;
import java.util.List;
public interface StoreContextAction {
List<DataStoreEntry> getStoreEntryContext();
}

View File

@@ -0,0 +1,23 @@
package io.xpipe.app.action;
import java.net.URI;
public class XPipeUrlProvider implements LauncherUrlProvider {
@Override
public String getScheme() {
return "xpipe";
}
@Override
public AbstractAction createAction(URI uri) throws Exception {
var a = uri.getHost();
if (!"action".equals(a)) {
return null;
}
var query = uri.getQuery();
var action = ActionUrls.parse(query);
return action.orElse(null);
}
}

View File

@@ -1,6 +1,6 @@
package io.xpipe.app.beacon;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.util.DocumentationLink;
import io.xpipe.beacon.BeaconConfig;
@@ -75,7 +75,7 @@ public class AppBeaconServer {
} catch (Exception ex) {
// Not terminal!
// We can still continue without the running server
ErrorEvent.fromThrowable("Unable to start local http server on port " + INSTANCE.getPort(), ex)
ErrorEventFactory.fromThrowable("Unable to start local http server on port " + INSTANCE.getPort(), ex)
.build()
.handle();
}
@@ -135,7 +135,7 @@ public class AppBeaconServer {
t.setDaemon(true);
t.setName("http handler");
t.setUncaughtExceptionHandler((t1, e) -> {
ErrorEvent.fromThrowable(e).handle();
ErrorEventFactory.fromThrowable(e).handle();
});
return t;
});

View File

@@ -1,7 +1,7 @@
package io.xpipe.app.beacon;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.ThreadHelper;
@@ -106,12 +106,12 @@ public class BeaconRequestHandler<T> implements HttpHandler {
response = beaconInterface.handle(exchange, object);
}
} catch (BeaconClientException clientException) {
ErrorEvent.fromThrowable(clientException).omit().expected().handle();
ErrorEventFactory.fromThrowable(clientException).omit().expected().handle();
writeError(exchange, new BeaconClientErrorResponse(clientException.getMessage()), 400);
return;
} catch (BeaconServerException serverException) {
var cause = serverException.getCause() != null ? serverException.getCause() : serverException;
var event = ErrorEvent.fromThrowable(cause).omit().handle();
var event = ErrorEventFactory.fromThrowable(cause).omit().handle();
var link = event.getLink();
writeError(exchange, new BeaconServerErrorResponse(cause, link), 500);
return;
@@ -119,9 +119,9 @@ public class BeaconRequestHandler<T> implements HttpHandler {
// Handle serialization errors as normal exceptions and other IO exceptions as assuming that the connection
// is broken
if (!ex.getClass().getName().contains("jackson")) {
ErrorEvent.fromThrowable(ex).omit().expected().handle();
ErrorEventFactory.fromThrowable(ex).omit().expected().handle();
} else {
ErrorEvent.fromThrowable(ex).omit().expected().handle();
ErrorEventFactory.fromThrowable(ex).omit().expected().handle();
// Make deserialization error message more readable
var message = ex.getMessage()
.replace("$RequestBuilder", "")
@@ -133,7 +133,7 @@ public class BeaconRequestHandler<T> implements HttpHandler {
}
return;
} catch (Throwable other) {
var event = ErrorEvent.fromThrowable(other).omit().expected().handle();
var event = ErrorEventFactory.fromThrowable(other).omit().expected().handle();
var link = event.getLink();
writeError(exchange, new BeaconServerErrorResponse(other, link), 500);
return;
@@ -159,10 +159,10 @@ public class BeaconRequestHandler<T> implements HttpHandler {
} catch (IOException ioException) {
// The exchange implementation might have already sent a response manually
if (!"headers already sent".equals(ioException.getMessage())) {
ErrorEvent.fromThrowable(ioException).omit().expected().handle();
ErrorEventFactory.fromThrowable(ioException).omit().expected().handle();
}
} catch (Throwable other) {
var event = ErrorEvent.fromThrowable(other).handle();
var event = ErrorEventFactory.fromThrowable(other).handle();
var link = event.getLink();
writeError(exchange, new BeaconServerErrorResponse(other, link), 500);
}
@@ -177,7 +177,7 @@ public class BeaconRequestHandler<T> implements HttpHandler {
os.write(bytes);
}
} catch (IOException ex) {
ErrorEvent.fromThrowable(ex).omit().expected().handle();
ErrorEventFactory.fromThrowable(ex).omit().expected().handle();
}
}

View File

@@ -1,6 +1,6 @@
package io.xpipe.app.beacon;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.util.ShellTemp;
import io.xpipe.beacon.BeaconClientException;
@@ -36,7 +36,7 @@ public class BlobManager {
} catch (IOException ignored) {
}
} catch (IOException e) {
ErrorEvent.fromThrowable(e).handle();
ErrorEventFactory.fromThrowable(e).handle();
}
}

View File

@@ -1,15 +1,15 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.terminal.TerminalView;
import io.xpipe.app.util.*;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.AskpassExchange;
import io.xpipe.core.util.InPlaceSecretValue;
import javafx.beans.property.SimpleStringProperty;
import com.sun.net.httpserver.HttpExchange;
import io.xpipe.core.util.InPlaceSecretValue;
import javafx.beans.property.SimpleStringProperty;
import java.time.Duration;
@@ -25,15 +25,19 @@ public class AskpassExchangeImpl extends AskpassExchange {
// SSH auth with a smartcard will prompt to confirm user presence
// Maybe we can show some dialog for this in the future
if (msg.getPrompt() != null && msg.getPrompt().toLowerCase().contains("confirm user presence")) {
var shown = AppLayoutModel.get().getQueueEntries().stream().anyMatch(queueEntry ->
msg.getPrompt().equals(queueEntry.getName().getValue()));
var shown = AppLayoutModel.get().getQueueEntries().stream().anyMatch(queueEntry -> msg.getPrompt()
.equals(queueEntry.getName().getValue()));
if (!shown) {
var qe = new AppLayoutModel.QueueEntry(new SimpleStringProperty(msg.getPrompt()), new LabelGraphic.IconGraphic("mdi2f-fingerprint"),
var qe = new AppLayoutModel.QueueEntry(
new SimpleStringProperty(msg.getPrompt()),
new LabelGraphic.IconGraphic("mdi2f-fingerprint"),
() -> {});
AppLayoutModel.get().getQueueEntries().add(qe);
GlobalTimer.delay(() -> {
AppLayoutModel.get().getQueueEntries().remove(qe);
}, Duration.ofSeconds(10));
GlobalTimer.delay(
() -> {
AppLayoutModel.get().getQueueEntries().remove(qe);
},
Duration.ofSeconds(10));
}
return Response.builder().value(InPlaceSecretValue.of("")).build();
}

View File

@@ -1,6 +1,6 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.beacon.BeaconClientException;
@@ -42,10 +42,10 @@ public class ConnectionAddExchangeImpl extends ConnectionAddExchange {
}
} catch (Throwable ex) {
if (ex instanceof ValidationException) {
ErrorEvent.expected(ex);
ErrorEventFactory.expected(ex);
} else if (ex instanceof StackOverflowError) {
// Cycles in connection graphs can fail hard but are expected
ErrorEvent.expected(ex);
ErrorEventFactory.expected(ex);
}
throw ex;
} finally {

View File

@@ -1,9 +1,9 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.ext.SingletonSessionStore;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionToggleExchange;
import io.xpipe.core.store.SingletonSessionStore;
import com.sun.net.httpserver.HttpExchange;

View File

@@ -20,7 +20,7 @@ public class FsScriptExchangeImpl extends FsScriptExchange {
try (var in = BlobManager.get().getBlob(msg.getBlob())) {
data = new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
data = shell.getControl().getShellDialect().prepareScriptContent(data);
data = shell.getControl().getShellDialect().prepareScriptContent(shell.getControl(), data);
var file = ScriptHelper.createExecScript(shell.getControl(), data);
return Response.builder().path(file).build();
}

View File

@@ -1,7 +1,6 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.terminal.TerminalLauncherManager;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.BeaconServerException;
import io.xpipe.beacon.api.TerminalWaitExchange;
@@ -10,7 +9,7 @@ import com.sun.net.httpserver.HttpExchange;
public class TerminalWaitExchangeImpl extends TerminalWaitExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException, BeaconServerException {
public Object handle(HttpExchange exchange, Request msg) throws BeaconServerException {
TerminalLauncherManager.waitExchange(msg.getRequest());
return Response.builder().build();
}

View File

@@ -7,11 +7,11 @@ import io.xpipe.app.browser.file.BrowserFileSystemTabComp;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.hub.comp.StoreEntryWrapper;
import io.xpipe.app.hub.comp.StoreViewState;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.FileReference;
@@ -43,7 +43,10 @@ public class BrowserFileChooserSessionComp extends ModalOverlayContentComp {
}
public static void openSingleFile(
Supplier<DataStoreEntryRef<? extends FileSystemStore>> store, Supplier<FilePath> initialPath, Consumer<FileReference> file, boolean save) {
Supplier<DataStoreEntryRef<? extends FileSystemStore>> store,
Supplier<FilePath> initialPath,
Consumer<FileReference> file,
boolean save) {
var model = new BrowserFileChooserSessionModel(BrowserFileSystemTabModel.SelectionMode.SINGLE_FILE);
model.setOnFinish(fileStores -> {
file.accept(fileStores.size() > 0 ? fileStores.getFirst() : null);

View File

@@ -6,11 +6,12 @@ import io.xpipe.app.browser.file.BrowserTransferComp;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.window.AppMainWindow;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.hub.comp.StoreEntryWrapper;
import io.xpipe.app.hub.comp.StoreViewState;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
@@ -61,9 +62,9 @@ public class BrowserFullSessionComp extends SimpleComp {
.bind(Bindings.createObjectBinding(
() -> new Insets(tabs.getHeaderHeight().get(), 0, 0, 0), tabs.getHeaderHeight()));
});
var loadingIndicator = LoadingOverlayComp.noProgress(Comp.empty(), model.getBusy())
var loadingIndicator = new LoadingIconComp(model.getBusy(), AppFontSizes::xxxl)
.apply(struc -> {
AnchorPane.setTopAnchor(struc.get(), 3.0);
AnchorPane.setTopAnchor(struc.get(), 0.0);
AnchorPane.setRightAnchor(struc.get(), 0.0);
})
.styleClass("tab-loading-indicator");
@@ -117,21 +118,21 @@ public class BrowserFullSessionComp extends SimpleComp {
return true;
}
return storeEntryWrapper.getEntry().getProvider().browserAction(model, storeEntryWrapper.getEntry(), null)
return storeEntryWrapper.getEntry().getProvider().launchBrowser(model, storeEntryWrapper.getEntry(), null)
!= null;
};
BiConsumer<StoreEntryWrapper, BooleanProperty> action = (w, busy) -> {
ThreadHelper.runFailableAsync(() -> {
var entry = w.getEntry();
if (!entry.getValidity().isUsable()) {
return;
}
var entry = w.getEntry();
if (!entry.getValidity().isUsable()) {
return;
}
var a = entry.getProvider().browserAction(model, entry, busy);
if (a != null) {
a.execute();
}
});
var a = entry.getProvider().launchBrowser(model, entry, busy);
if (a != null) {
ThreadHelper.runFailableAsync(() -> {
a.run();
});
}
};
var category = new SimpleObjectProperty<>(
@@ -160,7 +161,7 @@ public class BrowserFullSessionComp extends SimpleComp {
})
.vgrow();
var localDownloadStage = new BrowserTransferComp(model.getLocalTransfersStage())
.hide(PlatformThread.sync(Bindings.createBooleanBinding(
.hide(Bindings.createBooleanBinding(
() -> {
if (model.getSessionEntries().size() == 0) {
return true;
@@ -169,7 +170,7 @@ public class BrowserFullSessionComp extends SimpleComp {
return false;
},
model.getSessionEntries(),
model.getSelectedEntry())));
model.getSelectedEntry()));
localDownloadStage.prefHeight(200);
localDownloadStage.maxHeight(200);
var vertical =
@@ -212,6 +213,11 @@ public class BrowserFullSessionComp extends SimpleComp {
struc.get().setMaxWidth(newValue.doubleValue());
});
var clip = new Rectangle();
clip.widthProperty().bind(struc.get().widthProperty());
clip.heightProperty().bind(struc.get().heightProperty());
struc.get().setClip(clip);
AnchorPane.setBottomAnchor(struc.get(), 0.0);
AnchorPane.setRightAnchor(struc.get(), 0.0);
tabs.getHeaderHeight().subscribe(number -> {

View File

@@ -232,7 +232,12 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
}
}
if (path != null) {
model.initWithGivenDirectory(path.apply(model).toDirectory());
var applied = path.apply(model);
if (applied != null) {
model.initWithGivenDirectory(applied.toDirectory());
} else {
model.initWithDefaultDirectory();
}
} else {
model.initWithDefaultDirectory();
}

View File

@@ -2,8 +2,11 @@ package io.xpipe.app.browser;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.LoadingIconComp;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.comp.base.StackComp;
import io.xpipe.app.core.App;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.BooleanScope;
@@ -27,7 +30,6 @@ import javafx.scene.input.*;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import atlantafx.base.controls.RingProgressIndicator;
import atlantafx.base.theme.Styles;
import lombok.Getter;
@@ -206,7 +208,6 @@ public class BrowserSessionTabsComp extends SimpleComp {
r.setMinHeight(Region.USE_PREF_SIZE);
});
tabs.lookupAll(".headers-region").forEach(node -> {
node.setClip(null);
node.setPickOnBounds(false);
var r = (Region) node;
@@ -417,28 +418,24 @@ public class BrowserSessionTabsComp extends SimpleComp {
});
if (tabModel.getIcon() != null) {
var ring = new RingProgressIndicator(0, false);
ring.setMinSize(16, 16);
ring.setPrefSize(16, 16);
ring.setMaxSize(16, 16);
ring.progressProperty()
.bind(Bindings.createDoubleBinding(
() -> tabModel.getBusy().get()
&& !AppPrefs.get().performanceMode().get()
? -1d
: 0,
PlatformThread.sync(tabModel.getBusy()),
AppPrefs.get().performanceMode()));
var loading = new LoadingIconComp(tabModel.getBusy(), AppFontSizes::base);
loading.prefWidth(16);
loading.prefHeight(16);
var image = tabModel.getIcon();
var logo = PrettyImageHelper.ofFixedSizeSquare(image, 16).createRegion();
var logo = PrettyImageHelper.ofFixedSizeSquare(image, 16);
logo.apply(struc -> {
struc.get()
.opacityProperty()
.bind(PlatformThread.sync(Bindings.createDoubleBinding(
() -> {
return !tabModel.getBusy().get() ? 1.0 : 0.15;
},
tabModel.getBusy())));
});
tab.graphicProperty()
.bind(Bindings.createObjectBinding(
() -> {
return tabModel.getBusy().get() ? ring : logo;
},
PlatformThread.sync(tabModel.getBusy())));
var stack = new StackComp(List.of(logo, loading));
tab.setGraphic(stack.createRegion());
}
if (tabModel.getBrowserModel() instanceof BrowserFullSessionModel sessionModel) {
@@ -460,13 +457,15 @@ public class BrowserSessionTabsComp extends SimpleComp {
Comp<?> comp = tabModel.comp();
var compRegion = comp.createRegion();
var empty = new StackPane();
empty.setMinWidth(450);
empty.setMinWidth(0);
empty.widthProperty().addListener((observable, oldValue, newValue) -> {
if (tabModel.isCloseable() && tabs.getSelectionModel().getSelectedItem() == tab) {
rightPadding.setValue(newValue.doubleValue());
}
});
var split = new SplitPane(compRegion);
if (tabModel.isCloseable()) {
split.getItems().add(empty);
@@ -496,6 +495,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
if (newValue != null) {
Platform.runLater(() -> {
Label l = (Label) tabs.lookup("#" + id + " .tab-label");
l.setGraphicTextGap(7);
var w = l.maxWidthProperty();
l.minWidthProperty().bind(w);
l.prefWidthProperty().bind(w);
@@ -512,10 +512,11 @@ public class BrowserSessionTabsComp extends SimpleComp {
if (color != null) {
c.getStyleClass().add(color.getId());
}
c.addEventHandler(
DragEvent.DRAG_ENTERED,
mouseEvent -> Platform.runLater(
() -> tabs.getSelectionModel().select(tab)));
c.addEventHandler(DragEvent.DRAG_ENTERED, mouseEvent -> {
if (tabModel.isCloseable()) {
Platform.runLater(() -> tabs.getSelectionModel().select(tab));
}
});
});
}
});

View File

@@ -1,118 +1,112 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.browser.BrowserStoreSessionTab;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.util.ModuleLayerLoader;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.hub.action.StoreAction;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.store.FileSystemStore;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import javafx.scene.control.MenuItem;
import javafx.scene.input.KeyCombination;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.experimental.SuperBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
public interface BrowserAction {
@SuperBuilder
public abstract class BrowserAction extends StoreAction<FileSystemStore> {
List<BrowserAction> ALL = new ArrayList<>();
protected final List<FilePath> files;
static List<BrowserLeafAction> getFlattened(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return ALL.stream()
.map(browserAction -> getFlattened(browserAction, model, entries))
.flatMap(List::stream)
@JsonIgnore
protected BrowserFileSystemTabModel model;
@JsonIgnore
private List<BrowserEntry> entries;
@Override
protected boolean beforeExecute() throws Exception {
AppLayoutModel.get().selectBrowser();
if (model == null) {
var found = BrowserFullSessionModel.DEFAULT.getAllTabs().stream()
.filter(t -> t instanceof BrowserStoreSessionTab<?> bs
&& bs.getEntry().equals(ref))
.findFirst();
if (found.isPresent()) {
model = (BrowserFileSystemTabModel) found.get();
} else {
model = BrowserFullSessionModel.DEFAULT.openFileSystemSync(
ref.asNeeded(),
model -> {
var isFile = model.getFileSystem().fileExists(files.getFirst());
if (isFile) {
return files.getFirst().getParent();
} else {
var dir = files.getFirst().getParent();
if (!model.getFileSystem().directoryExists(dir)) {
throw new IllegalArgumentException("Directory does not exist: " + dir);
}
return dir;
}
},
null,
true);
}
}
model.getBusy().set(true);
// Start shell in case we exited
model.getFileSystem().getShell().orElseThrow().start();
return true;
}
@Override
protected void afterExecute() {
model.getBusy().set(false);
}
protected List<BrowserEntry> getEntries() {
if (entries != null) {
return entries;
}
entries = files.stream()
.map(filePath -> {
var be = model.getFileList().getAll().getValue().stream()
.filter(browserEntry ->
browserEntry.getRawFileEntry().getPath().equals(filePath))
.findFirst();
if (be.isPresent()) {
return be.get();
}
return null;
})
.filter(browserEntry -> browserEntry != null)
.toList();
return entries;
}
static List<BrowserLeafAction> getFlattened(
BrowserAction browserAction, BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return browserAction instanceof BrowserLeafAction
? List.of((BrowserLeafAction) browserAction)
: ((BrowserBranchAction) browserAction)
.getBranchingActions(model, entries).stream()
.map(action -> getFlattened(action, model, entries))
.flatMap(List::stream)
.toList();
}
public abstract static class BrowserActionBuilder<C extends BrowserAction, B extends BrowserActionBuilder<C, B>>
extends StoreActionBuilder<FileSystemStore, C, B> {
static BrowserLeafAction byId(String id, BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return getFlattened(model, entries).stream()
.filter(browserAction -> id.equals(browserAction.getId()))
.findAny()
.orElseThrow();
}
default List<BrowserEntry> resolveFilesIfNeeded(List<BrowserEntry> selected) {
return automaticallyResolveLinks()
? selected.stream()
.map(browserEntry ->
new BrowserEntry(browserEntry.getRawFileEntry().resolved(), browserEntry.getModel()))
.toList()
: selected;
}
MenuItem toMenuItem(BrowserFileSystemTabModel model, List<BrowserEntry> selected);
default void init(BrowserFileSystemTabModel model) throws Exception {}
default String getProFeatureId() {
return null;
}
default Node getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return null;
}
default Category getCategory() {
return null;
}
default KeyCombination getShortcut() {
return null;
}
default boolean acceptsEmptySelection() {
return false;
}
ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries);
default boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return true;
}
default boolean automaticallyResolveLinks() {
return true;
}
default boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return true;
}
enum Category {
CUSTOM,
OPEN,
NATIVE,
COPY_PASTE,
MUTATION
}
class Loader implements ModuleLayerLoader {
@Override
public void init(ModuleLayer layer) {
ALL.addAll(ServiceLoader.load(layer, BrowserAction.class).stream()
.map(actionProviderProvider -> actionProviderProvider.get())
.filter(provider -> {
try {
return true;
} catch (Throwable e) {
ErrorEvent.fromThrowable(e).handle();
return false;
}
})
public void initEntries(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
ref(model.getEntry().asNeeded());
model(model);
files(entries.stream()
.map(browserEntry -> browserEntry.getRawFileEntry().getPath())
.toList());
entries(entries);
}
public void initFiles(BrowserFileSystemTabModel model, List<FilePath> entries) {
ref(model.getEntry().asNeeded());
model(model);
files(entries);
}
}
}

View File

@@ -1,25 +0,0 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.file.BrowserEntry;
import java.util.List;
public class BrowserActionFormatter {
public static String filesArgument(List<BrowserEntry> entries) {
return entries.size() == 1 ? entries.getFirst().getFileName() : "(" + entries.size() + ")";
}
public static String centerEllipsis(String input, int length) {
if (input == null) {
return "";
}
if (input.length() <= length) {
return input;
}
var half = (length / 2) - 5;
return input.substring(0, half) + " ... " + input.substring(input.length() - half);
}
}

View File

@@ -0,0 +1,18 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.action.ActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import java.util.List;
public interface BrowserActionProvider extends ActionProvider {
default boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return true;
}
default boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return true;
}
}

View File

@@ -0,0 +1,13 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.action.ActionProvider;
public class BrowserActionProviders {
public static BrowserActionProvider forClass(Class<? extends BrowserActionProvider> clazz) {
return (BrowserActionProvider) ActionProvider.ALL.stream()
.filter(actionProvider -> actionProvider.getClass().equals(clazz))
.findFirst()
.orElseThrow();
}
}

View File

@@ -1,114 +0,0 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.comp.base.TooltipHelper;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.LicenseProvider;
import io.xpipe.app.util.ThreadHelper;
import javafx.scene.control.Button;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Region;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public interface BrowserLeafAction extends BrowserAction {
void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) throws Exception;
default Button toButton(Region root, BrowserFileSystemTabModel model, List<BrowserEntry> selected) {
var b = new Button();
b.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(model.getBusy(), () -> {
if (model.getFileSystem() == null) {
return;
}
// Start shell in case we exited
model.getFileSystem().getShell().orElseThrow().start();
execute(model, selected);
});
});
event.consume();
});
var name = getName(model, selected);
Tooltip.install(b, TooltipHelper.create(name, getShortcut()));
var graphic = getIcon(model, selected);
if (graphic != null) {
b.setGraphic(graphic);
}
b.setMnemonicParsing(false);
b.accessibleTextProperty().bind(name);
root.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (getShortcut() != null && getShortcut().match(event)) {
b.fire();
event.consume();
}
});
b.setDisable(!isActive(model, selected));
model.getCurrentPath().addListener((observable, oldValue, newValue) -> {
b.setDisable(!isActive(model, selected));
});
if (getProFeatureId() != null
&& !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) {
b.setDisable(true);
b.setGraphic(new FontIcon("mdi2p-professional-hexagon"));
}
return b;
}
default MenuItem toMenuItem(BrowserFileSystemTabModel model, List<BrowserEntry> selected) {
var name = getName(model, selected);
var mi = new MenuItem();
mi.textProperty().bind(BindingsHelper.map(name, s -> {
if (getProFeatureId() != null) {
return LicenseProvider.get().getFeature(getProFeatureId()).suffix(s);
}
return s;
}));
mi.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(model.getBusy(), () -> {
if (model.getFileSystem() == null) {
return;
}
// Start shell in case we exited
model.getFileSystem().getShell().orElseThrow().start();
execute(model, selected);
});
});
event.consume();
});
if (getShortcut() != null) {
mi.setAccelerator(getShortcut());
}
var graphic = getIcon(model, selected);
if (graphic != null) {
mi.setGraphic(graphic);
}
mi.setMnemonicParsing(false);
mi.setDisable(!isActive(model, selected));
if (getProFeatureId() != null
&& !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) {
mi.setDisable(true);
}
return mi;
}
default String getId() {
return null;
}
}

View File

@@ -0,0 +1,55 @@
package io.xpipe.app.browser.action.impl;
import io.xpipe.app.action.AbstractAction;
import io.xpipe.app.action.ActionProvider;
import io.xpipe.app.browser.file.BrowserFileOutput;
import lombok.NonNull;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.Map;
public class ApplyFileEditActionProvider implements ActionProvider {
@Override
public String getId() {
return "applyFileEdit";
}
@Jacksonized
@SuperBuilder
public static class Action extends AbstractAction {
@NonNull
String target;
@NonNull
InputStream input;
@NonNull
BrowserFileOutput output;
@Override
public boolean isMutation() {
return true;
}
@Override
public void executeImpl() throws Exception {
try (var out = output.open()) {
input.transferTo(out);
}
}
@Override
public Map<String, String> toDisplayMap() {
var map = new LinkedHashMap<String, String>();
map.put("action", getDisplayName());
map.put("target", target);
return map;
}
}
}

View File

@@ -0,0 +1,49 @@
package io.xpipe.app.browser.action.impl;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.util.DesktopHelper;
import io.xpipe.app.util.LocalShell;
import io.xpipe.core.process.ShellControl;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
public class BrowseInNativeManagerActionProvider implements BrowserActionProvider {
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@Override
public void executeImpl() throws Exception {
ShellControl sc = model.getFileSystem().getShell().orElseThrow();
for (BrowserEntry entry : getEntries()) {
var e = entry.getRawFileEntry().getPath();
var localFile = sc.getLocalSystemAccess().translateToLocalSystemPath(e);
try (var local = LocalShell.getShell().start()) {
DesktopHelper.browsePathRemote(
local, localFile, entry.getRawFileEntry().getKind());
}
}
}
}
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return model.getFileSystem()
.getShell()
.orElseThrow()
.getLocalSystemAccess()
.supportsFileSystemAccess();
}
@Override
public String getId() {
return "browseInNativeFileManager";
}
}

View File

@@ -0,0 +1,61 @@
package io.xpipe.app.browser.action.impl;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.OsType;
import lombok.NonNull;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
public class ChgrpActionProvider implements BrowserActionProvider {
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var os = model.getFileSystem().getShell().orElseThrow().getOsType();
return os != OsType.WINDOWS && os != OsType.MACOS;
}
@Override
public String getId() {
return "chgrp";
}
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@NonNull
private final String group;
private final boolean recursive;
@Override
public boolean isMutation() {
return true;
}
@Override
public void executeImpl() throws Exception {
model.getFileSystem()
.getShell()
.orElseThrow()
.executeSimpleCommand(CommandBuilder.of()
.add("chgrp")
.addIf(recursive, "-R")
.addLiteral(group)
.addFiles(getEntries().stream()
.map(browserEntry -> browserEntry
.getRawFileEntry()
.getPath()
.toString())
.toList()));
model.refreshEntriesSync(getEntries());
}
}
}

View File

@@ -0,0 +1,60 @@
package io.xpipe.app.browser.action.impl;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.OsType;
import lombok.NonNull;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
public class ChmodActionProvider implements BrowserActionProvider {
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS;
}
@Override
public String getId() {
return "chmod";
}
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@NonNull
private final String permissions;
private final boolean recursive;
@Override
public boolean isMutation() {
return true;
}
@Override
public void executeImpl() throws Exception {
model.getFileSystem()
.getShell()
.orElseThrow()
.executeSimpleCommand(CommandBuilder.of()
.add("chmod")
.addIf(recursive, "-R")
.addLiteral(permissions)
.addFiles(getEntries().stream()
.map(browserEntry -> browserEntry
.getRawFileEntry()
.getPath()
.toString())
.toList()));
model.refreshEntriesSync(getEntries());
}
}
}

View File

@@ -0,0 +1,61 @@
package io.xpipe.app.browser.action.impl;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.OsType;
import lombok.NonNull;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
public class ChownActionProvider implements BrowserActionProvider {
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var os = model.getFileSystem().getShell().orElseThrow().getOsType();
return os != OsType.WINDOWS && os != OsType.MACOS;
}
@Override
public String getId() {
return "chown";
}
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@NonNull
private final String owner;
private final boolean recursive;
@Override
public boolean isMutation() {
return true;
}
@Override
public void executeImpl() throws Exception {
model.getFileSystem()
.getShell()
.orElseThrow()
.executeSimpleCommand(CommandBuilder.of()
.add("chown")
.addIf(recursive, "-R")
.addLiteral(owner)
.addFiles(getEntries().stream()
.map(browserEntry -> browserEntry
.getRawFileEntry()
.getPath()
.toString())
.toList()));
model.refreshSync();
}
}
}

View File

@@ -0,0 +1,42 @@
package io.xpipe.app.browser.action.impl;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.core.store.FileKind;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
public class ComputeDirectorySizesActionProvider implements BrowserActionProvider {
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@Override
public void executeImpl() throws Exception {
var entries = getEntries();
if (entries.size() == 1 && entries.getFirst().getRawFileEntry().equals(model.getCurrentDirectory())) {
entries = model.getFileList().getAll().getValue();
}
for (BrowserEntry be : entries) {
if (be.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY) {
continue;
}
var size = model.getFileSystem()
.getDirectorySize(be.getRawFileEntry().resolved().getPath());
var fileEntry = be.getRawFileEntry();
fileEntry.resolved().setSize("" + size);
model.getFileList().updateEntry(be.getRawFileEntry().getPath(), fileEntry);
}
}
}
@Override
public String getId() {
return "computeDirectorySizes";
}
}

View File

@@ -0,0 +1,34 @@
package io.xpipe.app.browser.action.impl;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.file.*;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
public class DeleteActionProvider implements BrowserActionProvider {
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@Override
public boolean isMutation() {
return true;
}
@Override
public void executeImpl() throws Exception {
var toDelete =
getEntries().stream().map(entry -> entry.getRawFileEntry()).toList();
BrowserFileSystemHelper.delete(toDelete);
model.refreshSync();
}
}
@Override
public String getId() {
return "deleteFile";
}
}

View File

@@ -0,0 +1,36 @@
package io.xpipe.app.browser.action.impl;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.core.store.FilePath;
import lombok.NonNull;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
public class MoveFileActionProvider implements BrowserActionProvider {
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@NonNull
FilePath target;
@Override
public boolean isMutation() {
return true;
}
@Override
public void executeImpl() throws Exception {
model.getFileSystem().move(getEntries().getFirst().getRawFileEntry().getPath(), target);
model.refreshSync();
}
}
@Override
public String getId() {
return "moveFile";
}
}

View File

@@ -0,0 +1,44 @@
package io.xpipe.app.browser.action.impl;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.core.store.FileKind;
import lombok.NonNull;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
public class NewDirectoryActionProvider implements BrowserActionProvider {
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@NonNull
String name;
@Override
public boolean isMutation() {
return true;
}
@Override
public void executeImpl() throws Exception {
for (BrowserEntry entry : getEntries()) {
if (entry.getRawFileEntry().getKind() != FileKind.DIRECTORY) {
continue;
}
var file = entry.getRawFileEntry().getPath().join(name);
model.getFileSystem().mkdirs(file);
}
model.refreshSync();
}
}
@Override
public String getId() {
return "newDirectory";
}
}

View File

@@ -0,0 +1,44 @@
package io.xpipe.app.browser.action.impl;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.core.store.FileKind;
import lombok.NonNull;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
public class NewFileActionProvider implements BrowserActionProvider {
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@NonNull
String name;
@Override
public boolean isMutation() {
return true;
}
@Override
public void executeImpl() throws Exception {
for (BrowserEntry entry : getEntries()) {
if (entry.getRawFileEntry().getKind() != FileKind.DIRECTORY) {
continue;
}
var file = entry.getRawFileEntry().getPath().join(name);
model.getFileSystem().touch(file);
}
model.refreshSync();
}
}
@Override
public String getId() {
return "newFile";
}
}

View File

@@ -0,0 +1,48 @@
package io.xpipe.app.browser.action.impl;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FilePath;
import lombok.NonNull;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
public class NewLinkActionProvider implements BrowserActionProvider {
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@NonNull
String name;
@NonNull
FilePath target;
@Override
public boolean isMutation() {
return true;
}
@Override
public void executeImpl() throws Exception {
for (BrowserEntry entry : getEntries()) {
if (entry.getRawFileEntry().getKind() != FileKind.DIRECTORY) {
continue;
}
var file = entry.getRawFileEntry().getPath().join(name);
model.getFileSystem().symbolicLink(file, target);
}
model.refreshSync();
}
}
@Override
public String getId() {
return "newLink";
}
}

View File

@@ -0,0 +1,37 @@
package io.xpipe.app.browser.action.impl;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.core.store.FileKind;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
public class OpenDirectoryActionProvider implements BrowserActionProvider {
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@Override
public void executeImpl() {
var first = getEntries().getFirst();
model.cdSync(first.getRawFileEntry().getPath().toString());
}
}
@Override
public String getId() {
return "openDirectory";
}
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return entries.size() == 1
&& entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY);
}
}

View File

@@ -0,0 +1,39 @@
package io.xpipe.app.browser.action.impl;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileOpener;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.core.store.FileKind;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
public class OpenFileDefaultActionProvider implements BrowserActionProvider {
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@Override
public void executeImpl() {
for (var entry : getEntries()) {
BrowserFileOpener.openInDefaultApplication(model, entry.getRawFileEntry());
}
}
}
@Override
public String getId() {
return "openFileDefault";
}
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return model.getFileList().getEditing().getValue() == null
&& entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE);
}
}

View File

@@ -0,0 +1,97 @@
package io.xpipe.app.browser.action.impl;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.util.LocalShell;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.FileKind;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
public class OpenFileNativeDetailsActionProvider implements BrowserActionProvider {
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@Override
public void executeImpl() throws Exception {
ShellControl sc = model.getFileSystem().getShell().get();
for (BrowserEntry entry : getEntries()) {
var e = entry.getRawFileEntry().getPath();
var localFile = sc.getLocalSystemAccess().translateToLocalSystemPath(e);
switch (OsType.getLocal()) {
case OsType.Windows windows -> {
var parent = localFile.getParent();
// If we execute this on a drive root there will be no parent, so we have to check for that!
var content = parent != null
? String.format(
"$shell = New-Object -ComObject Shell.Application; $shell.NameSpace('%s').ParseName('%s').InvokeVerb('Properties')",
parent, localFile.getFileName())
: String.format(
"$shell = New-Object -ComObject Shell.Application; $shell.NameSpace('%s').Self.InvokeVerb('Properties')",
localFile);
// The Windows shell invoke verb functionality behaves kinda weirdly and only shows the window
// as
// long as the parent process is running.
// So let's keep one process running
LocalShell.getLocalPowershell()
.command(content)
.notComplex()
.execute();
}
case OsType.Linux linux -> {
var dbus = String.format(
"""
dbus-send --session --print-reply --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItemProperties array:string:"file://%s" string:""
""",
localFile);
var success = sc.executeSimpleBooleanCommand(dbus);
if (success) {
return;
}
sc.command(CommandBuilder.of()
.add("xdg-open")
.addFile(
entry.getRawFileEntry().getKind() == FileKind.DIRECTORY
? e
: e.getParent()))
.execute();
}
case OsType.MacOs macOs -> {
sc.osascriptCommand(String.format(
"""
set fileEntry to (POSIX file "%s") as text
tell application "Finder"
activate
open information window of alias fileEntry
end tell
""",
localFile))
.execute();
}
}
}
}
}
@Override
public String getId() {
return "openFileNativeDetails";
}
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var sc = model.getFileSystem().getShell().orElseThrow();
return sc.getLocalSystemAccess().supportsFileSystemAccess();
}
}

View File

@@ -0,0 +1,41 @@
package io.xpipe.app.browser.action.impl;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileOpener;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileKind;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
public class OpenFileWithActionProvider implements BrowserActionProvider {
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@Override
public void executeImpl() {
for (var entry : getEntries()) {
BrowserFileOpener.openWithAnyApplication(model, entry.getRawFileEntry());
}
}
}
@Override
public String getId() {
return "openFileWith";
}
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return OsType.getLocal().equals(OsType.WINDOWS)
&& entries.size() == 1
&& entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE);
}
}

View File

@@ -0,0 +1,56 @@
package io.xpipe.app.browser.action.impl;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FilePath;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.Collections;
import java.util.List;
public class OpenTerminalActionProvider implements BrowserActionProvider {
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@Override
public void executeImpl() throws Exception {
var entries = getEntries();
var dirs = entries.size() > 0
? entries.stream()
.map(browserEntry -> browserEntry.getRawFileEntry().getPath())
.toList()
: model.getCurrentDirectory() != null
? List.of(model.getCurrentDirectory().getPath())
: Collections.singletonList((FilePath) null);
for (var dir : dirs) {
var name = (dir != null ? dir + " - " : "") + model.getName().getValue();
model.openTerminalSync(
name, dir, model.getFileSystem().getShell().orElseThrow(), dirs.size() == 1);
}
}
}
@Override
public String getId() {
return "openTerminalInDirectory";
}
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY);
}
@Override
public boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var t = AppPrefs.get().terminalType().getValue();
return t != null;
}
}

View File

@@ -0,0 +1,63 @@
package io.xpipe.app.browser.action.impl;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.ProcessOutputException;
import lombok.NonNull;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.concurrent.atomic.AtomicReference;
public class RunCommandInBackgroundActionProvider implements BrowserActionProvider {
@Override
public String getId() {
return "runFileInBackground";
}
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@NonNull
String command;
@Override
public boolean isMutation() {
return true;
}
@Override
public void executeImpl() throws Exception {
var cmd = CommandBuilder.of().addFile(command);
for (BrowserEntry entry : getEntries()) {
cmd.addFile(entry.getRawFileEntry().getPath());
}
AtomicReference<String> out = new AtomicReference<>();
AtomicReference<String> err = new AtomicReference<>();
long exitCode;
try (var command = model.getFileSystem()
.getShell()
.orElseThrow()
.command(cmd)
.withWorkingDirectory(model.getCurrentDirectory().getPath())
.start()) {
var r = command.readStdoutAndStderr();
out.set(r[0]);
err.set(r[1]);
exitCode = command.getExitCode();
}
// Only throw actual error output
if (exitCode != 0) {
throw ErrorEventFactory.expected(ProcessOutputException.of(exitCode, out.get(), err.get()));
}
}
}
}

View File

@@ -0,0 +1,43 @@
package io.xpipe.app.browser.action.impl;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.util.CommandDialog;
import io.xpipe.core.process.CommandBuilder;
import lombok.NonNull;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
public class RunCommandInBrowserActionProvider implements BrowserActionProvider {
@Override
public String getId() {
return "runCommandInBrowser";
}
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@NonNull
String command;
@Override
public boolean isMutation() {
return true;
}
@Override
public void executeImpl() {
var builder = CommandBuilder.of().addFile(command);
for (BrowserEntry entry : getEntries()) {
builder.addFile(entry.getRawFileEntry().getPath());
}
var cmd = model.getFileSystem().getShell().orElseThrow().command(builder);
CommandDialog.runAsyncAndShow(cmd);
}
}
}

View File

@@ -0,0 +1,50 @@
package io.xpipe.app.browser.action.impl;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.core.process.CommandBuilder;
import lombok.NonNull;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
public class RunCommandInTerminalActionProvider implements BrowserActionProvider {
@Override
public String getId() {
return "runCommandInTerminal";
}
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@NonNull
String title;
@NonNull
String command;
@Override
public boolean isMutation() {
return true;
}
@Override
public void executeImpl() throws Exception {
var cmd = CommandBuilder.of().addFile(command);
for (BrowserEntry entry : getEntries()) {
cmd.addFile(entry.getRawFileEntry().getPath());
}
model.openTerminalSync(
title,
model.getCurrentDirectory() != null
? model.getCurrentDirectory().getPath()
: null,
model.getFileSystem().getShell().orElseThrow().command(cmd),
true);
}
}
}

View File

@@ -0,0 +1,69 @@
package io.xpipe.app.browser.action.impl;
import io.xpipe.app.action.AbstractAction;
import io.xpipe.app.action.ActionProvider;
import io.xpipe.app.action.StoreContextAction;
import io.xpipe.app.browser.file.BrowserFileTransferOperation;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.FileSystemStore;
import lombok.NonNull;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class TransferFilesActionProvider implements ActionProvider {
@Override
public String getId() {
return "transferFiles";
}
@Jacksonized
@SuperBuilder
public static class Action extends AbstractAction implements StoreContextAction {
@NonNull
DataStoreEntryRef<FileSystemStore> target;
@NonNull
BrowserFileTransferOperation operation;
boolean download;
@Override
public boolean isMutation() {
return !download;
}
@Override
public void executeImpl() throws Exception {
operation.execute();
}
@Override
public Map<String, String> toDisplayMap() {
var map = new LinkedHashMap<String, String>();
map.put("action", getDisplayName());
map.put(
"sources",
operation.getFiles().stream()
.map(fileEntry -> fileEntry.getName())
.collect(Collectors.joining("\n")));
map.put("target", DataStorage.get().getStoreEntryDisplayName(target.get()));
map.put("targetDirectory", operation.getTarget().getPath().toString());
return map;
}
@Override
public List<DataStoreEntry> getStoreEntryContext() {
return List.of(target.get());
}
}
}

View File

@@ -1,61 +1,45 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.comp.base.ModalButton;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FilePath;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.stream.Collectors;
public class BrowserAlerts {
public static FileConflictChoice showFileConflictAlert(FilePath file, boolean multiple) {
var map = new LinkedHashMap<ButtonType, FileConflictChoice>();
map.put(new ButtonType(AppI18n.get("cancel"), ButtonBar.ButtonData.CANCEL_CLOSE), FileConflictChoice.CANCEL);
if (multiple) {
map.put(new ButtonType(AppI18n.get("skip"), ButtonBar.ButtonData.OTHER), FileConflictChoice.SKIP);
map.put(new ButtonType(AppI18n.get("skipAll"), ButtonBar.ButtonData.OTHER), FileConflictChoice.SKIP_ALL);
}
map.put(new ButtonType(AppI18n.get("replace"), ButtonBar.ButtonData.OTHER), FileConflictChoice.REPLACE);
if (multiple) {
map.put(
new ButtonType(AppI18n.get("replaceAll"), ButtonBar.ButtonData.OTHER),
FileConflictChoice.REPLACE_ALL);
}
map.put(new ButtonType(AppI18n.get("rename"), ButtonBar.ButtonData.OTHER), FileConflictChoice.RENAME);
if (multiple) {
map.put(
new ButtonType(AppI18n.get("renameAll"), ButtonBar.ButtonData.OTHER),
FileConflictChoice.RENAME_ALL);
}
var choice = new SimpleObjectProperty<FileConflictChoice>();
var key = multiple ? "fileConflictAlertContentMultiple" : "fileConflictAlertContent";
var w = multiple ? 700 : 400;
return AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("fileConflictAlertTitle"));
alert.setHeaderText(AppI18n.get("fileConflictAlertHeader"));
alert.setAlertType(Alert.AlertType.CONFIRMATION);
alert.getButtonTypes().clear();
alert.getDialogPane()
.setContent(AppWindowHelper.alertContentText(
AppI18n.get(
multiple ? "fileConflictAlertContentMultiple" : "fileConflictAlertContent",
file),
w - 50));
alert.getDialogPane().setMinWidth(w);
alert.getDialogPane().setPrefWidth(w);
alert.getDialogPane().setMaxWidth(w);
map.sequencedKeySet()
.forEach(buttonType -> alert.getButtonTypes().add(buttonType));
})
.map(map::get)
.orElse(FileConflictChoice.CANCEL);
var modal = ModalOverlay.of(
"fileConflictAlertTitle",
AppDialog.dialogText(AppI18n.observable(key, file)).prefWidth(w));
modal.addButton(new ModalButton("cancel", () -> choice.set(FileConflictChoice.CANCEL), true, false));
if (multiple) {
modal.addButton(new ModalButton("skip", () -> choice.set(FileConflictChoice.SKIP), true, false));
modal.addButton(new ModalButton("skipAll", () -> choice.set(FileConflictChoice.SKIP_ALL), true, false));
}
modal.addButton(new ModalButton("replace", () -> choice.set(FileConflictChoice.REPLACE), true, false));
if (multiple) {
modal.addButton(
new ModalButton("replaceAll", () -> choice.set(FileConflictChoice.REPLACE_ALL), true, false));
}
modal.addButton(new ModalButton("rename", () -> choice.set(FileConflictChoice.RENAME), true, false));
if (multiple) {
modal.addButton(new ModalButton("renameAll", () -> choice.set(FileConflictChoice.RENAME_ALL), true, false));
}
modal.showAndWait();
return choice.get() != null ? choice.get() : FileConflictChoice.CANCEL;
}
public static boolean showMoveAlert(List<FileEntry> source, FileEntry target) {
@@ -74,25 +58,6 @@ public class BrowserAlerts {
.orElse(false);
}
public static boolean showDeleteAlert(BrowserFileSystemTabModel model, List<FileEntry> source) {
var config =
DataStorage.get().getEffectiveCategoryConfig(model.getEntry().get());
if (!Boolean.TRUE.equals(config.getConfirmAllModifications())
&& source.stream().noneMatch(entry -> entry.getKind() == FileKind.DIRECTORY)) {
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<FileEntry> source) {
var namesHeader = AppI18n.get("selectedElements");
var names = namesHeader + "\n"

View File

@@ -2,7 +2,7 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FilePath;
import javafx.scene.Node;
import javafx.scene.control.Button;
@@ -14,6 +14,7 @@ import javafx.util.Callback;
import atlantafx.base.controls.Breadcrumbs;
import java.util.ArrayList;
import java.util.List;
public class BrowserBreadcrumbBar extends SimpleComp {
@@ -26,7 +27,7 @@ public class BrowserBreadcrumbBar extends SimpleComp {
@Override
protected Region createSimple() {
Callback<Breadcrumbs.BreadCrumbItem<String>, ButtonBase> crumbFactory = crumb -> {
var name = crumb.getValue().equals("/") ? "/" : FileNames.getFileName(crumb.getValue());
var name = crumb.getValue().equals("/") ? "/" : FilePath.of(crumb.getValue()).getFileName();
var btn = new Button(name, null);
btn.setMnemonicParsing(false);
btn.setFocusTraversable(false);
@@ -41,37 +42,39 @@ public class BrowserBreadcrumbBar extends SimpleComp {
var breadcrumbs = new Breadcrumbs<String>();
breadcrumbs.setMinWidth(0);
PlatformThread.sync(model.getCurrentPath()).subscribe(val -> {
if (val == null) {
breadcrumbs.setSelectedCrumb(null);
return;
}
model.getCurrentPath().subscribe(val -> {
PlatformThread.runLaterIfNeeded(() -> {
if (val == null) {
breadcrumbs.setSelectedCrumb(null);
return;
}
var sc = model.getFileSystem().getShell();
if (sc.isEmpty()) {
breadcrumbs.setDividerFactory(item -> item != null && !item.isLast() ? new Label("/") : null);
} else {
breadcrumbs.setDividerFactory(item -> {
if (item == null) {
return null;
}
var sc = model.getFileSystem().getShell();
if (sc.isEmpty()) {
breadcrumbs.setDividerFactory(item -> item != null && !item.isLast() ? new Label("/") : null);
} else {
breadcrumbs.setDividerFactory(item -> {
if (item == null) {
return null;
}
if (item.isFirst() && item.getValue().equals("/")) {
return new Label("");
}
if (item.isFirst() && item.getValue().equals("/")) {
return new Label("");
}
return new Label(sc.get().getOsType().getFileSystemSeparator());
});
}
return new Label(sc.get().getOsType().getFileSystemSeparator());
});
}
var elements = val.splitHierarchy();
var modifiedElements = new ArrayList<>(elements);
if (val.toString().startsWith("/")) {
modifiedElements.addFirst("/");
}
Breadcrumbs.BreadCrumbItem<String> items =
Breadcrumbs.buildTreeModel(modifiedElements.toArray(String[]::new));
breadcrumbs.setSelectedCrumb(items);
var elements = createBreadcumbHierarchy(val);
var modifiedElements = new ArrayList<>(elements);
if (val.toString().startsWith("/")) {
modifiedElements.addFirst("/");
}
Breadcrumbs.BreadCrumbItem<String> items =
Breadcrumbs.buildTreeModel(modifiedElements.toArray(String[]::new));
breadcrumbs.setSelectedCrumb(items);
});
});
if (crumbFactory != null) {
@@ -87,4 +90,20 @@ public class BrowserBreadcrumbBar extends SimpleComp {
return breadcrumbs;
}
private List<String> createBreadcumbHierarchy(FilePath filePath) {
var f = filePath.toString() + "/";
var list = new ArrayList<String>();
int lastElementStart = 0;
for (int i = 0; i < f.length(); i++) {
if (f.charAt(i) == '\\' || f.charAt(i) == '/') {
if (i - lastElementStart > 0) {
list.add(f.substring(0, i));
}
lastElementStart = i + 1;
}
}
return list;
}
}

View File

@@ -1,11 +1,9 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.util.GlobalClipboard;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.util.FailableRunnable;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
@@ -16,7 +14,6 @@ import javafx.scene.input.Dragboard;
import lombok.SneakyThrows;
import lombok.Value;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.io.File;
@@ -45,7 +42,10 @@ public class BrowserClipboard {
List<File> data = (List<File>) clipboard.getData(DataFlavor.javaFileListFlavor);
// Sometimes file data can contain invalid chars. Why?
var files = data.stream().filter(file -> file.toString().chars().noneMatch(value -> Character.isISOControl(value))).map(f -> f.toPath()).toList();
var files = data.stream()
.filter(file -> file.toString().chars().noneMatch(value -> Character.isISOControl(value)))
.map(f -> f.toPath())
.toList();
if (files.size() == 0) {
return;
}
@@ -55,9 +55,10 @@ public class BrowserClipboard {
entries.add(BrowserLocalFileSystem.getLocalBrowserEntry(file));
}
currentCopyClipboard.setValue(new Instance(UUID.randomUUID(), null, entries, BrowserFileTransferMode.COPY));
currentCopyClipboard.setValue(
new Instance(UUID.randomUUID(), null, entries, BrowserFileTransferMode.COPY));
} catch (Exception e) {
ErrorEvent.fromThrowable(e).expected().omit().handle();
ErrorEventFactory.fromThrowable(e).expected().omit().handle();
}
}
});

View File

@@ -3,7 +3,7 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.store.*;
import io.xpipe.app.hub.comp.*;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.PlatformThread;
@@ -73,7 +73,8 @@ public final class BrowserConnectionListComp extends SimpleComp {
filter,
category,
StoreViewState.get().getEntriesListVisibilityObservable(),
StoreViewState.get().getEntriesListUpdateObservable()),
StoreViewState.get().getEntriesListUpdateObservable(),
new ReadOnlyBooleanWrapper(true)),
augment,
selectedAction -> {
BooleanProperty busy = new SimpleBooleanProperty(false);

View File

@@ -3,9 +3,9 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.FilterComp;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.store.StoreCategoryWrapper;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.hub.comp.StoreCategoryWrapper;
import io.xpipe.app.hub.comp.StoreViewState;
import io.xpipe.app.util.DataStoreCategoryChoiceComp;
import javafx.beans.property.Property;

View File

@@ -1,6 +1,8 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.action.ActionProvider;
import io.xpipe.app.browser.menu.BrowserMenuCategory;
import io.xpipe.app.browser.menu.BrowserMenuItemProvider;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.util.InputHelper;
@@ -44,8 +46,10 @@ public final class BrowserContextMenu extends ContextMenu {
return;
}
for (BrowserAction.Category cat : BrowserAction.Category.values()) {
var all = BrowserAction.ALL.stream()
for (var cat : BrowserMenuCategory.values()) {
var all = ActionProvider.ALL.stream()
.map(actionProvider -> actionProvider instanceof BrowserMenuItemProvider ba ? ba : null)
.filter(browserActionProvider -> browserActionProvider != null)
.filter(browserAction -> browserAction.getCategory() == cat)
.filter(browserAction -> {
if (model.isClosed()) {
@@ -72,13 +76,16 @@ public final class BrowserContextMenu extends ContextMenu {
getItems().add(new SeparatorMenuItem());
}
for (BrowserAction a : all) {
for (var a : all) {
if (model.isClosed()) {
return;
}
var used = a.resolveFilesIfNeeded(selected);
getItems().add(a.toMenuItem(model, used));
var item = a.toMenuItem(model, used);
if (item != null) {
getItems().add(item);
}
}
}
}

View File

@@ -1,6 +1,6 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.menu.BrowserMenuProviders;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.*;
@@ -29,6 +29,7 @@ import java.time.Instant;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import static io.xpipe.app.util.HumanReadableFormat.byteCount;
import static javafx.scene.control.TableColumn.SortType.ASCENDING;
@@ -76,7 +77,6 @@ public final class BrowserFileListComp extends SimpleComp {
param.getValue().getRawFileEntry().resolved().getSize()));
sizeCol.setCellFactory(col -> new FileSizeCell());
sizeCol.setResizable(false);
sizeCol.setPrefWidth(120);
sizeCol.setReorderable(false);
var mtimeCol = new TableColumn<BrowserEntry, Instant>();
@@ -124,13 +124,14 @@ public final class BrowserFileListComp extends SimpleComp {
});
table.setFixedCellSize(30.0);
prepareColumnVisibility(table, ownerCol, filenameCol);
prepareColumnVisibility(table, filenameCol, mtimeCol, modeCol, ownerCol, sizeCol);
prepareTableScrollFix(table);
prepareTableSelectionModel(table);
prepareTableShortcuts(table);
prepareTableEntries(table);
prepareTableChanges(table, filenameCol, mtimeCol, modeCol, ownerCol);
prepareTypedSelectionModel(table);
table.setMinWidth(0);
return table;
}
@@ -148,8 +149,11 @@ public final class BrowserFileListComp extends SimpleComp {
private void prepareColumnVisibility(
TableView<BrowserEntry> table,
TableColumn<BrowserEntry, String> filenameCol,
TableColumn<BrowserEntry, Instant> mtimeCol,
TableColumn<BrowserEntry, String> modeCol,
TableColumn<BrowserEntry, String> ownerCol,
TableColumn<BrowserEntry, String> filenameCol) {
TableColumn<BrowserEntry, String> sizeCol) {
var os = fileList.getFileSystemModel()
.getFileSystem()
.getShell()
@@ -159,6 +163,15 @@ public final class BrowserFileListComp extends SimpleComp {
if (os != OsType.WINDOWS && os != OsType.MACOS) {
ownerCol.setVisible(newValue.doubleValue() > 1000);
}
var shell = fileList.getFileSystemModel().getFileSystem().getShell().orElseThrow();
if (!OsType.WINDOWS.equals(shell.getOsType()) && !OsType.MACOS.equals(shell.getOsType())) {
modeCol.setVisible(newValue.doubleValue() > 600);
}
mtimeCol.setPrefWidth(newValue.doubleValue() == 0.0 || newValue.doubleValue() > 600 ? 150 : 100);
sizeCol.setPrefWidth(newValue.doubleValue() == 0.0 || newValue.doubleValue() > 600 ? 120 : 90);
var width = getFilenameWidth(table);
filenameCol.setPrefWidth(width);
});
@@ -171,7 +184,7 @@ public final class BrowserFileListComp extends SimpleComp {
.mapToDouble(value -> value.getPrefWidth())
.sum()
+ 7;
return tableView.getWidth() - sum;
return Math.max(200, tableView.getWidth() - sum);
}
private String formatOwner(BrowserEntry param) {
@@ -336,7 +349,7 @@ public final class BrowserFileListComp extends SimpleComp {
}
var selected = fileList.getSelection();
var action = BrowserAction.getFlattened(fileList.getFileSystemModel(), selected).stream()
var action = BrowserMenuProviders.getFlattened(fileList.getFileSystemModel(), selected).stream()
.filter(browserAction -> browserAction.isApplicable(fileList.getFileSystemModel(), selected)
&& browserAction.isActive(fileList.getFileSystemModel(), selected))
.filter(browserAction -> browserAction.getShortcut() != null)
@@ -345,9 +358,11 @@ public final class BrowserFileListComp extends SimpleComp {
action.ifPresent(browserAction -> {
// Prevent concurrent modification by creating copy on platform thread
var selectionCopy = new ArrayList<>(selected);
ThreadHelper.runFailableAsync(() -> {
try {
browserAction.execute(fileList.getFileSystemModel(), selectionCopy);
});
} catch (Exception e) {
throw new RuntimeException(e);
}
event.consume();
});
if (action.isPresent()) {
@@ -479,8 +494,29 @@ public final class BrowserFileListComp extends SimpleComp {
TableColumn<BrowserEntry, String> modeCol,
TableColumn<BrowserEntry, String> ownerCol) {
var lastDir = new SimpleObjectProperty<FileEntry>();
Runnable updateHandler = () -> {
BiConsumer<List<BrowserEntry>, List<BrowserEntry>> updateHandler = (o, n) -> {
PlatformThread.runLaterIfNeeded(() -> {
// Optimization for single entry updates
if (o != null && n != null && o.size() == n.size()) {
var left = new HashSet<>(n);
o.forEach(left::remove);
if (left.size() == 1) {
var updatedEntry = left.iterator().next();
var found = o.stream()
.filter(browserEntry -> browserEntry
.getRawFileEntry()
.getPath()
.equals(updatedEntry.getRawFileEntry().getPath()))
.findFirst();
if (found.isPresent()) {
table.refresh();
table.getItems().set(table.getItems().indexOf(found.get()), updatedEntry);
return;
}
}
}
table.setDisable(true);
var newItems = new ArrayList<>(fileList.getShown().getValue());
table.getItems().clear();
@@ -506,21 +542,17 @@ public final class BrowserFileListComp extends SimpleComp {
ownerCol.setPrefWidth(0);
}
if (fileList.getFileSystemModel().getFileSystem() != null) {
var shell = fileList.getFileSystemModel()
.getFileSystem()
.getShell()
.orElseThrow();
if (OsType.WINDOWS.equals(shell.getOsType()) || OsType.MACOS.equals(shell.getOsType())) {
modeCol.setVisible(false);
var shell =
fileList.getFileSystemModel().getFileSystem().getShell().orElseThrow();
if (OsType.WINDOWS.equals(shell.getOsType()) || OsType.MACOS.equals(shell.getOsType())) {
modeCol.setVisible(false);
ownerCol.setVisible(false);
} else {
modeCol.setVisible(table.getWidth() > 600);
if (table.getWidth() > 1000) {
ownerCol.setVisible(hasOwner);
} else if (!hasOwner) {
ownerCol.setVisible(false);
} else {
modeCol.setVisible(true);
if (table.getWidth() > 1000) {
ownerCol.setVisible(hasOwner);
} else if (!hasOwner) {
ownerCol.setVisible(false);
}
}
}
@@ -541,17 +573,20 @@ public final class BrowserFileListComp extends SimpleComp {
}
}
lastDir.setValue(currentDirectory);
table.setDisable(false);
});
};
updateHandler.run();
updateHandler.accept(null, null);
fileList.getShown().addListener((observable, oldValue, newValue) -> {
// Delay to prevent internal tableview exceptions when sorting
Platform.runLater(updateHandler);
Platform.runLater(() -> {
updateHandler.accept(oldValue, newValue);
});
});
fileList.getFileSystemModel().getCurrentPath().addListener((observable, oldValue, newValue) -> {
if (oldValue == null) {
updateHandler.run();
updateHandler.accept(null, null);
}
});
}
@@ -594,16 +629,15 @@ public final class BrowserFileListComp extends SimpleComp {
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
setText(null);
} else {
var path = getTableRow().getItem();
if (path.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
setText(null);
} else if (fileSize != null) {
if (fileSize != null) {
try {
var l = Long.parseLong(fileSize);
setText(byteCount(l));
} catch (NumberFormatException e) {
setText(fileSize);
}
} else {
setText(null);
}
}
}

View File

@@ -6,9 +6,8 @@ import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.comp.base.TooltipHelper;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.InputHelper;
import io.xpipe.app.util.PlatformThread;
import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Pos;

View File

@@ -1,10 +1,12 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.browser.action.impl.MoveFileActionProvider;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FilePath;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
@@ -56,12 +58,31 @@ public final class BrowserFileListModel {
}
}
public void updateEntry(FilePath p, FileEntry n) {
var found = all.getValue().stream()
.filter(browserEntry -> browserEntry.getRawFileEntry().getPath().equals(p))
.findFirst();
if (found.isEmpty()) {
return;
}
var index = all.getValue().indexOf(found.get());
var l = new ArrayList<>(all.getValue());
if (n != null) {
l.set(index, new BrowserEntry(n, this));
} else {
l.remove(index);
}
all.setValue(l);
refreshShown();
}
public void setComparator(Comparator<BrowserEntry> comparator) {
comparatorProperty.setValue(comparator);
refreshShown();
}
private void refreshShown() {
void refreshShown() {
List<BrowserEntry> filtered = fileSystemModel.getFilter().getValue() != null
? all.getValue().stream()
.filter(entry -> {
@@ -90,7 +111,7 @@ public final class BrowserFileListModel {
return us;
}
public BrowserEntry rename(BrowserEntry old, String newName) {
public BrowserEntry rename(BrowserEntry old, String newName) throws Exception {
if (old == null
|| newName == null
|| fileSystemModel == null
@@ -99,7 +120,6 @@ public final class BrowserFileListModel {
return old;
}
var fullPath = fileSystemModel.getCurrentPath().get().join(old.getFileName());
var newFullPath = fileSystemModel.getCurrentPath().get().join(newName);
// This check will fail on case-insensitive file systems when changing the case of the file
@@ -113,22 +133,25 @@ public final class BrowserFileListModel {
exists = fileSystemModel.getFileSystem().fileExists(newFullPath)
|| fileSystemModel.getFileSystem().directoryExists(newFullPath);
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
ErrorEventFactory.fromThrowable(e).handle();
return old;
}
if (exists) {
ErrorEvent.fromMessage("Target " + newFullPath + " does already exist")
ErrorEventFactory.fromMessage("Target " + newFullPath + " does already exist")
.expected()
.handle();
fileSystemModel.refresh();
fileSystemModel.refreshSync();
return old;
}
}
try {
fileSystemModel.getFileSystem().move(fullPath, newFullPath);
fileSystemModel.refresh();
var builder = MoveFileActionProvider.Action.builder();
builder.initEntries(fileSystemModel, List.of(old));
builder.target(newFullPath);
builder.build().executeSync();
var b = all.getValue().stream()
.filter(browserEntry ->
browserEntry.getRawFileEntry().getPath().equals(newFullPath))
@@ -136,7 +159,7 @@ public final class BrowserFileListModel {
.orElse(old);
return b;
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
ErrorEventFactory.fromThrowable(e).handle();
return old;
}
}

View File

@@ -8,6 +8,7 @@ import io.xpipe.app.util.InputHelper;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FilePath;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
@@ -58,7 +59,7 @@ class BrowserFileListNameCell extends TableCell<BrowserEntry, String> {
var textField = new LazyTextFieldComp(text)
.minWidth(USE_PREF_SIZE)
.createStructure()
.get();
.getTextField();
var quickAccess = createQuickAccessButton();
setupShortcuts(tableView, (ButtonBase) quickAccess);
setupRename(fileList, textField, editing);
@@ -143,7 +144,7 @@ class BrowserFileListNameCell extends TableCell<BrowserEntry, String> {
getTableRow().requestFocus();
var it = getTableRow().getItem();
editing.setValue(null);
ThreadHelper.runAsync(() -> {
ThreadHelper.runFailableAsync(() -> {
if (it == null) {
return;
}
@@ -163,6 +164,21 @@ class BrowserFileListNameCell extends TableCell<BrowserEntry, String> {
PlatformThread.runLaterIfNeeded(() -> {
textField.setDisable(false);
textField.requestFocus();
var content = textField.getText();
if (content != null && !content.isEmpty()) {
var name = FilePath.of(content);
var baseNameEnd = name.getBaseName().toString().length();
textField.positionCaret(baseNameEnd);
}
});
}
});
textField.disabledProperty().addListener((observable, oldValue, newValue) -> {
if (!oldValue && newValue) {
Platform.runLater(() -> {
editing.setValue(null);
});
}
});

View File

@@ -4,6 +4,7 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.ext.ConnectionFileSystem;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.FileBridge;
import io.xpipe.app.util.FileOpener;
@@ -20,58 +21,98 @@ import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Objects;
import java.util.Optional;
public class BrowserFileOpener {
private static OutputStream openFileOutput(BrowserFileSystemTabModel model, FileEntry file, long totalBytes)
private static BrowserFileOutput openFileOutput(BrowserFileSystemTabModel model, FileEntry file, long totalBytes)
throws Exception {
var fileSystem = model.getFileSystem();
if (model.isClosed() || fileSystem.getShell().isEmpty()) {
return OutputStream.nullOutputStream();
return BrowserFileOutput.none();
}
if (totalBytes == 0) {
var existingSize = model.getFileSystem().getFileSize(file.getPath());
if (existingSize != 0) {
var blank = AppDialog.confirm("fileWriteBlankTitle", AppI18n.observable("fileWriteBlankContent", file.getPath()));
var blank = AppDialog.confirm(
"fileWriteBlankTitle", AppI18n.observable("fileWriteBlankContent", file.getPath()));
if (!blank) {
return OutputStream.nullOutputStream();
return BrowserFileOutput.none();
}
}
}
var defOutput = new BrowserFileOutput() {
@Override
public Optional<DataStoreEntry> target() {
return Optional.of(model.getEntry().get());
}
@Override
public boolean hasOutput() {
return true;
}
@Override
public OutputStream open() throws Exception {
return fileSystem.openOutput(file.getPath(), totalBytes);
}
};
var sc = fileSystem.getShell().get();
if (sc.getOsType() == OsType.WINDOWS) {
return fileSystem.openOutput(file.getPath(), totalBytes);
return defOutput;
}
var info = (FileInfo.Unix) file.getInfo();
var requiresSudo = requiresSudo(model, info, file.getPath());
if (!requiresSudo) {
return fileSystem.openOutput(file.getPath(), totalBytes);
return defOutput;
}
var elevate = AppDialog.confirm("fileWriteSudo");
if (!elevate) {
return fileSystem.openOutput(file.getPath(), totalBytes);
return defOutput;
}
var rootSc = sc.identicalDialectSubShell()
.elevated(ElevationFunction.elevated(null))
.start();
var rootFs = new ConnectionFileSystem(rootSc);
try {
return new FilterOutputStream(rootFs.openOutput(file.getPath(), totalBytes)) {
@Override
public void close() throws IOException {
super.close();
var rootOutput = new BrowserFileOutput() {
@Override
public Optional<DataStoreEntry> target() {
return Optional.of(model.getEntry().get());
}
@Override
public boolean hasOutput() {
return true;
}
@Override
public OutputStream open() throws Exception {
try {
return new FilterOutputStream(rootFs.openOutput(file.getPath(), totalBytes)) {
@Override
public void close() throws IOException {
try {
super.close();
} finally {
rootFs.close();
}
}
};
} catch (Exception ex) {
rootFs.close();
throw ex;
}
};
} catch (Exception ex) {
rootFs.close();
throw ex;
}
}
};
return rootOutput;
}
private static boolean requiresSudo(BrowserFileSystemTabModel model, FileInfo.Unix info, FilePath filePath)
@@ -128,10 +169,25 @@ public class BrowserFileOpener {
},
(size) -> {
if (model.isClosed()) {
return OutputStream.nullOutputStream();
return BrowserFileOutput.none();
}
return entry.getFileSystem().openOutput(file, size);
return new BrowserFileOutput() {
@Override
public boolean hasOutput() {
return true;
}
@Override
public Optional<DataStoreEntry> target() {
return Optional.of(model.getEntry().get());
}
@Override
public OutputStream open() throws Exception {
return entry.getFileSystem().openOutput(file, size);
}
};
},
s -> FileOpener.openWithAnyApplication(s));
}
@@ -154,10 +210,25 @@ public class BrowserFileOpener {
},
(size) -> {
if (model.isClosed()) {
return OutputStream.nullOutputStream();
return BrowserFileOutput.none();
}
return entry.getFileSystem().openOutput(file, size);
return new BrowserFileOutput() {
@Override
public boolean hasOutput() {
return true;
}
@Override
public Optional<DataStoreEntry> target() {
return Optional.of(model.getEntry().get());
}
@Override
public OutputStream open() throws Exception {
return entry.getFileSystem().openOutput(file, size);
}
};
},
s -> FileOpener.openInDefaultApplication(s));
}

View File

@@ -0,0 +1,35 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.storage.DataStoreEntry;
import java.io.OutputStream;
import java.util.Optional;
public interface BrowserFileOutput {
static BrowserFileOutput none() {
return new BrowserFileOutput() {
@Override
public Optional<DataStoreEntry> target() {
return Optional.empty();
}
@Override
public boolean hasOutput() {
return false;
}
@Override
public OutputStream open() {
return null;
}
};
}
Optional<DataStoreEntry> target();
boolean hasOutput();
OutputStream open() throws Exception;
}

View File

@@ -1,6 +1,6 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.*;
@@ -26,9 +26,6 @@ public class BrowserFileSystemHelper {
}
// Handle special case when file system creation has failed
if (model.getFileSystem() == null) {
return path;
}
var shell = model.getFileSystem().getShell();
if (shell.isEmpty()) {
@@ -47,10 +44,6 @@ public class BrowserFileSystemHelper {
return null;
}
if (model.getFileSystem() == null) {
return path;
}
var shell = model.getFileSystem().getShell();
if (shell.isEmpty() || !shell.get().isRunning(true)) {
return path;
@@ -63,7 +56,7 @@ public class BrowserFileSystemHelper {
.readStdoutOrThrow();
return !r.isBlank() ? r : null;
} catch (Exception ex) {
ErrorEvent.expected(ex);
ErrorEventFactory.expected(ex);
throw ex;
}
}
@@ -74,10 +67,6 @@ public class BrowserFileSystemHelper {
return null;
}
if (model.getFileSystem() == null) {
return path;
}
var shell = model.getFileSystem().getShell();
if (shell.isEmpty()) {
return path;
@@ -105,24 +94,20 @@ public class BrowserFileSystemHelper {
return;
}
if (model.getFileSystem() == null) {
return;
}
var shell = model.getFileSystem().getShell();
if (shell.isEmpty()) {
return;
}
if (verifyExists && !model.getFileSystem().directoryExists(path)) {
throw ErrorEvent.expected(new IllegalArgumentException(
throw ErrorEventFactory.expected(new IllegalArgumentException(
String.format("Directory %s does not exist or is not accessible", path)));
}
try {
model.getFileSystem().directoryAccessible(path);
} catch (Exception ex) {
ErrorEvent.expected(ex);
ErrorEventFactory.expected(ex);
throw ex;
}
}
@@ -146,7 +131,7 @@ public class BrowserFileSystemHelper {
try {
file.getFileSystem().delete(file.getPath());
} catch (Throwable t) {
ErrorEvent.fromThrowable(t).handle();
ErrorEventFactory.fromThrowable(t).handle();
}
}
}

View File

@@ -1,7 +1,7 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.menu.BrowserMenuProviders;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.SimpleCompStructure;
@@ -65,11 +65,12 @@ public class BrowserFileSystemTabComp extends SimpleComp {
keyEvent.consume();
});
var backBtn = BrowserAction.byId("back", model, List.of()).toButton(root, model, List.of());
var forthBtn = BrowserAction.byId("forward", model, List.of()).toButton(root, model, List.of());
var refreshBtn = BrowserAction.byId("refresh", model, List.of()).toButton(root, model, List.of());
var backBtn = BrowserMenuProviders.byId("back", model, List.of()).toButton(root, model, List.of());
var forthBtn = BrowserMenuProviders.byId("forward", model, List.of()).toButton(root, model, List.of());
var refreshBtn = BrowserMenuProviders.byId("refresh", model, List.of()).toButton(root, model, List.of());
// Don't handle key events for this button, we also have that available as a menu item
var terminalBtn = BrowserAction.byId("openTerminal", model, List.of()).toButton(new Region(), model, List.of());
var terminalBtn =
BrowserMenuProviders.byId("openTerminal", model, List.of()).toButton(new Region(), model, List.of());
var menuButton = new MenuButton(null, new FontIcon("mdral-folder_open"));
new ContextMenuAugment<>(
@@ -80,7 +81,20 @@ public class BrowserFileSystemTabComp extends SimpleComp {
menuButton.disableProperty().bind(model.getInOverview());
menuButton.setAccessibleText("Directory options");
var filter = new BrowserFileListFilterComp(model, model.getFilter()).createStructure();
var smallWidth = Bindings.createBooleanBinding(
() -> {
return root.getWidth() < 450;
},
root.widthProperty());
refreshBtn.managedProperty().bind(smallWidth.not());
refreshBtn.visibleProperty().bind(refreshBtn.managedProperty());
terminalBtn.managedProperty().bind(smallWidth.not());
terminalBtn.visibleProperty().bind(terminalBtn.managedProperty());
var filter = new BrowserFileListFilterComp(model, model.getFilter())
.hide(smallWidth)
.createStructure();
var topBar = new HBox();
topBar.setAlignment(Pos.CENTER);
@@ -101,6 +115,7 @@ public class BrowserFileSystemTabComp extends SimpleComp {
refreshBtn,
terminalBtn,
menuButton);
topBar.setMinWidth(0);
if (model.getBrowserModel() instanceof BrowserFullSessionModel fullSessionModel) {
var pinButton = new Button();

View File

@@ -1,14 +1,17 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.action.ActionProvider;
import io.xpipe.app.browser.BrowserAbstractSessionModel;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.browser.BrowserStoreSessionTab;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.impl.TransferFilesActionProvider;
import io.xpipe.app.browser.menu.BrowserMenuItemProvider;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.core.window.AppMainWindow;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.ext.WrapperFileSystem;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.terminal.*;
@@ -17,7 +20,6 @@ import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.*;
import io.xpipe.core.store.*;
import io.xpipe.core.util.FailableConsumer;
import io.xpipe.core.util.FailableRunnable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
@@ -26,7 +28,6 @@ import javafx.collections.ObservableList;
import lombok.Getter;
import lombok.NonNull;
import lombok.SneakyThrows;
import java.io.IOException;
import java.nio.file.Path;
@@ -47,7 +48,9 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
private final Property<BrowserTransferProgress> progress = new SimpleObjectProperty<>();
private final ObservableList<UUID> terminalRequests = FXCollections.observableArrayList();
private final BooleanProperty transferCancelled = new SimpleBooleanProperty();
private FileSystem fileSystem;
private BrowserFileSystemSavedState savedState;
private BrowserFileSystemCache cache;
@@ -79,8 +82,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
@Override
public boolean canImmediatelyClose() {
if (fileSystem == null
|| fileSystem.getShell().isEmpty()
if (fileSystem.getShell().isEmpty()
|| !fileSystem.getShell().get().getLock().isLocked()) {
return true;
}
@@ -94,6 +96,9 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
var fs = entry.getStore().createFileSystem();
if (fs.getShell().isPresent()) {
ProcessControlProvider.get().withDefaultScripts(fs.getShell().get());
var originalFs = fs;
fs = new WrapperFileSystem(
originalFs, () -> originalFs.getShell().get().isRunning(true));
}
fs.open();
// Listen to kill after init as the shell might get killed during init for certain reasons
@@ -105,8 +110,10 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
this.fileSystem = fs;
this.cache = new BrowserFileSystemCache(this);
for (BrowserAction b : BrowserAction.ALL) {
b.init(this);
for (var a : ActionProvider.ALL) {
if (a instanceof BrowserMenuItemProvider ba) {
ba.init(this);
}
}
});
this.savedState = BrowserFileSystemSavedState.loadForStore(this);
@@ -115,10 +122,6 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
@Override
public void close() {
BooleanScope.executeExclusive(busy, () -> {
if (fileSystem == null) {
return;
}
var current = getCurrentDirectory();
// We might close this after storage shutdown
// If this entry does not exist, it's not that bad if we save it anyway
@@ -134,17 +137,12 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
try {
fileSystem.close();
} catch (IOException e) {
ErrorEvent.fromThrowable(e).handle();
ErrorEventFactory.fromThrowable(e).handle();
}
fileSystem = null;
});
}
private void startIfNeeded() throws Exception {
if (fileSystem == null) {
return;
}
var s = fileSystem.getShell();
if (s.isPresent()) {
s.get().start();
@@ -152,19 +150,11 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
}
public void killTransfer() {
if (fileSystem == null) {
return;
}
transferCancelled.set(true);
}
public void withShell(FailableConsumer<ShellControl, Exception> c, boolean refresh) {
ThreadHelper.runFailableAsync(() -> {
if (fileSystem == null) {
return;
}
BooleanScope.executeExclusive(busy, () -> {
if (entry.getStore() instanceof ShellStore s) {
c.accept(fileSystem.getShell().orElseThrow());
@@ -176,21 +166,30 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
});
}
@SneakyThrows
public void refresh() {
BooleanScope.executeExclusive(busy, () -> {
cdSyncWithoutCheck(currentPath.get());
});
}
public void refreshSync() throws Exception {
public void refreshSync() {
cdSyncWithoutCheck(currentPath.get());
}
public void refreshEntriesSync(List<BrowserEntry> entries) throws Exception {
if (fileList.getAll().getValue().size() < 10) {
refreshSync();
return;
}
if (entries.size() > 10 && fileList.getAll().getValue().size() < 100) {
refreshSync();
return;
}
for (BrowserEntry browserEntry : entries) {
var refresh = fileSystem.getFileInfo(browserEntry.getRawFileEntry().getPath());
fileList.updateEntry(browserEntry.getRawFileEntry().getPath(), refresh.orElse(null));
}
}
public FileEntry getCurrentParentDirectory() {
var current = getCurrentDirectory();
if (current == null) {
return null;
if (currentPath.get() == null) {
return FileEntry.ofDirectory(fileSystem, FilePath.of("?"));
}
var parent = currentPath.get().getParent();
@@ -202,12 +201,10 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
}
public FileEntry getCurrentDirectory() {
// This should never happen, this should not be called in a context
// where the current path is null
if (currentPath.get() == null) {
return null;
}
if (fileSystem == null) {
return null;
return FileEntry.ofDirectory(fileSystem, FilePath.of("?"));
}
return new FileEntry(fileSystem, currentPath.get(), null, null, null, FileKind.DIRECTORY);
@@ -238,7 +235,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
return false;
}
if (AppMainWindow.getInstance().getStage().getWidth() <= 1280) {
if (AppMainWindow.getInstance().getStage().getWidth() <= 1380) {
return false;
}
@@ -269,15 +266,11 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
return Optional.empty();
}
if (fileSystem == null) {
return Optional.empty();
}
try {
// Start shell in case we exited
startIfNeeded();
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
ErrorEventFactory.fromThrowable(ex).handle();
return Optional.ofNullable(cps);
}
@@ -297,7 +290,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
try {
evaluatedPath = BrowserFileSystemHelper.evaluatePath(this, adjustedPath);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
ErrorEventFactory.fromThrowable(ex).handle();
return Optional.ofNullable(cps);
}
@@ -345,31 +338,26 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
try {
resolvedPath = BrowserFileSystemHelper.resolveDirectoryPath(this, FilePath.of(evaluatedPath), customInput);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
ErrorEventFactory.fromThrowable(ex).handle();
return Optional.ofNullable(cps);
}
if (!Objects.equals(path, resolvedPath.toString())) {
return Optional.ofNullable(resolvedPath.toString());
return Optional.of(resolvedPath.toString());
}
try {
BrowserFileSystemHelper.validateDirectoryPath(this, resolvedPath, true);
cdSyncWithoutCheck(resolvedPath);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
ErrorEventFactory.fromThrowable(ex).handle();
return Optional.ofNullable(cps);
}
return Optional.empty();
}
private void cdSyncWithoutCheck(FilePath path) throws Exception {
if (fileSystem == null) {
var fs = entry.getStore().createFileSystem();
fs.open();
this.fileSystem = fs;
}
private void cdSyncWithoutCheck(FilePath path) {
// Assume that the path is normalized to improve performance!
// path = FileSystemHelper.normalizeDirectoryPath(this, path);
@@ -386,9 +374,6 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
if (dir != null) {
startIfNeeded();
var fs = getFileSystem();
if (fs == null) {
return;
}
var stream = fs.listFiles(dir);
consumer.accept(stream);
@@ -402,7 +387,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
try {
startIfNeeded();
var fs = getFileSystem();
if (dir != null && fs != null) {
if (dir != null) {
var stream = fs.listFiles(dir);
fileList.setAll(stream);
} else {
@@ -411,7 +396,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
return true;
} catch (Exception e) {
fileList.setAll(Stream.of());
ErrorEvent.fromThrowable(e).handle();
ErrorEventFactory.fromThrowable(e).handle();
return false;
}
}
@@ -419,14 +404,14 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
public void dropLocalFilesIntoAsync(FileEntry entry, List<Path> files) {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
if (fileSystem == null) {
return;
}
startIfNeeded();
var op = BrowserFileTransferOperation.ofLocal(
entry, files, BrowserFileTransferMode.COPY, true, progress::setValue, transferCancelled);
op.execute();
var action = TransferFilesActionProvider.Action.builder()
.operation(op)
.target(this.entry.asNeeded())
.build();
action.executeSync();
refreshSync();
});
});
@@ -440,137 +425,21 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
if (fileSystem == null) {
return;
}
startIfNeeded();
var op = new BrowserFileTransferOperation(
target, files, mode, true, progress::setValue, transferCancelled);
op.execute();
refreshSync();
});
});
}
public void createDirectoryAsync(String name) {
if (name == null || name.isBlank()) {
return;
}
if (getCurrentDirectory() == null) {
return;
}
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
if (fileSystem == null) {
return;
}
startIfNeeded();
var abs = getCurrentDirectory().getPath().join(name);
if (fileSystem.directoryExists(abs)) {
throw ErrorEvent.expected(
new IllegalStateException(String.format("Directory %s already exists", abs)));
}
fileSystem.mkdirs(abs);
refreshSync();
});
});
}
public void createLinkAsync(String linkName, FilePath targetFile) {
if (linkName == null || linkName.isBlank() || targetFile == null) {
return;
}
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
if (fileSystem == null) {
return;
}
if (getCurrentDirectory() == null) {
return;
}
startIfNeeded();
var abs = getCurrentDirectory().getPath().join(linkName);
fileSystem.symbolicLink(abs, targetFile);
refreshSync();
});
});
}
public void runCommandAsync(CommandBuilder command, boolean refresh) {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
if (fileSystem == null) {
return;
}
if (getCurrentDirectory() == null) {
return;
}
fileSystem
.getShell()
.orElseThrow()
.command(command)
.withWorkingDirectory(getCurrentDirectory().getPath())
.execute();
if (refresh) {
refreshSync();
}
});
});
}
public void runAsync(FailableRunnable<Exception> r, boolean refresh) {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
if (fileSystem == null) {
return;
}
if (getCurrentDirectory() == null) {
return;
}
r.run();
if (refresh) {
refreshSync();
}
});
});
}
public void createFileAsync(String name) {
if (name == null || name.isBlank()) {
return;
}
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
if (fileSystem == null) {
return;
}
if (getCurrentDirectory() == null) {
return;
}
var abs = getCurrentDirectory().getPath().join(name);
fileSystem.touch(abs);
var action = TransferFilesActionProvider.Action.builder()
.operation(op)
.target(entry.asNeeded())
.build();
action.executeSync();
refreshSync();
});
});
}
public boolean isClosed() {
return fileSystem == null;
return false;
}
public void initWithGivenDirectory(FilePath dir) {
@@ -585,30 +454,28 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
public void openTerminalAsync(
String name, FilePath directory, ProcessControl processControl, boolean dockIfPossible) {
ThreadHelper.runFailableAsync(() -> {
if (fileSystem == null) {
return;
}
BooleanScope.executeExclusive(busy, () -> {
if (fileSystem.getShell().isPresent()) {
var dock = shouldLaunchSplitTerminal() && dockIfPossible;
var uuid = UUID.randomUUID();
terminalRequests.add(uuid);
if (dock
&& browserModel instanceof BrowserFullSessionModel fullSessionModel
&& !(fullSessionModel.getSplits().get(this) instanceof BrowserTerminalDockTabModel)) {
fullSessionModel.splitTab(
this, new BrowserTerminalDockTabModel(browserModel, this, terminalRequests));
}
TerminalLauncher.open(entry.get(), name, directory, processControl, uuid, !dock);
// Restart connection as we will have to start it anyway, so we speed it up by doing it preemptively
startIfNeeded();
}
openTerminalSync(name, directory, processControl, dockIfPossible);
});
});
}
public void openTerminalSync(String name, FilePath directory, ProcessControl processControl, boolean dockIfPossible)
throws Exception {
var dock = shouldLaunchSplitTerminal() && dockIfPossible;
var uuid = UUID.randomUUID();
terminalRequests.add(uuid);
if (dock
&& browserModel instanceof BrowserFullSessionModel fullSessionModel
&& !(fullSessionModel.getSplits().get(this) instanceof BrowserTerminalDockTabModel)) {
fullSessionModel.splitTab(this, new BrowserTerminalDockTabModel(browserModel, this, terminalRequests));
}
TerminalLauncher.open(entry.get(), name, directory, processControl, uuid, !dock);
// Restart connection as we will have to start it anyway, so we speed it up by doing it preemptively
startIfNeeded();
}
public void backSync(int i) {
var b = history.back(i);
if (b != null) {

View File

@@ -1,14 +1,18 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.*;
import javafx.beans.property.BooleanProperty;
import javafx.beans.value.ChangeListener;
import lombok.Getter;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.List;
@@ -20,8 +24,12 @@ import java.util.regex.Pattern;
public class BrowserFileTransferOperation {
@Getter
private final FileEntry target;
@Getter
private final List<FileEntry> files;
private final BrowserFileTransferMode transferMode;
private final boolean checkConflicts;
private final Consumer<BrowserTransferProgress> progress;
@@ -184,7 +192,7 @@ public class BrowserFileTransferOperation {
}
if (source.getKind() == FileKind.DIRECTORY && target.getFileSystem().directoryExists(targetFile)) {
throw ErrorEvent.expected(
throw ErrorEventFactory.expected(
new IllegalArgumentException("Target directory " + targetFile + " does already exist"));
}
@@ -232,8 +240,8 @@ public class BrowserFileTransferOperation {
}
}
var noExt = target.getFileName().equals(target.getExtension());
return FilePath.of(target.getBaseName() + " (" + 1 + ")" + (noExt ? "" : "." + target.getExtension()));
var ext = target.getExtension();
return FilePath.of(target.getBaseName() + " (" + 1 + ")" + (ext.isPresent() ? "." + ext.get() : ""));
}
private void handleSingleAcrossFileSystems(FileEntry source) throws Exception {
@@ -287,59 +295,90 @@ public class BrowserFileTransferOperation {
totalSize.addAndGet(source.getFileSizeLong().orElse(0));
}
var start = Instant.now();
AtomicLong transferred = new AtomicLong();
for (var e : flatFiles.entrySet()) {
if (cancelled()) {
return;
}
var originalSourceFs = flatFiles.keySet().iterator().next().getFileSystem();
if (!flatFiles.keySet().stream()
.allMatch(fileEntry -> fileEntry.getFileSystem().equals(originalSourceFs))) {
throw new IllegalArgumentException("Mixed source file systems");
}
var sourceFile = e.getKey();
var fixedRelPath = FilePath.of(e.getValue())
.fileSystemCompatible(
target.getFileSystem().getShell().orElseThrow().getOsType());
var targetFile = target.getPath().join(fixedRelPath.toString());
if (sourceFile.getFileSystem().equals(target.getFileSystem())) {
throw new IllegalStateException();
}
var optimizedSourceFs = originalSourceFs.createTransferOptimizedFileSystem();
var targetFs = target.getFileSystem().createTransferOptimizedFileSystem();
if (sourceFile.getKind() == FileKind.DIRECTORY) {
target.getFileSystem().mkdirs(targetFile);
} else if (sourceFile.getKind() == FileKind.FILE) {
if (checkConflicts) {
var fileConflictChoice =
handleChoice(target.getFileSystem(), targetFile, files.size() > 1 || flatFiles.size() > 1);
if (fileConflictChoice == BrowserAlerts.FileConflictChoice.SKIP
|| fileConflictChoice == BrowserAlerts.FileConflictChoice.CANCEL) {
continue;
}
if (fileConflictChoice == BrowserAlerts.FileConflictChoice.RENAME) {
targetFile = renameFileLoop(target.getFileSystem(), targetFile, false);
}
try {
var start = Instant.now();
AtomicLong transferred = new AtomicLong();
for (var e : flatFiles.entrySet()) {
if (cancelled()) {
return;
}
transfer(sourceFile, targetFile, transferred, totalSize, start);
var sourceFile = e.getKey();
var fixedRelPath = FilePath.of(e.getValue())
.fileSystemCompatible(targetFs.getShell().orElseThrow().getOsType());
var targetFile = target.getPath().join(fixedRelPath.toString());
if (sourceFile.getFileSystem().equals(targetFs)) {
throw new IllegalStateException();
}
if (sourceFile.getKind() == FileKind.DIRECTORY) {
targetFs.mkdirs(targetFile);
} else if (sourceFile.getKind() == FileKind.FILE) {
if (checkConflicts) {
var fileConflictChoice =
handleChoice(targetFs, targetFile, files.size() > 1 || flatFiles.size() > 1);
if (fileConflictChoice == BrowserAlerts.FileConflictChoice.SKIP
|| fileConflictChoice == BrowserAlerts.FileConflictChoice.CANCEL) {
continue;
}
if (fileConflictChoice == BrowserAlerts.FileConflictChoice.RENAME) {
targetFile = renameFileLoop(targetFs, targetFile, false);
}
}
transfer(
sourceFile.getPath(),
optimizedSourceFs,
targetFile,
targetFs,
transferred,
totalSize,
start);
}
}
updateProgress(BrowserTransferProgress.finished(source.getName(), totalSize.get()));
} finally {
if (optimizedSourceFs != originalSourceFs) {
optimizedSourceFs.close();
}
if (target.getFileSystem() != targetFs) {
targetFs.close();
}
}
updateProgress(BrowserTransferProgress.finished(source.getName(), totalSize.get()));
}
private void transfer(
FileEntry sourceFile, FilePath targetFile, AtomicLong transferred, AtomicLong totalSize, Instant start)
FilePath sourceFile,
FileSystem sourceFs,
FilePath targetFile,
FileSystem targetFs,
AtomicLong transferred,
AtomicLong totalSize,
Instant start)
throws Exception {
if (cancelled()) {
return;
}
var fileSize = sourceFs.getFileSize(sourceFile);
InputStream inputStream = null;
OutputStream outputStream = null;
try {
var fileSize = sourceFile.getFileSystem().getFileSize(sourceFile.getPath());
// Read the first few bytes to figure out possible command failure early
// before creating the output stream
inputStream = new BufferedInputStream(sourceFile.getFileSystem().openInput(sourceFile.getPath()), 1024);
inputStream = new BufferedInputStream(sourceFs.openInput(sourceFile), 1024);
inputStream.mark(1024);
var streamStart = new byte[1024];
var streamStartLength = inputStream.read(streamStart, 0, 1024);
@@ -350,13 +389,11 @@ public class BrowserFileTransferOperation {
inputStream.reset();
}
outputStream = target.getFileSystem().openOutput(targetFile, fileSize);
outputStream = targetFs.openOutput(targetFile, fileSize);
transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, start, fileSize);
outputStream.flush();
inputStream.transferTo(OutputStream.nullOutputStream());
} catch (Exception ex) {
// Mark progress as finished to reset any progress display
updateProgress(BrowserTransferProgress.finished(sourceFile.getName(), transferred.get()));
updateProgress(BrowserTransferProgress.finished(sourceFile.getFileName(), transferred.get()));
if (inputStream != null) {
try {
@@ -364,7 +401,7 @@ public class BrowserFileTransferOperation {
} catch (Exception om) {
// This is expected as the process control has to be killed
// When calling close, it will throw an exception when it has to kill
// ErrorEvent.fromThrowable(om).handle();
ErrorEventFactory.fromThrowable(om).expected().omit().handle();
}
}
if (outputStream != null) {
@@ -373,12 +410,24 @@ public class BrowserFileTransferOperation {
} catch (Exception om) {
// This is expected as the process control has to be killed
// When calling close, it will throw an exception when it has to kill
// ErrorEvent.fromThrowable(om).handle();
ErrorEventFactory.fromThrowable(om).expected().omit().handle();
}
}
throw ex;
}
// If we receive a cancel while we are closing, there's a good chance that the close is stuck
// Then, we just straight up kill the shells
ChangeListener<Boolean> closeCancelListener = (observableValue, oldValue, newValue) -> {
if (!newValue) {
return;
}
sourceFs.getShell().orElseThrow().killExternal();
targetFs.getShell().orElseThrow().killExternal();
};
cancelled.addListener(closeCancelListener);
Exception exception = null;
try {
inputStream.close();
@@ -389,12 +438,18 @@ public class BrowserFileTransferOperation {
outputStream.close();
} catch (Exception om) {
if (exception != null) {
ErrorEvent.fromThrowable(om).handle();
exception.addSuppressed(om);
} else {
exception = om;
}
}
cancelled.removeListener(closeCancelListener);
if (exception != null) {
ErrorEventFactory.preconfigure(ErrorEventFactory.fromThrowable(exception)
.reportable(!cancelled())
.omitted(cancelled()));
throw exception;
}
}
@@ -406,7 +461,7 @@ public class BrowserFileTransferOperation {
private static final int DEFAULT_BUFFER_SIZE = 1024;
private void transferFile(
FileEntry sourceFile,
FilePath sourceFile,
InputStream inputStream,
OutputStream outputStream,
AtomicLong transferred,
@@ -415,13 +470,13 @@ public class BrowserFileTransferOperation {
long expectedFileSize)
throws Exception {
// Initialize progress immediately prior to reading anything
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
updateProgress(new BrowserTransferProgress(sourceFile.getFileName(), transferred.get(), total.get(), start));
var killStreams = new AtomicBoolean(false);
var exception = new AtomicReference<Exception>();
var readCount = new AtomicLong();
var thread = ThreadHelper.createPlatformThread("transfer", true, () -> {
try {
long readCount = 0;
var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, expectedFileSize);
byte[] buffer = new byte[bs];
int read;
@@ -438,14 +493,17 @@ public class BrowserFileTransferOperation {
outputStream.write(buffer, 0, read);
transferred.addAndGet(read);
readCount += read;
updateProgress(
new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
readCount.addAndGet(read);
updateProgress(new BrowserTransferProgress(
sourceFile.getFileName(), transferred.get(), total.get(), start));
}
var incomplete = readCount < expectedFileSize;
outputStream.flush();
inputStream.transferTo(OutputStream.nullOutputStream());
var incomplete = readCount.get() < expectedFileSize;
if (incomplete) {
throw new IOException("Source file " + sourceFile.getPath() + " input did end prematurely");
throw new IOException("Source file " + sourceFile + " input did end prematurely");
}
} catch (Exception ex) {
exception.set(ex);
@@ -459,9 +517,7 @@ public class BrowserFileTransferOperation {
var cancelled = cancelled();
if (cancelled) {
// Assume that the transfer has stalled if it doesn't finish until then
thread.join(1000);
killStreams();
killStreams(thread, readCount, false);
break;
}
@@ -471,7 +527,7 @@ public class BrowserFileTransferOperation {
}
if (killStreams.get()) {
killStreams();
killStreams(thread, readCount, true);
}
var ex = exception.get();
@@ -491,17 +547,33 @@ public class BrowserFileTransferOperation {
var sourceShell = sourceFs.getShell().orElseThrow();
var targetShell = targetFs.getShell().orElseThrow();
// Check for null on shell reset
return sourceShell.getStdout() != null && !sourceShell.getStdout().isClosed()
&& targetShell.getStdin() != null && !targetShell.getStdin().isClosed();
return sourceShell.getStdout() != null
&& !sourceShell.getStdout().isClosed()
&& targetShell.getStdin() != null
&& !targetShell.getStdin().isClosed();
} else {
return true;
}
}
private void killStreams() throws Exception {
private void killStreams(Thread thread, AtomicLong transferred, boolean instant) throws Exception {
var sourceFs = files.getFirst().getFileSystem();
var targetFs = target.getFileSystem();
var same = files.getFirst().getFileSystem().equals(target.getFileSystem());
if (!instant && !same && checkTransferValidity()) {
var initialTransferred = transferred.get();
if (!thread.join(Duration.ofMillis(1000))) {
var nowTransferred = transferred.get();
var stuck = initialTransferred == nowTransferred;
if (stuck) {
sourceFs.getShell().orElseThrow().killExternal();
targetFs.getShell().orElseThrow().killExternal();
return;
}
}
}
if (!same) {
var sourceShell = sourceFs.getShell().orElseThrow();
var targetShell = targetFs.getShell().orElseThrow();

View File

@@ -82,7 +82,9 @@ public class BrowserHistorySavedStateImpl implements BrowserHistorySavedState {
if (ls == null) {
ls = List.of();
}
var valid = ls.stream().filter(entry -> entry.getUuid() != null && entry.getPath() != null).toList();
var valid = ls.stream()
.filter(entry -> entry.getUuid() != null && entry.getPath() != null)
.toList();
return new BrowserHistorySavedStateImpl(valid);
}
}

View File

@@ -4,7 +4,7 @@ import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.SimpleTitledPaneComp;
import io.xpipe.app.comp.base.VerticalComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.util.DerivedObservableList;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.ShellControl;
@@ -32,9 +32,6 @@ public class BrowserOverviewComp extends SimpleComp {
@SneakyThrows
protected Region createSimple() {
// The open file system might have already been closed
if (model.getFileSystem() == null) {
return new Region();
}
ShellControl sc = model.getFileSystem().getShell().orElseThrow();
@@ -44,14 +41,11 @@ public class BrowserOverviewComp extends SimpleComp {
.map(s -> FileEntry.ofDirectory(model.getFileSystem(), s))
.filter(entry -> {
var fs = model.getFileSystem();
if (fs == null) {
return false;
}
try {
return fs.directoryExists(entry.getPath());
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
ErrorEventFactory.fromThrowable(e).handle();
return false;
}
})

View File

@@ -64,8 +64,7 @@ public class BrowserStatusBarComp extends SimpleComp {
var cancel = PlatformThread.sync(model.getTransferCancelled());
var hide = Bindings.createBooleanBinding(
() -> {
if (model.getProgress().getValue() == null
|| model.getProgress().getValue().done()) {
if (model.getProgress().getValue() == null) {
return true;
}

View File

@@ -71,7 +71,7 @@ public class BrowserTransferComp extends SimpleComp {
var hideProgress =
sourceItem.get().downloadFinished().get();
var share = p != null ? (p.getTransferred() * 100 / p.getTotal()) : 0;
var share = p.getTransferred() * 100 / p.getTotal();
var progressSuffix = hideProgress ? "" : " " + share + "%";
return entry.getFileName() + progressSuffix;
},

View File

@@ -1,13 +1,15 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.browser.action.impl.TransferFilesActionProvider;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.DesktopHelper;
import io.xpipe.app.util.ShellTemp;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.OsType;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
@@ -77,7 +79,7 @@ public class BrowserTransferModel {
try {
FileUtils.forceDelete(item.getLocalFile().toFile());
} catch (IOException e) {
ErrorEvent.fromThrowable(e).handle();
ErrorEventFactory.fromThrowable(e).handle();
}
}
@@ -118,7 +120,7 @@ public class BrowserTransferModel {
try {
FileUtils.forceMkdir(TEMP.toFile());
} catch (IOException e) {
ErrorEvent.fromThrowable(e).handle();
ErrorEventFactory.fromThrowable(e).handle();
return;
}
@@ -151,9 +153,14 @@ public class BrowserTransferModel {
itemModel.getProgress().setValue(progress);
},
itemModel.getTransferCancelled());
op.execute();
var action = TransferFilesActionProvider.Action.builder()
.operation(op)
.target(DataStorage.get().local().ref())
.download(true)
.build();
action.executeSync();
} catch (Throwable t) {
ErrorEvent.fromThrowable(t).handle();
ErrorEventFactory.fromThrowable(t).handle();
synchronized (items) {
items.remove(item);
}

View File

@@ -1,6 +1,6 @@
package io.xpipe.app.browser.icon;
import io.xpipe.app.resources.AppResources;
import io.xpipe.app.core.AppResources;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;

View File

@@ -1,6 +1,6 @@
package io.xpipe.app.browser.icon;
import io.xpipe.app.resources.AppResources;
import io.xpipe.app.core.AppResources;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;
@@ -85,7 +85,8 @@ public abstract class BrowserIconFileType {
var name = entry.getPath().getFileName();
var ext = entry.getPath().getExtension();
return (ext != null && endings.contains("." + ext.toLowerCase(Locale.ROOT))) || endings.contains(name);
return (ext.isPresent() && endings.contains("." + ext.get().toLowerCase(Locale.ROOT)))
|| endings.contains(name);
}
@Override

View File

@@ -14,8 +14,8 @@ public class BrowserIcons {
return PrettyImageHelper.ofFixedSizeSquare("browser/default_folder.svg", 24);
}
public static Comp<?> createIcon(BrowserIconFileType type) {
return PrettyImageHelper.ofFixedSizeSquare(type.getIcon(), 24);
public static Comp<?> createContextMenuIcon(BrowserIconFileType type) {
return PrettyImageHelper.ofFixedSizeSquare(type.getIcon(), 16);
}
public static Comp<?> createIcon(FileEntry entry) {

View File

@@ -1,11 +1,11 @@
package io.xpipe.app.browser.action;
package io.xpipe.app.browser.menu;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import java.util.List;
public interface BrowserApplicationPathAction extends BrowserAction {
public interface BrowserApplicationPathMenuProvider extends BrowserMenuItemProvider {
String getExecutable();

View File

@@ -1,4 +1,4 @@
package io.xpipe.app.browser.action;
package io.xpipe.app.browser.menu;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
@@ -11,7 +11,7 @@ import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public interface BrowserBranchAction extends BrowserAction {
public interface BrowserMenuBranchProvider extends BrowserMenuItemProvider {
default MenuItem toMenuItem(BrowserFileSystemTabModel model, List<BrowserEntry> selected) {
var m = new Menu(getName(model, selected).getValue() + " ...");
@@ -20,16 +20,24 @@ public interface BrowserBranchAction extends BrowserAction {
if (!sub.isApplicable(model, subselected)) {
continue;
}
m.getItems().add(sub.toMenuItem(model, subselected));
var item = sub.toMenuItem(model, subselected);
if (item != null) {
m.getItems().add(item);
}
}
if (m.getItems().isEmpty()) {
return null;
}
var graphic = getIcon(model, selected);
if (graphic != null) {
m.setGraphic(graphic);
m.setGraphic(graphic.createGraphicNode());
}
m.setDisable(!isActive(model, selected));
if (getProFeatureId() != null
&& !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) {
if (getLicensedFeatureId() != null
&& !LicenseProvider.get().getFeature(getLicensedFeatureId()).isSupported()) {
m.setDisable(true);
m.setGraphic(new FontIcon("mdi2p-professional-hexagon"));
}
@@ -37,5 +45,6 @@ public interface BrowserBranchAction extends BrowserAction {
return m;
}
List<? extends BrowserAction> getBranchingActions(BrowserFileSystemTabModel model, List<BrowserEntry> entries);
List<? extends BrowserMenuItemProvider> getBranchingActions(
BrowserFileSystemTabModel model, List<BrowserEntry> entries);
}

View File

@@ -0,0 +1,9 @@
package io.xpipe.app.browser.menu;
public enum BrowserMenuCategory {
CUSTOM,
OPEN,
COPY_PASTE,
ACTION,
MUTATION
}

View File

@@ -0,0 +1,58 @@
package io.xpipe.app.browser.menu;
import io.xpipe.app.action.ActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.util.LabelGraphic;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.MenuItem;
import javafx.scene.input.KeyCombination;
import java.util.List;
public interface BrowserMenuItemProvider extends ActionProvider {
MenuItem toMenuItem(BrowserFileSystemTabModel model, List<BrowserEntry> selected);
default void init(BrowserFileSystemTabModel model) throws Exception {}
default boolean automaticallyResolveLinks() {
return true;
}
default List<BrowserEntry> resolveFilesIfNeeded(List<BrowserEntry> selected) {
return automaticallyResolveLinks()
? selected.stream()
.map(browserEntry ->
new BrowserEntry(browserEntry.getRawFileEntry().resolved(), browserEntry.getModel()))
.toList()
: selected;
}
default LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return null;
}
default BrowserMenuCategory getCategory() {
return null;
}
default KeyCombination getShortcut() {
return null;
}
ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries);
default boolean acceptsEmptySelection() {
return false;
}
default boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return true;
}
default boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return true;
}
}

View File

@@ -0,0 +1,156 @@
package io.xpipe.app.browser.menu;
import io.xpipe.app.action.AbstractAction;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.action.BrowserActionProviders;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.comp.base.TooltipHelper;
import io.xpipe.app.hub.action.StoreAction;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.LicenseProvider;
import javafx.scene.control.Button;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Region;
import lombok.SneakyThrows;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public interface BrowserMenuLeafProvider extends BrowserMenuItemProvider {
default void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) throws Exception {
createAction(model, entries).executeAsync();
}
default Class<? extends BrowserActionProvider> getDelegateActionProvider() {
return null;
}
@Override
default boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
if (getDelegateActionProvider() != null) {
var provider = BrowserActionProviders.forClass(getDelegateActionProvider());
return provider.isApplicable(model, entries);
} else {
return true;
}
}
@SneakyThrows
default AbstractAction createAction(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var c = getDelegateActionProvider() != null
? BrowserActionProviders.forClass(getDelegateActionProvider())
.getActionClass()
.orElseThrow()
: getActionClass().orElseThrow();
var bm = c.getDeclaredMethod("builder");
bm.setAccessible(true);
var b = bm.invoke(null);
if (StoreAction.class.isAssignableFrom(c)) {
var refMethod = b.getClass().getMethod("ref", DataStoreEntryRef.class);
refMethod.setAccessible(true);
refMethod.invoke(b, model.getEntry());
}
if (BrowserAction.class.isAssignableFrom(c)) {
var modelMethod = b.getClass().getMethod("model", BrowserFileSystemTabModel.class);
modelMethod.setAccessible(true);
modelMethod.invoke(b, model);
var entriesMethod = b.getClass().getMethod("files", List.class);
entriesMethod.setAccessible(true);
entriesMethod.invoke(
b,
entries.stream()
.map(browserEntry -> browserEntry.getRawFileEntry().getPath())
.toList());
}
var m = b.getClass().getDeclaredMethod("build");
m.setAccessible(true);
var defValue = c.cast(m.invoke(b));
return (AbstractAction) defValue;
}
default Button toButton(Region root, BrowserFileSystemTabModel model, List<BrowserEntry> selected) {
var b = new Button();
b.setOnAction(event -> {
try {
execute(model, selected);
} catch (Exception e) {
throw new RuntimeException(e);
}
event.consume();
});
var name = getName(model, selected);
Tooltip.install(b, TooltipHelper.create(name, getShortcut()));
var graphic = getIcon(model, selected);
if (graphic != null) {
b.setGraphic(graphic.createGraphicNode());
}
b.setMnemonicParsing(false);
b.accessibleTextProperty().bind(name);
root.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (getShortcut() != null && getShortcut().match(event)) {
b.fire();
event.consume();
}
});
b.setDisable(!isActive(model, selected));
model.getCurrentPath().addListener((observable, oldValue, newValue) -> {
b.setDisable(!isActive(model, selected));
});
if (getLicensedFeatureId() != null
&& !LicenseProvider.get().getFeature(getLicensedFeatureId()).isSupported()) {
b.setDisable(true);
b.setGraphic(new FontIcon("mdi2p-professional-hexagon"));
}
return b;
}
default MenuItem toMenuItem(BrowserFileSystemTabModel model, List<BrowserEntry> selected) {
var name = getName(model, selected);
var mi = new MenuItem();
mi.textProperty().bind(BindingsHelper.map(name, s -> {
if (getLicensedFeatureId() != null) {
return LicenseProvider.get().getFeature(getLicensedFeatureId()).suffix(s);
}
return s;
}));
mi.setOnAction(event -> {
try {
execute(model, selected);
} catch (Exception e) {
throw new RuntimeException(e);
}
event.consume();
});
if (getShortcut() != null) {
mi.setAccelerator(getShortcut());
}
var graphic = getIcon(model, selected);
if (graphic != null) {
mi.setGraphic(graphic.createGraphicNode());
}
mi.setMnemonicParsing(false);
mi.setDisable(!isActive(model, selected));
if (getLicensedFeatureId() != null
&& !LicenseProvider.get().getFeature(getLicensedFeatureId()).isSupported()) {
mi.setDisable(true);
}
return mi;
}
}

View File

@@ -0,0 +1,38 @@
package io.xpipe.app.browser.menu;
import io.xpipe.app.action.ActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import java.util.List;
public class BrowserMenuProviders {
public static List<BrowserMenuLeafProvider> getFlattened(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return ActionProvider.ALL.stream()
.map(browserAction -> browserAction instanceof BrowserMenuItemProvider ba
? getFlattened(ba, model, entries)
: List.<BrowserMenuLeafProvider>of())
.flatMap(List::stream)
.toList();
}
public static List<BrowserMenuLeafProvider> getFlattened(
BrowserMenuItemProvider browserAction, BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return browserAction instanceof BrowserMenuLeafProvider
? List.of((BrowserMenuLeafProvider) browserAction)
: ((BrowserMenuBranchProvider) browserAction)
.getBranchingActions(model, entries).stream()
.map(action -> getFlattened(action, model, entries))
.flatMap(List::stream)
.toList();
}
public static BrowserMenuLeafProvider byId(String id, BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return getFlattened(model, entries).stream()
.filter(browserAction -> id.equals(browserAction.getId()))
.findAny()
.orElseThrow();
}
}

View File

@@ -1,20 +1,18 @@
package io.xpipe.ext.base.browser;
package io.xpipe.app.browser.menu;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.icon.BrowserIconFileType;
import io.xpipe.app.browser.icon.BrowserIcons;
import javafx.scene.Node;
import io.xpipe.app.util.LabelGraphic;
import java.util.List;
public interface FileTypeAction extends BrowserAction {
public interface FileTypeMenuProvider extends BrowserMenuItemProvider {
@Override
default Node getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return BrowserIcons.createIcon(getType()).createRegion();
default LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new LabelGraphic.CompGraphic(BrowserIcons.createContextMenuIcon(getType()));
}
@Override

View File

@@ -1,7 +1,5 @@
package io.xpipe.ext.base.browser;
package io.xpipe.app.browser.menu;
import io.xpipe.app.browser.action.BrowserBranchAction;
import io.xpipe.app.browser.action.BrowserLeafAction;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.core.AppI18n;
@@ -14,15 +12,16 @@ import javafx.beans.value.ObservableValue;
import java.util.List;
public abstract class MultiExecuteAction implements BrowserBranchAction {
public abstract class MultiExecuteMenuProvider implements BrowserMenuBranchProvider {
protected abstract CommandBuilder createCommand(
ShellControl sc, BrowserFileSystemTabModel model, BrowserEntry entry);
@Override
public List<BrowserLeafAction> getBranchingActions(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
public List<BrowserMenuLeafProvider> getBranchingActions(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return List.of(
new BrowserLeafAction() {
new BrowserMenuLeafProvider() {
@Override
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
@@ -62,7 +61,7 @@ public abstract class MultiExecuteAction implements BrowserBranchAction {
return AppPrefs.get().terminalType().getValue() != null;
}
},
new BrowserLeafAction() {
new BrowserMenuLeafProvider() {
@Override
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
@@ -87,7 +86,7 @@ public abstract class MultiExecuteAction implements BrowserBranchAction {
return AppI18n.observable("runInFileBrowser");
}
},
new BrowserLeafAction() {
new BrowserMenuLeafProvider() {
@Override
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {

View File

@@ -0,0 +1,83 @@
package io.xpipe.app.browser.menu;
import io.xpipe.app.browser.action.impl.RunCommandInBackgroundActionProvider;
import io.xpipe.app.browser.action.impl.RunCommandInBrowserActionProvider;
import io.xpipe.app.browser.action.impl.RunCommandInTerminalActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.prefs.AppPrefs;
import javafx.beans.value.ObservableValue;
import java.util.List;
public abstract class MultiExecuteSelectionMenuProvider implements BrowserMenuBranchProvider {
protected abstract String createCommand(BrowserFileSystemTabModel model);
protected abstract String getTerminalTitle();
@Override
public List<BrowserMenuLeafProvider> getBranchingActions(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return List.of(
new BrowserMenuLeafProvider() {
@Override
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var builder = RunCommandInTerminalActionProvider.Action.builder();
builder.initEntries(model, entries);
builder.title(getTerminalTitle());
builder.command(createCommand(model));
builder.build().executeAsync();
}
@Override
public ObservableValue<String> getName(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var t = AppPrefs.get().terminalType().getValue();
return AppI18n.observable(
"executeInTerminal",
t != null ? t.toTranslatedString().getValue() : "?");
}
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return AppPrefs.get().terminalType().getValue() != null;
}
},
new BrowserMenuLeafProvider() {
@Override
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var builder = RunCommandInBrowserActionProvider.Action.builder();
builder.initEntries(model, entries);
builder.command(createCommand(model));
builder.build().executeAsync();
}
@Override
public ObservableValue<String> getName(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return AppI18n.observable("runInFileBrowser");
}
},
new BrowserMenuLeafProvider() {
@Override
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var builder = RunCommandInBackgroundActionProvider.Action.builder();
builder.initEntries(model, entries);
builder.command(createCommand(model));
builder.build().executeAsync();
}
@Override
public ObservableValue<String> getName(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return AppI18n.observable("runSilent");
}
});
}
}

View File

@@ -1,21 +1,19 @@
package io.xpipe.ext.base.browser;
package io.xpipe.app.browser.menu.impl;
import io.xpipe.app.browser.action.BrowserLeafAction;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.LabelGraphic;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class BackAction implements BrowserLeafAction {
public class BackMenuProvider implements BrowserMenuLeafProvider {
@Override
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
@@ -27,8 +25,8 @@ public class BackAction implements BrowserLeafAction {
}
@Override
public Node getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new FontIcon("fth-arrow-left");
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new LabelGraphic.IconGraphic("fth-arrow-left");
}
@Override

View File

@@ -0,0 +1,41 @@
package io.xpipe.app.browser.menu.impl;
import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.action.impl.BrowseInNativeManagerActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.menu.BrowserMenuCategory;
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.core.AppI18n;
import io.xpipe.core.process.OsType;
import javafx.beans.value.ObservableValue;
import java.util.List;
public class BrowseInNativeManagerMenuProvider implements BrowserMenuLeafProvider {
@Override
public Class<? extends BrowserActionProvider> getDelegateActionProvider() {
return BrowseInNativeManagerActionProvider.class;
}
@Override
public BrowserMenuCategory getCategory() {
return BrowserMenuCategory.OPEN;
}
@Override
public boolean acceptsEmptySelection() {
return true;
}
@Override
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return switch (OsType.getLocal()) {
case OsType.Windows windows -> AppI18n.observable("browseInWindowsExplorer");
case OsType.Linux linux -> AppI18n.observable("browseInDefaultFileManager");
case OsType.MacOs macOs -> AppI18n.observable("browseInFinder");
};
}
}

View File

@@ -0,0 +1,175 @@
package io.xpipe.app.browser.menu.impl;
import io.xpipe.app.browser.action.impl.ChgrpActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.menu.BrowserMenuBranchProvider;
import io.xpipe.app.browser.menu.BrowserMenuCategory;
import io.xpipe.app.browser.menu.BrowserMenuItemProvider;
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileKind;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TextField;
import java.util.List;
import java.util.stream.Stream;
public class ChgrpMenuProvider implements BrowserMenuBranchProvider {
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new LabelGraphic.IconGraphic("mdi2a-account-group-outline");
}
@Override
public BrowserMenuCategory getCategory() {
return BrowserMenuCategory.MUTATION;
}
@Override
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return AppI18n.observable("chgrp");
}
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var os = model.getFileSystem().getShell().orElseThrow().getOsType();
return os != OsType.WINDOWS && os != OsType.MACOS;
}
@Override
public List<BrowserMenuItemProvider> getBranchingActions(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
if (entries.stream()
.anyMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY)) {
return List.of(new FlatProvider(), new RecursiveProvider());
} else {
return getLeafActions(model, false);
}
}
private static class FlatProvider implements BrowserMenuBranchProvider {
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new LabelGraphic.IconGraphic("mdi2f-file-outline");
}
@Override
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return AppI18n.observable("flat");
}
@Override
public List<BrowserMenuItemProvider> getBranchingActions(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return getLeafActions(model, false);
}
}
private static class RecursiveProvider implements BrowserMenuBranchProvider {
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new LabelGraphic.IconGraphic("mdi2f-file-tree");
}
@Override
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return AppI18n.observable("recursive");
}
@Override
public List<BrowserMenuItemProvider> getBranchingActions(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return getLeafActions(model, true);
}
}
private static List<BrowserMenuItemProvider> getLeafActions(BrowserFileSystemTabModel model, boolean recursive) {
List<BrowserMenuItemProvider> actions = Stream.<BrowserMenuItemProvider>concat(
model.getCache().getGroups().entrySet().stream()
.filter(e -> !e.getValue().equals("nohome")
&& !e.getValue().equals("nogroup")
&& !e.getValue().equals("nobody")
&& (e.getKey().equals(0) || e.getKey() >= 900))
.map(e -> e.getValue())
.map(s -> (BrowserMenuLeafProvider) new FixedProvider(s, recursive)),
Stream.of(new CustomProvider(recursive)))
.toList();
return actions;
}
private static class FixedProvider implements BrowserMenuLeafProvider {
private final String group;
private final boolean recursive;
private FixedProvider(String group, boolean recursive) {
this.group = group;
this.recursive = recursive;
}
@Override
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new SimpleStringProperty(group);
}
@Override
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var builder = ChgrpActionProvider.Action.builder();
builder.initEntries(model, entries);
builder.group(group);
builder.recursive(recursive);
var action = builder.build();
action.executeAsync();
}
}
private static class CustomProvider implements BrowserMenuLeafProvider {
private final boolean recursive;
private CustomProvider(boolean recursive) {
this.recursive = recursive;
}
@Override
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return AppI18n.observable("custom");
}
@Override
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var group = new SimpleStringProperty();
var modal = ModalOverlay.of(
"groupName",
Comp.of(() -> {
var creationName = new TextField();
creationName.textProperty().bindBidirectional(group);
return creationName;
})
.prefWidth(350));
modal.withDefaultButtons(() -> {
if (group.getValue() == null) {
return;
}
var builder = ChgrpActionProvider.Action.builder();
builder.initEntries(model, entries);
builder.group(group.getValue());
builder.recursive(recursive);
var action = builder.build();
action.executeAsync();
});
modal.show();
}
}
}

View File

@@ -0,0 +1,173 @@
package io.xpipe.app.browser.menu.impl;
import io.xpipe.app.browser.action.impl.ChmodActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.menu.BrowserMenuBranchProvider;
import io.xpipe.app.browser.menu.BrowserMenuCategory;
import io.xpipe.app.browser.menu.BrowserMenuItemProvider;
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileKind;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TextField;
import java.util.List;
public class ChmodMenuProvider implements BrowserMenuBranchProvider {
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new LabelGraphic.IconGraphic("mdi2w-wrench-outline");
}
@Override
public BrowserMenuCategory getCategory() {
return BrowserMenuCategory.MUTATION;
}
@Override
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return AppI18n.observable("chmod");
}
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS;
}
@Override
public List<BrowserMenuItemProvider> getBranchingActions(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
if (entries.stream()
.anyMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY)) {
return List.of(new FlatProvider(), new RecursiveProvider());
} else {
return getLeafActions(model, false);
}
}
private static class FlatProvider implements BrowserMenuBranchProvider {
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new LabelGraphic.IconGraphic("mdi2f-file-outline");
}
@Override
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return AppI18n.observable("flat");
}
@Override
public List<BrowserMenuItemProvider> getBranchingActions(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return getLeafActions(model, false);
}
}
private static class RecursiveProvider implements BrowserMenuBranchProvider {
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new LabelGraphic.IconGraphic("mdi2f-file-tree");
}
@Override
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return AppI18n.observable("recursive");
}
@Override
public List<BrowserMenuItemProvider> getBranchingActions(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return getLeafActions(model, true);
}
}
private static List<BrowserMenuItemProvider> getLeafActions(BrowserFileSystemTabModel model, boolean recursive) {
var custom = new CustomProvider(recursive);
return List.of(
new FixedProvider("400", recursive),
new FixedProvider("600", recursive),
new FixedProvider("644", recursive),
new FixedProvider("700", recursive),
new FixedProvider("755", recursive),
new FixedProvider("777", recursive),
new FixedProvider("u+x", recursive),
new FixedProvider("a+x", recursive),
custom);
}
private static class FixedProvider implements BrowserMenuLeafProvider {
private final String permissions;
private final boolean recursive;
private FixedProvider(String permissions, boolean recursive) {
this.permissions = permissions;
this.recursive = recursive;
}
@Override
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new SimpleStringProperty(permissions);
}
@Override
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var builder = ChmodActionProvider.Action.builder();
builder.initEntries(model, entries);
builder.permissions(permissions);
builder.recursive(recursive);
var action = builder.build();
action.executeAsync();
}
}
private static class CustomProvider implements BrowserMenuLeafProvider {
private final boolean recursive;
private CustomProvider(boolean recursive) {
this.recursive = recursive;
}
@Override
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return AppI18n.observable("custom");
}
@Override
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var permissions = new SimpleStringProperty();
var modal = ModalOverlay.of(
"chmodPermissions",
Comp.of(() -> {
var creationName = new TextField();
creationName.textProperty().bindBidirectional(permissions);
return creationName;
})
.prefWidth(350));
modal.withDefaultButtons(() -> {
if (permissions.getValue() == null) {
return;
}
var builder = ChmodActionProvider.Action.builder();
builder.initEntries(model, entries);
builder.permissions(permissions.getValue());
builder.recursive(recursive);
var action = builder.build();
action.executeAsync();
});
modal.show();
}
}
}

View File

@@ -0,0 +1,174 @@
package io.xpipe.app.browser.menu.impl;
import io.xpipe.app.browser.action.impl.ChownActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.menu.BrowserMenuBranchProvider;
import io.xpipe.app.browser.menu.BrowserMenuCategory;
import io.xpipe.app.browser.menu.BrowserMenuItemProvider;
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileKind;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TextField;
import java.util.List;
import java.util.stream.Stream;
public class ChownMenuProvider implements BrowserMenuBranchProvider {
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new LabelGraphic.IconGraphic("mdi2a-account-edit");
}
@Override
public BrowserMenuCategory getCategory() {
return BrowserMenuCategory.MUTATION;
}
@Override
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return AppI18n.observable("chown");
}
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var os = model.getFileSystem().getShell().orElseThrow().getOsType();
return os != OsType.WINDOWS && os != OsType.MACOS;
}
@Override
public List<BrowserMenuItemProvider> getBranchingActions(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
if (entries.stream()
.anyMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY)) {
return List.of(new FlatProvider(), new RecursiveProvider());
} else {
return getLeafActions(model, false);
}
}
private static class FlatProvider implements BrowserMenuBranchProvider {
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new LabelGraphic.IconGraphic("mdi2f-file-outline");
}
@Override
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return AppI18n.observable("flat");
}
@Override
public List<BrowserMenuItemProvider> getBranchingActions(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return getLeafActions(model, false);
}
}
private static class RecursiveProvider implements BrowserMenuBranchProvider {
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new LabelGraphic.IconGraphic("mdi2f-file-tree");
}
@Override
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return AppI18n.observable("recursive");
}
@Override
public List<BrowserMenuItemProvider> getBranchingActions(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return getLeafActions(model, true);
}
}
private static List<BrowserMenuItemProvider> getLeafActions(BrowserFileSystemTabModel model, boolean recursive) {
var actions = Stream.<BrowserMenuItemProvider>concat(
model.getCache().getUsers().entrySet().stream()
.filter(e -> !e.getValue().equals("nohome")
&& !e.getValue().equals("nobody")
&& (e.getKey().equals(0) || e.getKey() >= 900))
.map(e -> e.getValue())
.map(s -> (BrowserMenuLeafProvider) new FixedProvider(s, recursive)),
Stream.of(new CustomProvider(recursive)))
.toList();
return actions;
}
private static class FixedProvider implements BrowserMenuLeafProvider {
private final String owner;
private final boolean recursive;
private FixedProvider(String owner, boolean recursive) {
this.owner = owner;
this.recursive = recursive;
}
@Override
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new SimpleStringProperty(owner);
}
@Override
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var builder = ChownActionProvider.Action.builder();
builder.initEntries(model, entries);
builder.owner(owner);
builder.recursive(recursive);
var action = builder.build();
action.executeAsync();
}
}
private static class CustomProvider implements BrowserMenuLeafProvider {
private final boolean recursive;
private CustomProvider(boolean recursive) {
this.recursive = recursive;
}
@Override
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return AppI18n.observable("custom");
}
@Override
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var user = new SimpleStringProperty();
var modal = ModalOverlay.of(
"userName",
Comp.of(() -> {
var creationName = new TextField();
creationName.textProperty().bindBidirectional(user);
return creationName;
})
.prefWidth(350));
modal.withDefaultButtons(() -> {
if (user.getValue() == null) {
return;
}
var builder = ChownActionProvider.Action.builder();
builder.initEntries(model, entries);
builder.owner(user.getValue());
builder.recursive(recursive);
var action = builder.build();
action.executeAsync();
});
modal.show();
}
}
}

View File

@@ -0,0 +1,57 @@
package io.xpipe.app.browser.menu.impl;
import io.xpipe.app.action.AbstractAction;
import io.xpipe.app.browser.action.impl.ComputeDirectorySizesActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.menu.BrowserMenuCategory;
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.core.store.FileKind;
import javafx.beans.value.ObservableValue;
import java.util.List;
public class ComputeDirectorySizesMenuProvider implements BrowserMenuLeafProvider {
@Override
public AbstractAction createAction(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var builder = ComputeDirectorySizesActionProvider.Action.builder();
builder.initEntries(model, entries);
return builder.build();
}
public String getId() {
return "computeDirectorySizes";
}
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new LabelGraphic.IconGraphic("mdi2f-format-list-text");
}
@Override
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var topLevel =
entries.size() == 1 && entries.getFirst().getRawFileEntry().equals(model.getCurrentDirectory());
return AppI18n.observable(topLevel ? "computeDirectorySizes" : "computeSize");
}
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return entries.stream()
.allMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY);
}
@Override
public boolean acceptsEmptySelection() {
return true;
}
@Override
public BrowserMenuCategory getCategory() {
return BrowserMenuCategory.ACTION;
}
}

View File

@@ -1,22 +1,21 @@
package io.xpipe.ext.base.browser;
package io.xpipe.app.browser.menu.impl;
import io.xpipe.app.browser.action.BrowserLeafAction;
import io.xpipe.app.browser.file.BrowserClipboard;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.menu.BrowserMenuCategory;
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.LabelGraphic;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class CopyAction implements BrowserLeafAction {
public class CopyMenuProvider implements BrowserMenuLeafProvider {
@Override
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
@@ -24,13 +23,13 @@ public class CopyAction implements BrowserLeafAction {
}
@Override
public Node getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new FontIcon("mdi2c-content-copy");
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new LabelGraphic.IconGraphic("mdoal-file_copy");
}
@Override
public Category getCategory() {
return Category.COPY_PASTE;
public BrowserMenuCategory getCategory() {
return BrowserMenuCategory.COPY_PASTE;
}
@Override

View File

@@ -1,13 +1,13 @@
package io.xpipe.ext.base.browser;
package io.xpipe.app.browser.menu.impl;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionFormatter;
import io.xpipe.app.browser.action.BrowserBranchAction;
import io.xpipe.app.browser.action.BrowserLeafAction;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.menu.BrowserMenuBranchProvider;
import io.xpipe.app.browser.menu.BrowserMenuCategory;
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.ClipboardHelper;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.core.store.FileKind;
import javafx.beans.property.SimpleObjectProperty;
@@ -19,11 +19,11 @@ import javafx.scene.input.KeyCombination;
import java.util.List;
import java.util.stream.Collectors;
public class CopyPathAction implements BrowserAction, BrowserBranchAction {
public class CopyPathMenuProvider implements BrowserMenuBranchProvider {
@Override
public Category getCategory() {
return Category.COPY_PASTE;
public BrowserMenuCategory getCategory() {
return BrowserMenuCategory.COPY_PASTE;
}
@Override
@@ -37,9 +37,15 @@ public class CopyPathAction implements BrowserAction, BrowserBranchAction {
}
@Override
public List<BrowserLeafAction> getBranchingActions(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new LabelGraphic.IconGraphic("mdi2c-content-copy");
}
@Override
public List<BrowserMenuLeafProvider> getBranchingActions(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return List.of(
new BrowserLeafAction() {
new BrowserMenuLeafProvider() {
@Override
public KeyCombination getShortcut() {
return new KeyCodeCombination(KeyCode.C, KeyCombination.ALT_DOWN, KeyCombination.SHORTCUT_DOWN);
@@ -49,7 +55,7 @@ public class CopyPathAction implements BrowserAction, BrowserBranchAction {
public ObservableValue<String> getName(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
if (entries.size() == 1) {
return new SimpleObjectProperty<>(BrowserActionFormatter.centerEllipsis(
return new SimpleObjectProperty<>(centerEllipsis(
entries.getFirst()
.getRawFileEntry()
.getPath()
@@ -68,12 +74,12 @@ public class CopyPathAction implements BrowserAction, BrowserBranchAction {
ClipboardHelper.copyText(s);
}
},
new BrowserLeafAction() {
new BrowserMenuLeafProvider() {
@Override
public ObservableValue<String> getName(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
if (entries.size() == 1) {
return new SimpleObjectProperty<>(BrowserActionFormatter.centerEllipsis(
return new SimpleObjectProperty<>(centerEllipsis(
entries.getFirst()
.getRawFileEntry()
.getPath()
@@ -104,13 +110,13 @@ public class CopyPathAction implements BrowserAction, BrowserBranchAction {
ClipboardHelper.copyText(s);
}
},
new BrowserLeafAction() {
new BrowserMenuLeafProvider() {
@Override
public ObservableValue<String> getName(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
if (entries.size() == 1) {
return new SimpleObjectProperty<>("\""
+ BrowserActionFormatter.centerEllipsis(
+ centerEllipsis(
entries.getFirst()
.getRawFileEntry()
.getPath()
@@ -138,7 +144,7 @@ public class CopyPathAction implements BrowserAction, BrowserBranchAction {
ClipboardHelper.copyText(s);
}
},
new BrowserLeafAction() {
new BrowserMenuLeafProvider() {
@Override
public KeyCombination getShortcut() {
return new KeyCodeCombination(
@@ -149,7 +155,7 @@ public class CopyPathAction implements BrowserAction, BrowserBranchAction {
public ObservableValue<String> getName(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
if (entries.size() == 1) {
return new SimpleObjectProperty<>(BrowserActionFormatter.centerEllipsis(
return new SimpleObjectProperty<>(centerEllipsis(
entries.getFirst()
.getRawFileEntry()
.getPath()
@@ -168,12 +174,12 @@ public class CopyPathAction implements BrowserAction, BrowserBranchAction {
ClipboardHelper.copyText(s);
}
},
new BrowserLeafAction() {
new BrowserMenuLeafProvider() {
@Override
public ObservableValue<String> getName(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
if (entries.size() == 1) {
return new SimpleObjectProperty<>(BrowserActionFormatter.centerEllipsis(
return new SimpleObjectProperty<>(centerEllipsis(
entries.getFirst()
.getRawFileEntry()
.getPath()
@@ -211,13 +217,13 @@ public class CopyPathAction implements BrowserAction, BrowserBranchAction {
ClipboardHelper.copyText(s);
}
},
new BrowserLeafAction() {
new BrowserMenuLeafProvider() {
@Override
public ObservableValue<String> getName(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
if (entries.size() == 1) {
return new SimpleObjectProperty<>("\""
+ BrowserActionFormatter.centerEllipsis(
+ centerEllipsis(
entries.getFirst()
.getRawFileEntry()
.getPath()
@@ -247,4 +253,17 @@ public class CopyPathAction implements BrowserAction, BrowserBranchAction {
}
});
}
private static String centerEllipsis(String input, int length) {
if (input == null) {
return "";
}
if (input.length() <= length) {
return input;
}
var half = (length / 2) - 5;
return input.substring(0, half) + " ... " + input.substring(input.length() - half);
}
}

View File

@@ -0,0 +1,66 @@
package io.xpipe.app.browser.menu.impl;
import io.xpipe.app.action.AbstractAction;
import io.xpipe.app.browser.action.impl.DeleteActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.menu.BrowserMenuCategory;
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.core.store.FileKind;
import javafx.beans.value.ObservableValue;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import java.util.List;
public class DeleteMenuProvider implements BrowserMenuLeafProvider {
@Override
public AbstractAction createAction(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var link = entries.stream()
.anyMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.LINK);
var files = entries.stream()
.map(browserEntry -> !link
? browserEntry.getRawFileEntry().resolved().getPath()
: browserEntry.getRawFileEntry().getPath())
.toList();
var builder = DeleteActionProvider.Action.builder();
builder.initFiles(model, files);
return builder.build();
}
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new LabelGraphic.IconGraphic("mdi2d-delete");
}
@Override
public BrowserMenuCategory getCategory() {
return BrowserMenuCategory.MUTATION;
}
@Override
public KeyCombination getShortcut() {
return new KeyCodeCombination(KeyCode.DELETE);
}
@Override
public boolean automaticallyResolveLinks() {
return false;
}
@Override
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return AppI18n.observable(
"deleteFile",
entries.stream()
.anyMatch(browserEntry ->
browserEntry.getRawFileEntry().getKind() == FileKind.LINK)
? "link"
: "");
}
}

View File

@@ -1,22 +1,21 @@
package io.xpipe.ext.base.browser;
package io.xpipe.app.browser.menu.impl;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.browser.action.BrowserLeafAction;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.menu.BrowserMenuCategory;
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.LabelGraphic;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class DownloadAction implements BrowserLeafAction {
public class DownloadMenuProvider implements BrowserMenuLeafProvider {
@Override
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
@@ -33,13 +32,13 @@ public class DownloadAction implements BrowserLeafAction {
}
@Override
public Node getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new FontIcon("mdi2d-download");
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new LabelGraphic.IconGraphic("mdi2d-download");
}
@Override
public Category getCategory() {
return Category.MUTATION;
public BrowserMenuCategory getCategory() {
return BrowserMenuCategory.ACTION;
}
@Override

View File

@@ -1,24 +1,23 @@
package io.xpipe.ext.base.browser;
package io.xpipe.app.browser.menu.impl;
import io.xpipe.app.browser.action.BrowserLeafAction;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileOpener;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.menu.BrowserMenuCategory;
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.core.store.FileKind;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class EditFileAction implements BrowserLeafAction {
public class EditFileMenuProvider implements BrowserMenuLeafProvider {
@Override
public KeyCombination getShortcut() {
@@ -33,13 +32,13 @@ public class EditFileAction implements BrowserLeafAction {
}
@Override
public Node getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new FontIcon("mdi2p-pencil");
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new LabelGraphic.IconGraphic("mdi2p-pencil");
}
@Override
public Category getCategory() {
return Category.OPEN;
public BrowserMenuCategory getCategory() {
return BrowserMenuCategory.OPEN;
}
@Override

View File

@@ -1,19 +1,18 @@
package io.xpipe.ext.base.browser;
package io.xpipe.app.browser.menu.impl;
import io.xpipe.app.browser.action.BrowserLeafAction;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.menu.BrowserMenuCategory;
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.core.store.FileKind;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class FollowLinkAction implements BrowserLeafAction {
public class FollowLinkMenuProvider implements BrowserMenuLeafProvider {
@Override
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
@@ -22,13 +21,13 @@ public class FollowLinkAction implements BrowserLeafAction {
}
@Override
public Node getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new FontIcon("mdi2a-arrow-top-right-thick");
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new LabelGraphic.IconGraphic("mdi2a-arrow-top-right-thick");
}
@Override
public Category getCategory() {
return Category.OPEN;
public BrowserMenuCategory getCategory() {
return BrowserMenuCategory.OPEN;
}
@Override

View File

@@ -1,21 +1,19 @@
package io.xpipe.ext.base.browser;
package io.xpipe.app.browser.menu.impl;
import io.xpipe.app.browser.action.BrowserLeafAction;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.LabelGraphic;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class ForwardAction implements BrowserLeafAction {
public class ForwardMenuProvider implements BrowserMenuLeafProvider {
@Override
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
@@ -27,8 +25,8 @@ public class ForwardAction implements BrowserLeafAction {
}
@Override
public Node getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new FontIcon("fth-arrow-right");
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new LabelGraphic.IconGraphic("fth-arrow-right");
}
@Override

View File

@@ -1,9 +1,12 @@
package io.xpipe.ext.base.browser;
package io.xpipe.app.browser.menu.impl;
import io.xpipe.app.browser.action.BrowserActionFormatter;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.icon.BrowserIconFileType;
import io.xpipe.app.browser.menu.BrowserApplicationPathMenuProvider;
import io.xpipe.app.browser.menu.BrowserMenuCategory;
import io.xpipe.app.browser.menu.FileTypeMenuProvider;
import io.xpipe.app.browser.menu.MultiExecuteMenuProvider;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.ShellControl;
@@ -12,21 +15,23 @@ import javafx.beans.value.ObservableValue;
import java.util.List;
public class JarAction extends MultiExecuteAction implements JavaAction, FileTypeAction {
public class JarMenuProvider extends MultiExecuteMenuProvider
implements BrowserApplicationPathMenuProvider, FileTypeMenuProvider {
@Override
public Category getCategory() {
return Category.CUSTOM;
public BrowserMenuCategory getCategory() {
return BrowserMenuCategory.CUSTOM;
}
@Override
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return new SimpleStringProperty("java -jar " + BrowserActionFormatter.filesArgument(entries));
var arg = entries.size() == 1 ? entries.getFirst().getFileName() : "(" + entries.size() + ")";
return new SimpleStringProperty("java -jar " + arg);
}
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return super.isApplicable(model, entries) && FileTypeAction.super.isApplicable(model, entries);
return super.isApplicable(model, entries) && FileTypeMenuProvider.super.isApplicable(model, entries);
}
@Override
@@ -40,4 +45,9 @@ public class JarAction extends MultiExecuteAction implements JavaAction, FileTyp
public BrowserIconFileType getType() {
return BrowserIconFileType.byId("jar");
}
@Override
public String getExecutable() {
return "java";
}
}

View File

@@ -0,0 +1,61 @@
package io.xpipe.app.browser.menu.impl;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.icon.BrowserIconFileType;
import io.xpipe.app.browser.menu.BrowserApplicationPathMenuProvider;
import io.xpipe.app.browser.menu.BrowserMenuCategory;
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.browser.menu.FileTypeMenuProvider;
import io.xpipe.app.util.FileOpener;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.ShellControl;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import java.util.List;
public class JavapMenuProvider
implements FileTypeMenuProvider, BrowserApplicationPathMenuProvider, BrowserMenuLeafProvider {
@Override
public BrowserMenuCategory getCategory() {
return BrowserMenuCategory.CUSTOM;
}
@Override
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var arg = entries.size() == 1 ? entries.getFirst().getFileName() : "(" + entries.size() + ")";
return new SimpleStringProperty("javap -c -p " + arg);
}
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return FileTypeMenuProvider.super.isApplicable(model, entries);
}
@Override
public BrowserIconFileType getType() {
return BrowserIconFileType.byId("class");
}
@Override
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) throws Exception {
ShellControl sc = model.getFileSystem().getShell().orElseThrow();
for (BrowserEntry entry : entries) {
var command = CommandBuilder.of()
.add("javap", "-c", "-p")
.addFile(entry.getRawFileEntry().getPath());
var out = sc.command(command)
.withWorkingDirectory(model.getCurrentDirectory().getPath())
.readStdoutOrThrow();
FileOpener.openReadOnlyString(out);
}
}
@Override
public String getExecutable() {
return "java";
}
}

Some files were not shown because too many files have changed in this diff Show More