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 000000000..16f6f8174 --- /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 37936640e..e8915cc51 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 58e13b3b4..8cac3bb66 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 ef9ceaead..1e6392d8d 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 428fe52e0..21a037d1b 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 ac507aa1e..8cdd03686 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 c91138a13..446948b23 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 ef1eda350..8a85e335a 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 fa57213a2..27f6cc314 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 4712996c8..d9a7247d6 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 a0d8fcd19..fa389c10b 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 5c8ee3fe3..d84303b1f 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 c13b78794..74f6db578 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 d6a952a3a..9f493350a 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 000000000..c8f5fe9c8 Binary files /dev/null and b/app/src/main/resources/io/xpipe/app/resources/img/home_icon.png differ 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 5561da144..115b34cc7 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 3f755c6bf..9add5c24f 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 0a8d6a01c..d88a80fa4 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 7d4760267..c3487a3bf 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 17543eb47..585d09f63 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 4341a43cb..ac1245213 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) {