mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-04-22 15:40:31 -04:00
Rework
This commit is contained in:
@@ -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`.
|
||||
|
||||
@@ -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")
|
||||
|
||||
191
app/src/main/java/io/xpipe/app/action/AbstractAction.java
Normal file
191
app/src/main/java/io/xpipe/app/action/AbstractAction.java
Normal 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();
|
||||
}
|
||||
105
app/src/main/java/io/xpipe/app/action/ActionConfigComp.java
Normal file
105
app/src/main/java/io/xpipe/app/action/ActionConfigComp.java
Normal 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;
|
||||
}
|
||||
}
|
||||
79
app/src/main/java/io/xpipe/app/action/ActionConfirmComp.java
Normal file
79
app/src/main/java/io/xpipe/app/action/ActionConfirmComp.java
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
105
app/src/main/java/io/xpipe/app/action/ActionJacksonMapper.java
Normal file
105
app/src/main/java/io/xpipe/app/action/ActionJacksonMapper.java
Normal 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;
|
||||
}
|
||||
}
|
||||
27
app/src/main/java/io/xpipe/app/action/ActionPickComp.java
Normal file
27
app/src/main/java/io/xpipe/app/action/ActionPickComp.java
Normal 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();
|
||||
}
|
||||
}
|
||||
57
app/src/main/java/io/xpipe/app/action/ActionProvider.java
Normal file
57
app/src/main/java/io/xpipe/app/action/ActionProvider.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
146
app/src/main/java/io/xpipe/app/action/ActionUrls.java
Normal file
146
app/src/main/java/io/xpipe/app/action/ActionUrls.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
23
app/src/main/java/io/xpipe/app/action/XPipeUrlProvider.java
Normal file
23
app/src/main/java/io/xpipe/app/action/XPipeUrlProvider.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.xpipe.app.browser.menu;
|
||||
|
||||
public enum BrowserMenuCategory {
|
||||
CUSTOM,
|
||||
OPEN,
|
||||
COPY_PASTE,
|
||||
ACTION,
|
||||
MUTATION
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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");
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
: "");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user