From 52cdcfa0aa81e37d8129f5d60d490f0f865b9032 Mon Sep 17 00:00:00 2001 From: crschnick Date: Mon, 29 May 2023 00:33:34 +0000 Subject: [PATCH] File browser overview page improvements --- .../app/browser/BrowserFileOverviewComp.java | 41 +++++ .../xpipe/app/browser/BrowserFilterComp.java | 9 +- .../io/xpipe/app/browser/BrowserNavBar.java | 20 ++- .../app/browser/BrowserOverviewComp.java | 25 ++- .../xpipe/app/browser/OpenFileSystemComp.java | 11 +- ...istory.java => OpenFileSystemHistory.java} | 23 ++- .../app/browser/OpenFileSystemModel.java | 97 ++++-------- .../app/browser/OpenFileSystemSavedState.java | 148 +++++++++++++++++- .../app/browser/action/BrowserAction.java | 4 + .../xpipe/app/browser/action/LeafAction.java | 30 ++++ .../xpipe/app/browser/icon/DirectoryType.java | 19 ++- .../main/java/io/xpipe/app/fxcomps/Comp.java | 5 + .../app/fxcomps/impl/FancyTooltipAugment.java | 3 +- .../io/xpipe/app/fxcomps/util/Shortcuts.java | 2 +- .../io/xpipe/app/resources/img/home_icon.png | Bin 0 -> 22980 bytes .../resources/lang/translations_en.properties | 2 + .../io/xpipe/app/resources/style/browser.css | 17 +- .../java/io/xpipe/core/process/OsType.java | 2 +- .../java/io/xpipe/core/store/FileSystem.java | 5 + .../ext/base/browser/CopyPathAction.java | 13 ++ .../ext/base/browser/OpenTerminalAction.java | 4 + 21 files changed, 367 insertions(+), 113 deletions(-) create mode 100644 app/src/main/java/io/xpipe/app/browser/BrowserFileOverviewComp.java rename app/src/main/java/io/xpipe/app/browser/{BrowserHistory.java => OpenFileSystemHistory.java} (77%) create mode 100644 app/src/main/resources/io/xpipe/app/resources/img/home_icon.png diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileOverviewComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileOverviewComp.java new file mode 100644 index 00000000..16f6f817 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileOverviewComp.java @@ -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 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(); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFilterComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserFilterComp.java index 37936640..e8915cc5 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFilterComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFilterComp.java @@ -48,6 +48,10 @@ public class BrowserFilterComp extends Comp { 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 { 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 { } } + private final OpenFileSystemModel model; private final Property filterString; - public BrowserFilterComp(Property filterString) { + public BrowserFilterComp(OpenFileSystemModel model, Property filterString) { + this.model = model; this.filterString = filterString; } } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java b/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java index 58e13b3b..8cac3bb6 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java @@ -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); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserOverviewComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserOverviewComp.java index ef9ceaea..1e6392d8 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserOverviewComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserOverviewComp.java @@ -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(); } } diff --git a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java index 428fe52e..21a037d1 100644 --- a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java +++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java @@ -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(); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserHistory.java b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemHistory.java similarity index 77% rename from app/src/main/java/io/xpipe/app/browser/BrowserHistory.java rename to app/src/main/java/io/xpipe/app/browser/OpenFileSystemHistory.java index ac507aa1..8cdd0368 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserHistory.java +++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemHistory.java @@ -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 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 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 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() { diff --git a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java index c91138a1..446948b2 100644 --- a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java +++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java @@ -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 filter = new SimpleStringProperty(); private final BrowserFileListModel fileList; private final ReadOnlyObjectWrapper 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 savedState = new SimpleObjectProperty<>(); + private OpenFileSystemSavedState savedState; private final OpenFileSystemCache cache = new OpenFileSystemCache(this); private final Property 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()); } } } diff --git a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemSavedState.java b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemSavedState.java index ef1eda35..8a85e335 100644 --- a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemSavedState.java +++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemSavedState.java @@ -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 { + + 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 { + + 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 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 recentDirectories; + + public OpenFileSystemSavedState(String lastDirectory, @NonNull ObservableList 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); + } + } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java b/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java index fa57213a..27f6cc31 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java +++ b/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java @@ -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 entries) { return null; } diff --git a/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java b/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java index 4712996c..d9a7247d 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java +++ b/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java @@ -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 entries) throws Exception; + default Button toButton(OpenFileSystemModel model, List 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 selected, UnaryOperator 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; + } } diff --git a/app/src/main/java/io/xpipe/app/browser/icon/DirectoryType.java b/app/src/main/java/io/xpipe/app/browser/icon/DirectoryType.java index a0d8fcd1..fa389c10 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/DirectoryType.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/DirectoryType.java @@ -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 = diff --git a/app/src/main/java/io/xpipe/app/fxcomps/Comp.java b/app/src/main/java/io/xpipe/app/fxcomps/Comp.java index 5c8ee3fe..d84303b1 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/Comp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/Comp.java @@ -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> { return apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS)); } + public Comp vgrow() { + return apply(struc -> VBox.setVgrow(struc.get(), Priority.ALWAYS)); + } + public Comp visible(ObservableValue o) { return apply(struc -> struc.get().visibleProperty().bind(o)); } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/FancyTooltipAugment.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/FancyTooltipAugment.java index c13b7879..74f6db57 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/FancyTooltipAugment.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/FancyTooltipAugment.java @@ -39,8 +39,7 @@ public class FancyTooltipAugment> 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;"); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/util/Shortcuts.java b/app/src/main/java/io/xpipe/app/fxcomps/util/Shortcuts.java index d6a952a3..9f493350 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/util/Shortcuts.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/util/Shortcuts.java @@ -32,6 +32,7 @@ public class Shortcuts { }; AtomicReference 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); } }); diff --git a/app/src/main/resources/io/xpipe/app/resources/img/home_icon.png b/app/src/main/resources/io/xpipe/app/resources/img/home_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c8f5fe9c849093029f429930018f3197d068b048 GIT binary patch literal 22980 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 E04W4?*8l(j literal 0 HcmV?d00001 diff --git a/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties b/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties index 5561da14..115b34cc 100644 --- a/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties +++ b/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties @@ -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 diff --git a/app/src/main/resources/io/xpipe/app/resources/style/browser.css b/app/src/main/resources/io/xpipe/app/resources/style/browser.css index 3f755c6b..9add5c24 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/browser.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/browser.css @@ -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; diff --git a/core/src/main/java/io/xpipe/core/process/OsType.java b/core/src/main/java/io/xpipe/core/process/OsType.java index 0a8d6a01..d88a80fa 100644 --- a/core/src/main/java/io/xpipe/core/process/OsType.java +++ b/core/src/main/java/io/xpipe/core/process/OsType.java @@ -53,7 +53,7 @@ public sealed interface OsType permits OsType.Windows, OsType.Linux, OsType.MacO @Override public List 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 diff --git a/core/src/main/java/io/xpipe/core/store/FileSystem.java b/core/src/main/java/io/xpipe/core/store/FileSystem.java index 7d476026..c3487a3b 100644 --- a/core/src/main/java/io/xpipe/core/store/FileSystem.java +++ b/core/src/main/java/io/xpipe/core/store/FileSystem.java @@ -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(); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyPathAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyPathAction.java index 17543eb4..585d09f6 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyPathAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyPathAction.java @@ -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 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 entries) throws Exception { var s = entries.stream() diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java index 4341a43c..ac124521 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java @@ -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 entries) throws Exception { if (entries.size() == 0) {