mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-22 07:30:24 +00:00
File browser overview page improvements
This commit is contained in:
parent
40a5c6a306
commit
52cdcfa0aa
21 changed files with 367 additions and 113 deletions
|
@ -0,0 +1,41 @@
|
|||
package io.xpipe.app.browser;
|
||||
|
||||
import io.xpipe.app.browser.icon.BrowserIcons;
|
||||
import io.xpipe.app.comp.base.ListBoxViewComp;
|
||||
import io.xpipe.app.fxcomps.Comp;
|
||||
import io.xpipe.app.fxcomps.SimpleComp;
|
||||
import io.xpipe.app.fxcomps.augment.GrowAugment;
|
||||
import io.xpipe.core.store.FileSystem;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.layout.Region;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
|
||||
@Value
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class BrowserFileOverviewComp extends SimpleComp {
|
||||
|
||||
OpenFileSystemModel model;
|
||||
ObservableList<FileSystem.FileEntry> list;
|
||||
|
||||
@Override
|
||||
protected Region createSimple() {
|
||||
var c = new ListBoxViewComp<>(list, list, entry -> {
|
||||
return Comp.of(() -> {
|
||||
var icon = BrowserIcons.createIcon(entry);
|
||||
var l = new Button(entry.getPath(), icon.createRegion());
|
||||
l.setOnAction(event -> {
|
||||
model.cd(entry.getPath());
|
||||
event.consume();
|
||||
});
|
||||
l.setAlignment(Pos.CENTER_LEFT);
|
||||
GrowAugment.create(true,false).augment(l);
|
||||
return l;
|
||||
});
|
||||
})
|
||||
.styleClass("overview-file-list");
|
||||
return c.createRegion();
|
||||
}
|
||||
}
|
|
@ -48,6 +48,10 @@ public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> {
|
|||
var fi = new FontIcon("mdi2m-magnify");
|
||||
button.setGraphic(fi);
|
||||
button.setOnAction(event -> {
|
||||
if (model.getCurrentDirectory() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (expanded.get()) {
|
||||
if (filterString.getValue() == null) {
|
||||
expanded.set(false);
|
||||
|
@ -62,6 +66,7 @@ public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> {
|
|||
|
||||
text.setPrefWidth(0);
|
||||
button.getStyleClass().add(Styles.FLAT);
|
||||
button.disableProperty().bind(model.getInOverview());
|
||||
expanded.addListener((observable, oldValue, val) -> {
|
||||
if (val) {
|
||||
text.setPrefWidth(250);
|
||||
|
@ -88,9 +93,11 @@ public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> {
|
|||
}
|
||||
}
|
||||
|
||||
private final OpenFileSystemModel model;
|
||||
private final Property<String> filterString;
|
||||
|
||||
public BrowserFilterComp(Property<String> filterString) {
|
||||
public BrowserFilterComp(OpenFileSystemModel model, Property<String> filterString) {
|
||||
this.model = model;
|
||||
this.filterString = filterString;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ public class BrowserNavBar extends SimpleComp {
|
|||
@Override
|
||||
protected Region createSimple() {
|
||||
var path = new SimpleStringProperty(model.getCurrentPath().get());
|
||||
model.getCurrentPath().addListener((observable, oldValue, newValue) -> {
|
||||
SimpleChangeListener.apply(model.getCurrentPath(), (newValue) -> {
|
||||
path.set(newValue);
|
||||
});
|
||||
path.addListener((observable, oldValue, newValue) -> {
|
||||
|
@ -49,7 +49,11 @@ public class BrowserNavBar extends SimpleComp {
|
|||
.styleClass("path-text")
|
||||
.apply(struc -> {
|
||||
SimpleChangeListener.apply(struc.get().focusedProperty(), val -> {
|
||||
struc.get().pseudoClassStateChanged(INVISIBLE, !val);
|
||||
struc.get().pseudoClassStateChanged(INVISIBLE, !val && !model.getInOverview().get());
|
||||
});
|
||||
|
||||
SimpleChangeListener.apply(model.getInOverview(), val -> {
|
||||
struc.get().pseudoClassStateChanged(INVISIBLE, !val && !struc.get().isFocused());
|
||||
});
|
||||
|
||||
struc.get().setOnMouseClicked(event -> {
|
||||
|
@ -61,13 +65,15 @@ public class BrowserNavBar extends SimpleComp {
|
|||
struc.get().selectAll();
|
||||
struc.get().requestFocus();
|
||||
});
|
||||
|
||||
struc.get().setPromptText("Overview of " + model.getName());
|
||||
});
|
||||
|
||||
var graphic = Bindings.createStringBinding(
|
||||
() -> {
|
||||
var icon = model.getCurrentDirectory() != null
|
||||
? FileIconManager.getFileIcon(model.getCurrentDirectory(), false)
|
||||
: null;
|
||||
: "home_icon.png";
|
||||
return icon;
|
||||
},
|
||||
model.getCurrentPath());
|
||||
|
@ -80,7 +86,9 @@ public class BrowserNavBar extends SimpleComp {
|
|||
graphicButton.getStyleClass().add(Styles.LEFT_PILL);
|
||||
graphicButton.getStyleClass().add("path-graphic-button");
|
||||
new ContextMenuAugment<>(
|
||||
event -> event.getButton() == MouseButton.PRIMARY, () -> new BrowserContextMenu(model, null))
|
||||
event -> event.getButton() == MouseButton.PRIMARY, () -> {
|
||||
return model.getInOverview().get() ? null : new BrowserContextMenu(model, null);
|
||||
})
|
||||
.augment(new SimpleCompStructure<>(graphicButton));
|
||||
GrowAugment.create(false, true).augment(graphicButton);
|
||||
|
||||
|
@ -92,7 +100,9 @@ public class BrowserNavBar extends SimpleComp {
|
|||
.apply(struc -> {
|
||||
var t = struc.get().getChildren().get(0);
|
||||
var b = struc.get().getChildren().get(1);
|
||||
b.visibleProperty().bind(t.focusedProperty().not());
|
||||
b.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
|
||||
return !t.isFocused() && !model.getInOverview().get();
|
||||
}, t.focusedProperty(), model.getInOverview()));
|
||||
})
|
||||
.grow(false, true);
|
||||
|
||||
|
|
|
@ -4,11 +4,13 @@ import io.xpipe.app.comp.base.SimpleTitledPaneComp;
|
|||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.fxcomps.SimpleComp;
|
||||
import io.xpipe.app.fxcomps.impl.VerticalComp;
|
||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
import io.xpipe.core.store.FileSystem;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.scene.layout.Region;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
public class BrowserOverviewComp extends SimpleComp {
|
||||
|
@ -20,16 +22,23 @@ public class BrowserOverviewComp extends SimpleComp {
|
|||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
protected Region createSimple() {
|
||||
var commonList = new BrowserSelectionListComp(FXCollections.observableArrayList(
|
||||
new FileSystem.FileEntry(model.getFileSystem(), "C:\\", Instant.now(), true, false, false, 0, null)));
|
||||
var common = new SimpleTitledPaneComp(AppI18n.observable("a"), commonList);
|
||||
ShellControl sc = model.getFileSystem().getShell().orElseThrow();
|
||||
var common = sc.getOsType().determineInterestingPaths(sc).stream().map(s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s)).toList();
|
||||
var commonOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(common));
|
||||
var commonPane = new SimpleTitledPaneComp(AppI18n.observable("common"), commonOverview);
|
||||
|
||||
var recentList = new BrowserSelectionListComp(FXCollections.observableArrayList(
|
||||
new FileSystem.FileEntry(model.getFileSystem(), "C:\\", Instant.now(), true, false, false, 0, null)));
|
||||
var recent = new SimpleTitledPaneComp(AppI18n.observable("Recent"), recentList);
|
||||
var roots = sc.getShellDialect().listRoots(sc).map(s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s)).toList();
|
||||
var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots));
|
||||
var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview);
|
||||
|
||||
var vbox = new VerticalComp(List.of(common, recent)).styleClass("home");
|
||||
|
||||
var recent = BindingsHelper.mappedContentBinding(model.getSavedState().getRecentDirectories(), s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory()));
|
||||
var recentOverview = new BrowserFileOverviewComp(model, recent);
|
||||
var recentPane = new SimpleTitledPaneComp(AppI18n.observable("recent"), recentOverview).vgrow();
|
||||
|
||||
var vbox = new VerticalComp(List.of(commonPane, rootsPane, recentPane)).styleClass("overview");
|
||||
return vbox.createRegion();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package io.xpipe.app.browser;
|
||||
|
||||
import atlantafx.base.controls.Spacer;
|
||||
import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.comp.base.ModalOverlayComp;
|
||||
import io.xpipe.app.comp.base.MultiContentComp;
|
||||
import io.xpipe.app.fxcomps.Comp;
|
||||
|
@ -9,7 +10,6 @@ import io.xpipe.app.fxcomps.SimpleCompStructure;
|
|||
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
|
||||
import io.xpipe.app.fxcomps.impl.VerticalComp;
|
||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||
import io.xpipe.app.fxcomps.util.Shortcuts;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Button;
|
||||
|
@ -44,6 +44,7 @@ public class OpenFileSystemComp extends SimpleComp {
|
|||
private Region createContent() {
|
||||
var overview = new Button(null, new FontIcon("mdi2m-monitor"));
|
||||
overview.setOnAction(e -> model.cd(null));
|
||||
overview.disableProperty().bind(model.getInOverview());
|
||||
|
||||
var backBtn = new Button(null, new FontIcon("fth-arrow-left"));
|
||||
backBtn.setOnAction(e -> model.back());
|
||||
|
@ -56,18 +57,20 @@ public class OpenFileSystemComp extends SimpleComp {
|
|||
var refreshBtn = new Button(null, new FontIcon("mdmz-refresh"));
|
||||
refreshBtn.setOnAction(e -> model.refresh());
|
||||
Shortcuts.addShortcut(refreshBtn, new KeyCodeCombination(KeyCode.F5));
|
||||
refreshBtn.disableProperty().bind(model.getInOverview());
|
||||
|
||||
var terminalBtn = new Button(null, new FontIcon("mdi2c-code-greater-than"));
|
||||
var terminalBtn = BrowserAction.byId("openTerminal").toButton(model, List.of());
|
||||
terminalBtn.setOnAction(
|
||||
e -> model.openTerminalAsync(model.getCurrentPath().get()));
|
||||
terminalBtn.disableProperty().bind(PlatformThread.sync(model.getNoDirectory()));
|
||||
terminalBtn.disableProperty().bind(model.getInOverview());
|
||||
|
||||
var menuButton = new MenuButton(null, new FontIcon("mdral-folder_open"));
|
||||
new ContextMenuAugment<>(
|
||||
event -> event.getButton() == MouseButton.PRIMARY, () -> new BrowserContextMenu(model, null))
|
||||
.augment(new SimpleCompStructure<>(menuButton));
|
||||
menuButton.disableProperty().bind(model.getInOverview());
|
||||
|
||||
var filter = new BrowserFilterComp(model.getFilter()).createStructure();
|
||||
var filter = new BrowserFilterComp(model, model.getFilter()).createStructure();
|
||||
Shortcuts.addShortcut(filter.toggleButton(), new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN));
|
||||
|
||||
var topBar = new ToolBar();
|
||||
|
|
|
@ -8,11 +8,10 @@ import javafx.beans.property.SimpleIntegerProperty;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
final class BrowserHistory {
|
||||
final class OpenFileSystemHistory {
|
||||
|
||||
private final IntegerProperty cursor = new SimpleIntegerProperty(0);
|
||||
private final IntegerProperty cursor = new SimpleIntegerProperty(-1);
|
||||
private final List<String> history = new ArrayList<>();
|
||||
private final BooleanBinding canGoBack = Bindings.createBooleanBinding(
|
||||
() -> cursor.get() > 0 && history.size() > 1, cursor);
|
||||
|
@ -24,35 +23,33 @@ final class BrowserHistory {
|
|||
}
|
||||
|
||||
public void updateCurrent(String s) {
|
||||
if (s == null) {
|
||||
return;
|
||||
}
|
||||
var lastString = getCurrent();
|
||||
if (Objects.equals(lastString, s)) {
|
||||
if (cursor.get() != -1 && Objects.equals(lastString, s)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (canGoForth.get()) {
|
||||
history.subList(cursor.get() + 1, history.size()).clear();
|
||||
}
|
||||
|
||||
history.add(s);
|
||||
cursor.set(history.size() - 1);
|
||||
}
|
||||
|
||||
public Optional<String> back() {
|
||||
public String back() {
|
||||
if (!canGoBack.get()) {
|
||||
return Optional.empty();
|
||||
return null;
|
||||
}
|
||||
cursor.set(cursor.get() - 1);
|
||||
return Optional.of(history.get(cursor.get()));
|
||||
return history.get(cursor.get());
|
||||
}
|
||||
|
||||
public Optional<String> forth() {
|
||||
public String forth() {
|
||||
if (!canGoForth.get()) {
|
||||
return Optional.empty();
|
||||
return null;
|
||||
}
|
||||
cursor.set(cursor.get() + 1);
|
||||
return Optional.of(history.get(cursor.get()));
|
||||
return history.get(cursor.get());
|
||||
}
|
||||
|
||||
public BooleanBinding canGoBackProperty() {
|
|
@ -1,7 +1,6 @@
|
|||
package io.xpipe.app.browser;
|
||||
|
||||
import io.xpipe.app.comp.base.ModalOverlayComp;
|
||||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.util.BusyProperty;
|
||||
|
@ -15,6 +14,7 @@ import io.xpipe.core.store.ConnectionFileSystem;
|
|||
import io.xpipe.core.store.FileSystem;
|
||||
import io.xpipe.core.store.FileSystemStore;
|
||||
import io.xpipe.core.store.ShellStore;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
|
@ -26,7 +26,6 @@ import java.time.Instant;
|
|||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Getter
|
||||
|
@ -37,13 +36,13 @@ public final class OpenFileSystemModel {
|
|||
private final Property<String> filter = new SimpleStringProperty();
|
||||
private final BrowserFileListModel fileList;
|
||||
private final ReadOnlyObjectWrapper<String> currentPath = new ReadOnlyObjectWrapper<>();
|
||||
private final BrowserHistory history = new BrowserHistory();
|
||||
private final OpenFileSystemHistory history = new OpenFileSystemHistory();
|
||||
private final BooleanProperty busy = new SimpleBooleanProperty();
|
||||
private final BrowserModel browserModel;
|
||||
private final BooleanProperty noDirectory = new SimpleBooleanProperty();
|
||||
private final Property<OpenFileSystemSavedState> savedState = new SimpleObjectProperty<>();
|
||||
private OpenFileSystemSavedState savedState;
|
||||
private final OpenFileSystemCache cache = new OpenFileSystemCache(this);
|
||||
private final Property<ModalOverlayComp.OverlayContent> overlay = new SimpleObjectProperty<>();
|
||||
private final BooleanProperty inOverview = new SimpleBooleanProperty();
|
||||
private final String name;
|
||||
private boolean local;
|
||||
|
||||
|
@ -51,6 +50,9 @@ public final class OpenFileSystemModel {
|
|||
this.browserModel = browserModel;
|
||||
this.store = store;
|
||||
this.name = name != null ? name : DataStorage.get().getStoreEntry(store).getName();
|
||||
this.inOverview.bind(Bindings.createBooleanBinding(() -> {
|
||||
return currentPath.get() == null;
|
||||
}, currentPath));
|
||||
fileList = new BrowserFileListModel(this);
|
||||
addListeners();
|
||||
}
|
||||
|
@ -73,18 +75,14 @@ public final class OpenFileSystemModel {
|
|||
}
|
||||
|
||||
private void addListeners() {
|
||||
savedState.addListener((observable, oldValue, newValue) -> {
|
||||
if (store == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var storageEntry = DataStorage.get().getStoreEntryIfPresent(store);
|
||||
storageEntry.ifPresent(entry -> AppCache.update("browser-state-" + entry.getUuid(), newValue));
|
||||
});
|
||||
|
||||
currentPath.addListener((observable, oldValue, newValue) -> {
|
||||
savedState.setValue(savedState.getValue().withLastDirectory(newValue));
|
||||
});
|
||||
// savedState.addListener((observable, oldValue, newValue) -> {
|
||||
// if (store == null) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// var storageEntry = DataStorage.get().getStoreEntryIfPresent(store);
|
||||
// storageEntry.ifPresent(entry -> AppCache.update("browser-state-" + entry.getUuid(), newValue));
|
||||
// });
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
|
@ -132,12 +130,15 @@ public final class OpenFileSystemModel {
|
|||
}
|
||||
|
||||
// Handle commands typed into navigation bar
|
||||
if (normalizedPath != null && !FileNames.isAbsolute(normalizedPath) && fileSystem.getShell().isPresent()) {
|
||||
if (normalizedPath != null
|
||||
&& !FileNames.isAbsolute(normalizedPath)
|
||||
&& fileSystem.getShell().isPresent()) {
|
||||
var directory = currentPath.get();
|
||||
var name = normalizedPath + " - "
|
||||
+ XPipeDaemon.getInstance().getStoreName(store).orElse("?");
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
if (ShellDialects.ALL.stream().anyMatch(dialect -> normalizedPath.startsWith(dialect.getOpenCommand()))) {
|
||||
if (ShellDialects.ALL.stream()
|
||||
.anyMatch(dialect -> normalizedPath.startsWith(dialect.getOpenCommand()))) {
|
||||
var cmd = fileSystem
|
||||
.getShell()
|
||||
.get()
|
||||
|
@ -167,7 +168,7 @@ public final class OpenFileSystemModel {
|
|||
dirPath = FileSystemHelper.validateDirectoryPath(this, normalizedPath);
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
return Optional.of(currentPath.get());
|
||||
return Optional.ofNullable(currentPath.get());
|
||||
}
|
||||
|
||||
if (!Objects.equals(path, dirPath)) {
|
||||
|
@ -194,7 +195,7 @@ public final class OpenFileSystemModel {
|
|||
|
||||
filter.setValue(null);
|
||||
currentPath.set(path);
|
||||
savedState.setValue(savedState.getValue().withLastDirectory(path));
|
||||
savedState.cd(path);
|
||||
history.updateCurrent(path);
|
||||
loadFilesSync(path);
|
||||
}
|
||||
|
@ -203,13 +204,11 @@ public final class OpenFileSystemModel {
|
|||
try {
|
||||
if (dir != null) {
|
||||
var stream = getFileSystem().listFiles(dir);
|
||||
noDirectory.set(false);
|
||||
fileList.setAll(stream);
|
||||
} else {
|
||||
var stream = getFileSystem().listRoots().stream()
|
||||
.map(s -> new FileSystem.FileEntry(
|
||||
getFileSystem(), s, Instant.now(), true, false, false, 0, null));
|
||||
noDirectory.set(true);
|
||||
fileList.setAll(stream);
|
||||
}
|
||||
return true;
|
||||
|
@ -302,23 +301,6 @@ public final class OpenFileSystemModel {
|
|||
});
|
||||
}
|
||||
|
||||
public void deleteSelectionAsync() {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
BusyProperty.execute(busy, () -> {
|
||||
if (fileSystem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!BrowserAlerts.showDeleteAlert(fileList.getSelectedRaw())) {
|
||||
return;
|
||||
}
|
||||
|
||||
FileSystemHelper.delete(fileList.getSelectedRaw());
|
||||
refreshSync();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void closeSync() {
|
||||
if (fileSystem == null) {
|
||||
return;
|
||||
|
@ -337,38 +319,25 @@ public final class OpenFileSystemModel {
|
|||
var fs = store.createFileSystem();
|
||||
fs.open();
|
||||
this.fileSystem = fs;
|
||||
this.local = fs.getShell().map(shellControl -> shellControl.isLocal()).orElse(false);
|
||||
this.local =
|
||||
fs.getShell().map(shellControl -> shellControl.isLocal()).orElse(false);
|
||||
this.cache.init();
|
||||
});
|
||||
}
|
||||
|
||||
public void initWithGivenDirectory(String dir) throws Exception {
|
||||
initSavedState(dir);
|
||||
initState();
|
||||
cdSyncWithoutCheck(dir);
|
||||
}
|
||||
|
||||
public void initWithDefaultDirectory() throws Exception {
|
||||
var dir = FileSystemHelper.getStartDirectory(this);
|
||||
initSavedState(dir);
|
||||
cdSyncWithoutCheck(dir);
|
||||
initState();
|
||||
savedState.cd(null);
|
||||
history.updateCurrent(null);
|
||||
}
|
||||
|
||||
private void initSavedState(String path) {
|
||||
var storageEntry = DataStorage.get()
|
||||
.getStoreEntryIfPresent(store)
|
||||
.map(entry -> entry.getUuid())
|
||||
.orElse(UUID.randomUUID());
|
||||
this.savedState.setValue(
|
||||
AppCache.get("browser-state-" + storageEntry, OpenFileSystemSavedState.class, () -> {
|
||||
try {
|
||||
return OpenFileSystemSavedState.builder()
|
||||
.lastDirectory(path)
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).handle();
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
private void initState() {
|
||||
this.savedState = OpenFileSystemSavedState.loadForStore(store);
|
||||
}
|
||||
|
||||
public void openTerminalAsync(String directory) {
|
||||
|
@ -392,19 +361,19 @@ public final class OpenFileSystemModel {
|
|||
});
|
||||
}
|
||||
|
||||
public BrowserHistory getHistory() {
|
||||
public OpenFileSystemHistory getHistory() {
|
||||
return history;
|
||||
}
|
||||
|
||||
public void back() {
|
||||
try (var ignored = new BusyProperty(busy)) {
|
||||
history.back().ifPresent(s -> cd(s));
|
||||
cd(history.back());
|
||||
}
|
||||
}
|
||||
|
||||
public void forth() {
|
||||
try (var ignored = new BusyProperty(busy)) {
|
||||
history.forth().ifPresent(s -> cd(s));
|
||||
cd(history.forth());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,147 @@
|
|||
package io.xpipe.app.browser;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import lombok.With;
|
||||
import com.fasterxml.jackson.core.JacksonException;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JavaType;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.core.store.FileSystemStore;
|
||||
import io.xpipe.core.util.JacksonMapper;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import lombok.*;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
@Value
|
||||
@With
|
||||
@Jacksonized
|
||||
@Builder
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@JsonSerialize(using = OpenFileSystemSavedState.Serializer.class)
|
||||
@JsonDeserialize(using = OpenFileSystemSavedState.Deserializer.class)
|
||||
public class OpenFileSystemSavedState {
|
||||
|
||||
String lastDirectory;
|
||||
public static class Serializer extends StdSerializer<OpenFileSystemSavedState> {
|
||||
|
||||
protected Serializer() {
|
||||
super(OpenFileSystemSavedState.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(OpenFileSystemSavedState value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
var node = JsonNodeFactory.instance.objectNode();
|
||||
node.set("recentDirectories", JacksonMapper.getDefault().valueToTree(value.getRecentDirectories()));
|
||||
gen.writeTree(node);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Deserializer extends StdDeserializer<OpenFileSystemSavedState> {
|
||||
|
||||
protected Deserializer() {
|
||||
super(OpenFileSystemSavedState.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public OpenFileSystemSavedState deserialize(JsonParser p, DeserializationContext ctxt)
|
||||
throws IOException, JacksonException {
|
||||
var tree = (ObjectNode) JacksonMapper.getDefault().readTree(p);
|
||||
JavaType javaType = JacksonMapper.getDefault().getTypeFactory().constructCollectionLikeType(List.class, RecentEntry.class);
|
||||
List<RecentEntry> recentDirectories = JacksonMapper.getDefault().treeToValue(tree.remove("recentDirectories"), javaType);
|
||||
return new OpenFileSystemSavedState(null, FXCollections.observableList(recentDirectories));
|
||||
}
|
||||
}
|
||||
|
||||
static OpenFileSystemSavedState loadForStore(FileSystemStore store) {
|
||||
var storageEntry = DataStorage.get()
|
||||
.getStoreEntryIfPresent(store)
|
||||
.map(entry -> entry.getUuid())
|
||||
.orElse(UUID.randomUUID());
|
||||
var state = AppCache.get("fs-state-" + storageEntry, OpenFileSystemSavedState.class, () -> {
|
||||
return new OpenFileSystemSavedState();
|
||||
});
|
||||
state.store = store;
|
||||
return state;
|
||||
}
|
||||
|
||||
@Value
|
||||
@Jacksonized
|
||||
@Builder
|
||||
public static class RecentEntry {
|
||||
|
||||
String directory;
|
||||
Instant time;
|
||||
}
|
||||
|
||||
private FileSystemStore store;
|
||||
private String lastDirectory;
|
||||
@NonNull
|
||||
private ObservableList<RecentEntry> recentDirectories;
|
||||
|
||||
public OpenFileSystemSavedState(String lastDirectory, @NonNull ObservableList<RecentEntry> recentDirectories) {
|
||||
this.lastDirectory = lastDirectory;
|
||||
this.recentDirectories = recentDirectories;
|
||||
}
|
||||
|
||||
private static final Timer TIMEOUT_TIMER = new Timer(true);
|
||||
private static final int STORED = 10;
|
||||
|
||||
public OpenFileSystemSavedState() {
|
||||
lastDirectory = null;
|
||||
recentDirectories = FXCollections.observableList(new ArrayList<>(STORED));
|
||||
}
|
||||
|
||||
public void save() {
|
||||
if (store == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var storageEntry = DataStorage.get().getStoreEntryIfPresent(store);
|
||||
storageEntry.ifPresent(entry -> AppCache.update("fs-state-" + entry.getUuid(), this));
|
||||
}
|
||||
|
||||
public void cd(String dir) {
|
||||
if (dir == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastDirectory = dir;
|
||||
TIMEOUT_TIMER.schedule(
|
||||
new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Synchronize with platform thread
|
||||
Platform.runLater(() -> {
|
||||
if (Objects.equals(lastDirectory, dir)) {
|
||||
updateRecent(dir);
|
||||
save();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
20000);
|
||||
}
|
||||
|
||||
private void updateRecent(String dir) {
|
||||
recentDirectories.removeIf(recentEntry -> Objects.equals(recentEntry.directory, dir));
|
||||
|
||||
var o = new RecentEntry(dir, Instant.now());
|
||||
if (recentDirectories.size() < STORED) {
|
||||
recentDirectories.add(0, o);
|
||||
} else {
|
||||
recentDirectories.remove(recentDirectories.size() - 1);
|
||||
recentDirectories.add(o);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,10 @@ public interface BrowserAction {
|
|||
.toList();
|
||||
}
|
||||
|
||||
static LeafAction byId(String id) {
|
||||
return getFlattened().stream().filter(browserAction -> id.equals(browserAction.getId())).findAny().orElseThrow();
|
||||
}
|
||||
|
||||
default Node getIcon(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -2,8 +2,12 @@ package io.xpipe.app.browser.action;
|
|||
|
||||
import io.xpipe.app.browser.BrowserEntry;
|
||||
import io.xpipe.app.browser.OpenFileSystemModel;
|
||||
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
|
||||
import io.xpipe.app.fxcomps.util.Shortcuts;
|
||||
import io.xpipe.app.util.BusyProperty;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.MenuItem;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -13,6 +17,29 @@ public interface LeafAction extends BrowserAction {
|
|||
|
||||
public abstract void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception;
|
||||
|
||||
default Button toButton(OpenFileSystemModel model, List<BrowserEntry> selected) {
|
||||
var b = new Button();
|
||||
b.setOnAction(event -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
BusyProperty.execute(model.getBusy(), () -> {
|
||||
execute(model, selected);
|
||||
});
|
||||
});
|
||||
event.consume();
|
||||
});
|
||||
if (getShortcut() != null) {
|
||||
Shortcuts.addShortcut(b, getShortcut());
|
||||
}
|
||||
new FancyTooltipAugment<>(new SimpleStringProperty(getName(model, selected))).augment(b);
|
||||
var graphic = getIcon(model, selected);
|
||||
if (graphic != null) {
|
||||
b.setGraphic(graphic);
|
||||
}
|
||||
b.setMnemonicParsing(false);
|
||||
b.setDisable(!isActive(model, selected));
|
||||
return b;
|
||||
}
|
||||
|
||||
default MenuItem toItem(OpenFileSystemModel model, List<BrowserEntry> selected, UnaryOperator<String> nameFunc) {
|
||||
var mi = new MenuItem(nameFunc.apply(getName(model, selected)));
|
||||
mi.setOnAction(event -> {
|
||||
|
@ -35,4 +62,7 @@ public interface LeafAction extends BrowserAction {
|
|||
return mi;
|
||||
}
|
||||
|
||||
default String getId() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,8 +22,23 @@ public interface DirectoryType {
|
|||
}
|
||||
|
||||
public static void loadDefinitions() {
|
||||
ALL.add(new Simple(
|
||||
"default", new IconVariant("default_root_folder.svg"), new IconVariant("default_root_folder_opened.svg"), ""));
|
||||
ALL.add(new DirectoryType() {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "root";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(FileSystem.FileEntry entry) {
|
||||
return entry.getPath().equals("/") || entry.getPath().matches("\\w:\\\\");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIcon(FileSystem.FileEntry entry, boolean open) {
|
||||
return open ? "default_root_folder_opened.svg" : "default_root_folder.svg";
|
||||
}
|
||||
});
|
||||
|
||||
AppResources.with(AppResources.XPIPE_MODULE, "folder_list.txt", path -> {
|
||||
try (var reader =
|
||||
|
|
|
@ -13,6 +13,7 @@ import javafx.scene.input.KeyCombination;
|
|||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -54,6 +55,10 @@ public abstract class Comp<S extends CompStructure<?>> {
|
|||
return apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS));
|
||||
}
|
||||
|
||||
public Comp<S> vgrow() {
|
||||
return apply(struc -> VBox.setVgrow(struc.get(), Priority.ALWAYS));
|
||||
}
|
||||
|
||||
public Comp<S> visible(ObservableValue<Boolean> o) {
|
||||
return apply(struc -> struc.get().visibleProperty().bind(o));
|
||||
}
|
||||
|
|
|
@ -39,8 +39,7 @@ public class FancyTooltipAugment<S extends CompStructure<?>> implements Augment<
|
|||
var tt = new JFXTooltip();
|
||||
var toDisplay = text.getValue();
|
||||
if (Shortcuts.getShortcut((Region) region) != null) {
|
||||
toDisplay =
|
||||
toDisplay + " (" + Shortcuts.getShortcut((Region) region).getDisplayText() + ")";
|
||||
toDisplay = toDisplay + " (" + Shortcuts.getShortcut((Region) region).getDisplayText() + ")";
|
||||
}
|
||||
tt.textProperty().setValue(toDisplay);
|
||||
tt.setStyle("-fx-font-size: 11pt;");
|
||||
|
|
|
@ -32,6 +32,7 @@ public class Shortcuts {
|
|||
};
|
||||
|
||||
AtomicReference<Scene> scene = new AtomicReference<>();
|
||||
SHORTCUTS.put(region, comb);
|
||||
SimpleChangeListener.apply(region.sceneProperty(), s -> {
|
||||
if (Objects.equals(s, scene.get())) {
|
||||
return;
|
||||
|
@ -45,7 +46,6 @@ public class Shortcuts {
|
|||
|
||||
if (s != null) {
|
||||
scene.set(s);
|
||||
s.addEventHandler(KeyEvent.KEY_PRESSED, filter);
|
||||
SHORTCUTS.put(region, comb);
|
||||
}
|
||||
});
|
||||
|
|
BIN
app/src/main/resources/io/xpipe/app/resources/img/home_icon.png
Normal file
BIN
app/src/main/resources/io/xpipe/app/resources/img/home_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
|
@ -30,6 +30,8 @@ mustNotBeEmpty=$NAME$ must not be empty
|
|||
download=Drop to transfer
|
||||
dragFiles=Drag files from here
|
||||
null=$VALUE$ must be not null
|
||||
roots=Roots
|
||||
recent=Recent
|
||||
hostFeatureUnsupported=$FEATURE$ is not available on the host
|
||||
missingStore=$NAME$ does not exist
|
||||
connectionName=Connection name
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
-fx-padding: 1em;
|
||||
}
|
||||
|
||||
.browser .home {
|
||||
-fx-spacing: 1em;
|
||||
-fx-padding: 1em;
|
||||
.browser .overview {
|
||||
-fx-spacing: 1.5em;
|
||||
-fx-padding: 1.5em;
|
||||
}
|
||||
|
||||
.selected-file-list {
|
||||
|
@ -106,9 +106,18 @@
|
|||
-fx-text-fill: transparent;
|
||||
}
|
||||
.browser .path-graphic-button {
|
||||
-fx-padding: 0 2px 0 7px;
|
||||
-fx-padding: 0 5px 0 5px;
|
||||
}
|
||||
|
||||
.browser .overview-file-list {
|
||||
-fx-border-width: 1px;
|
||||
}
|
||||
|
||||
.browser .overview-file-list .button {
|
||||
-fx-border-width: 0;
|
||||
-fx-background-radius: 0;
|
||||
-fx-background-insets: 0;
|
||||
}
|
||||
|
||||
.browser .context-menu .accelerator-text {
|
||||
-fx-padding: 3px 0px 3px 50px;
|
||||
|
|
|
@ -53,7 +53,7 @@ public sealed interface OsType permits OsType.Windows, OsType.Linux, OsType.MacO
|
|||
@Override
|
||||
public List<String> determineInterestingPaths(ShellControl pc) throws Exception {
|
||||
var home = getHomeDirectory(pc);
|
||||
return List.of(FileNames.join(home, "Desktop"));
|
||||
return List.of(home, FileNames.join(home, "Documents"), FileNames.join(home, "Downloads"), FileNames.join(home, "Desktop"));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -42,6 +42,11 @@ public interface FileSystem extends Closeable, AutoCloseable {
|
|||
this.executable = executable;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public static FileEntry ofDirectory(FileSystem fileSystem, String path) {
|
||||
return new FileEntry(fileSystem, path, Instant.now(), true, false, false, 0, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
FileSystemStore getStore();
|
||||
|
|
|
@ -7,6 +7,9 @@ import io.xpipe.app.browser.action.BrowserAction;
|
|||
import io.xpipe.app.browser.action.BrowserActionFormatter;
|
||||
import io.xpipe.app.browser.action.LeafAction;
|
||||
import io.xpipe.core.impl.FileNames;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.Clipboard;
|
||||
|
@ -44,6 +47,11 @@ public class CopyPathAction implements BrowserAction, BranchAction {
|
|||
return "Absolute Path";
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyCombination getShortcut() {
|
||||
return new KeyCodeCombination(KeyCode.C, KeyCombination.ALT_DOWN, KeyCombination.SHORTCUT_DOWN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception {
|
||||
var s = entries.stream()
|
||||
|
@ -89,6 +97,11 @@ public class CopyPathAction implements BrowserAction, BranchAction {
|
|||
return "File Name";
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyCombination getShortcut() {
|
||||
return new KeyCodeCombination(KeyCode.C, KeyCombination.SHIFT_DOWN, KeyCombination.SHORTCUT_DOWN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception {
|
||||
var s = entries.stream()
|
||||
|
|
|
@ -13,6 +13,10 @@ import java.util.List;
|
|||
|
||||
public class OpenTerminalAction implements LeafAction {
|
||||
|
||||
public String getId() {
|
||||
return "openTerminal";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception {
|
||||
if (entries.size() == 0) {
|
||||
|
|
Loading…
Reference in a new issue