From 9d4c4fe97d504c893fca1bbac45b21d23126346e Mon Sep 17 00:00:00 2001 From: crschnick Date: Tue, 7 May 2024 07:31:09 +0000 Subject: [PATCH] Merge branch browser-model into master --- app/build.gradle | 1 - .../xpipe/app/browser/BrowserClipboard.java | 27 +- .../app/browser/BrowserSelectionListComp.java | 18 +- .../app/browser/BrowserStatusBarComp.java | 15 +- .../app/browser/BrowserTransferComp.java | 23 +- .../app/browser/BrowserTransferModel.java | 36 +-- .../app/browser/BrowserTransferProgress.java | 25 +- .../app/browser/file/BrowserContextMenu.java | 5 +- .../xpipe/app/browser/file/BrowserEntry.java | 20 +- .../app/browser/file/BrowserFileListComp.java | 92 ++---- .../file/BrowserFileListCompEntry.java | 28 +- .../browser/file/BrowserFileListModel.java | 10 +- .../browser/file/BrowserFileOverviewComp.java | 2 +- .../browser/file/BrowserFileTransferMode.java | 8 + .../file/BrowserFileTransferOperation.java | 300 +++++++++++++++++ .../file/BrowserQuickAccessContextMenu.java | 2 +- .../app/browser/file/FileSystemHelper.java | 303 +----------------- .../app/browser/file/LocalFileSystem.java | 42 +++ .../app/browser/fs/OpenFileSystemModel.java | 15 +- .../xpipe/app/comp/store/StoreEntryComp.java | 2 +- .../main/java/io/xpipe/app/core/AppI18n.java | 16 +- .../java/io/xpipe/app/core/mode/GuiMode.java | 7 +- .../io/xpipe/app/storage/DataStorage.java | 3 - .../xpipe/app/util/HumanReadableFormat.java | 10 + app/src/main/java/module-info.java | 1 - build.gradle | 1 - .../io/xpipe/ext/base/browser/CopyAction.java | 2 +- .../xpipe/ext/base/browser/PasteAction.java | 3 +- 28 files changed, 520 insertions(+), 497 deletions(-) create mode 100644 app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferMode.java create mode 100644 app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java create mode 100644 app/src/main/java/io/xpipe/app/browser/file/LocalFileSystem.java diff --git a/app/build.gradle b/app/build.gradle index 3d07a6484..d4ce1bbba 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -43,7 +43,6 @@ dependencies { api 'info.picocli:picocli:4.7.5' api 'org.kohsuke:github-api:1.321' api 'io.sentry:sentry:7.8.0' - api 'org.ocpsoft.prettytime:prettytime:5.0.7.Final' api 'commons-io:commons-io:2.16.1' api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.0" api group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: "2.17.0" diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserClipboard.java b/app/src/main/java/io/xpipe/app/browser/BrowserClipboard.java index fc4c0f786..ba9761b48 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserClipboard.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserClipboard.java @@ -1,17 +1,17 @@ package io.xpipe.app.browser; -import io.xpipe.app.browser.file.FileSystemHelper; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.file.BrowserFileTransferMode; +import io.xpipe.app.browser.file.LocalFileSystem; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.process.ProcessControlProvider; import io.xpipe.core.store.FileSystem; import io.xpipe.core.util.FailableRunnable; - import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; import javafx.scene.input.ClipboardContent; import javafx.scene.input.Dragboard; - import lombok.SneakyThrows; import lombok.Value; @@ -45,17 +45,17 @@ public class BrowserClipboard { List data = (List) clipboard.getData(DataFlavor.javaFileListFlavor); var files = - data.stream().map(string -> string.toPath()).toList(); + data.stream().map(f -> f.toPath()).toList(); if (files.size() == 0) { return; } - var entries = new ArrayList(); + var entries = new ArrayList(); for (Path file : files) { - entries.add(FileSystemHelper.getLocal(file)); + entries.add(LocalFileSystem.getLocalBrowserEntry(file)); } - currentCopyClipboard.setValue(new Instance(UUID.randomUUID(), null, entries)); + currentCopyClipboard.setValue(new Instance(UUID.randomUUID(), null, entries, BrowserFileTransferMode.COPY)); } catch (Exception e) { ErrorEvent.fromThrowable(e).expected().omit().handle(); } @@ -64,27 +64,27 @@ public class BrowserClipboard { } @SneakyThrows - public static ClipboardContent startDrag(FileSystem.FileEntry base, List selected) { + public static ClipboardContent startDrag(FileSystem.FileEntry base, List selected, BrowserFileTransferMode mode) { if (selected.isEmpty()) { return null; } var content = new ClipboardContent(); var id = UUID.randomUUID(); - currentDragClipboard = new Instance(id, base, new ArrayList<>(selected)); + currentDragClipboard = new Instance(id, base, new ArrayList<>(selected), mode); content.putString(currentDragClipboard.toClipboardString()); return content; } @SneakyThrows - public static void startCopy(FileSystem.FileEntry base, List selected) { + public static void startCopy(FileSystem.FileEntry base, List selected) { if (selected.isEmpty()) { currentCopyClipboard.setValue(null); return; } var id = UUID.randomUUID(); - currentCopyClipboard.setValue(new Instance(id, base, new ArrayList<>(selected))); + currentCopyClipboard.setValue(new Instance(id, base, new ArrayList<>(selected), BrowserFileTransferMode.COPY)); } public static Instance retrieveCopy() { @@ -118,11 +118,12 @@ public class BrowserClipboard { public static class Instance { UUID uuid; FileSystem.FileEntry baseDirectory; - List entries; + List entries; + BrowserFileTransferMode mode; public String toClipboardString() { return entries.stream() - .map(fileEntry -> "\"" + fileEntry.getPath() + "\"") + .map(fileEntry -> "\"" + fileEntry.getRawFileEntry().getPath() + "\"") .collect(Collectors.joining(ProcessControlProvider.get() .getEffectiveLocalDialect() .getNewLine() diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java index e17120371..02edb3d4e 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java @@ -1,6 +1,6 @@ package io.xpipe.app.browser; -import io.xpipe.app.browser.icon.FileIconManager; +import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.comp.base.ListBoxViewComp; import io.xpipe.app.core.AppStyle; import io.xpipe.app.core.AppWindowHelper; @@ -8,9 +8,6 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.core.store.FileNames; -import io.xpipe.core.store.FileSystem; - import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; @@ -21,7 +18,6 @@ import javafx.scene.control.OverrunStyle; import javafx.scene.image.Image; import javafx.scene.layout.Region; import javafx.scene.paint.Color; - import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Value; @@ -33,14 +29,14 @@ import java.util.function.Function; @AllArgsConstructor public class BrowserSelectionListComp extends SimpleComp { - ObservableList list; - Function> nameTransformation; + ObservableList list; + Function> nameTransformation; - public BrowserSelectionListComp(ObservableList list) { - this(list, entry -> new SimpleStringProperty(FileNames.getFileName(entry.getPath()))); + public BrowserSelectionListComp(ObservableList list) { + this(list, entry -> new SimpleStringProperty(entry.getFileName())); } - public static Image snapshot(ObservableList list) { + public static Image snapshot(ObservableList list) { var r = new BrowserSelectionListComp(list).styleClass("drag").createRegion(); var scene = new Scene(r); AppWindowHelper.setupStylesheets(scene); @@ -54,7 +50,7 @@ public class BrowserSelectionListComp extends SimpleComp { protected Region createSimple() { var c = new ListBoxViewComp<>(list, list, entry -> { return Comp.of(() -> { - var image = PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(entry, false), 24) + var image = PrettyImageHelper.ofFixedSizeSquare(entry.getIcon(), 24) .createRegion(); var l = new Label(null, image); l.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java index 249ea23e7..97be0db3f 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java @@ -1,5 +1,6 @@ package io.xpipe.app.browser; +import atlantafx.base.controls.Spacer; import io.xpipe.app.browser.file.BrowserContextMenu; import io.xpipe.app.browser.file.BrowserFileListCompEntry; import io.xpipe.app.browser.fs.OpenFileSystemModel; @@ -11,16 +12,16 @@ import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.impl.LabelComp; import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.util.HumanReadableFormat; - import javafx.beans.binding.Bindings; import javafx.scene.control.ToolBar; import javafx.scene.input.MouseButton; import javafx.scene.layout.Region; - -import atlantafx.base.controls.Spacer; import lombok.EqualsAndHashCode; import lombok.Value; +import java.time.Duration; +import java.time.temporal.ChronoUnit; + @Value @EqualsAndHashCode(callSuper = true) public class BrowserStatusBarComp extends SimpleComp { @@ -56,7 +57,9 @@ public class BrowserStatusBarComp extends SimpleComp { var transferred = HumanReadableFormat.progressByteCount(p.getTransferred()); var all = HumanReadableFormat.byteCount(p.getTotal()); var name = (p.getName() != null ? " @ " + p.getName() + " " : ""); - return transferred + " / " + all + name; + var time = p.getTotal() > 50_000_000 && p.elapsedTime().compareTo(Duration.of(200, ChronoUnit.MILLIS)) > 0 ? " | " + + HumanReadableFormat.duration(p.expectedTimeRemaining()) : " | ..."; + return transferred + " / " + all + name + time; } }); var progressComp = new LabelComp(text).styleClass("progress"); @@ -87,9 +90,7 @@ public class BrowserStatusBarComp extends SimpleComp { var allCount = Bindings.createIntegerBinding( () -> { - return (int) model.getFileList().getAll().getValue().stream() - .filter(entry -> !entry.isSynthetic()) - .count(); + return model.getFileList().getAll().getValue().size(); }, model.getFileList().getAll()); var selectedComp = new LabelComp(Bindings.createStringBinding( diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java index 523204445..2d5112720 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java @@ -1,5 +1,6 @@ package io.xpipe.app.browser; +import io.xpipe.app.browser.file.BrowserFileTransferMode; import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.comp.base.LoadingOverlayComp; import io.xpipe.app.core.AppI18n; @@ -9,10 +10,7 @@ import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment; import io.xpipe.app.fxcomps.impl.*; import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.storage.DataStorage; import io.xpipe.core.process.OsType; -import io.xpipe.core.store.FileNames; - import javafx.beans.binding.Bindings; import javafx.collections.FXCollections; import javafx.geometry.Insets; @@ -21,7 +19,6 @@ import javafx.scene.input.Dragboard; import javafx.scene.input.TransferMode; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.Region; - import org.kordamp.ikonli.javafx.FontIcon; import java.io.File; @@ -50,25 +47,21 @@ public class BrowserTransferComp extends SimpleComp { var backgroundStack = new StackComp(List.of(background)).grow(true, true).styleClass("download-background"); - var binding = ListBindingsHelper.mappedContentBinding(syncItems, item -> item.getFileEntry()); + var binding = ListBindingsHelper.mappedContentBinding(syncItems, item -> item.getBrowserEntry()); var list = new BrowserSelectionListComp( binding, entry -> Bindings.createStringBinding( () -> { var sourceItem = syncItems.stream() - .filter(item -> item.getFileEntry() == entry) + .filter(item -> item.getBrowserEntry() == entry) .findAny(); if (sourceItem.isEmpty()) { return "?"; } - var name = - sourceItem.get().downloadFinished().get() + var name = entry.getModel() == null || sourceItem.get().downloadFinished().get() ? "Local" - : DataStorage.get() - .getStoreDisplayName(entry.getFileSystem() - .getStore()) - .orElse("?"); - return FileNames.getFileName(entry.getPath()) + " (" + name + ")"; + : entry.getModel().getFileSystemModel().getName(); + return entry.getFileName() + " (" + name + ")"; }, syncAllDownloaded)) .apply(struc -> struc.get().setMinHeight(150)) @@ -154,11 +147,11 @@ public class BrowserTransferComp extends SimpleComp { } var selected = syncItems.stream() - .map(BrowserTransferModel.Item::getFileEntry) + .map(item -> item.getBrowserEntry()) .toList(); Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY); - var cc = BrowserClipboard.startDrag(null, selected); + var cc = BrowserClipboard.startDrag(null, selected, BrowserFileTransferMode.NORMAL); if (cc == null) { return; } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java index 41c2146a8..a7b174170 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java @@ -1,14 +1,14 @@ package io.xpipe.app.browser; -import io.xpipe.app.browser.file.FileSystemHelper; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.file.BrowserFileTransferMode; +import io.xpipe.app.browser.file.BrowserFileTransferOperation; +import io.xpipe.app.browser.file.LocalFileSystem; import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.browser.session.BrowserSessionModel; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.ShellTemp; -import io.xpipe.core.store.FileNames; -import io.xpipe.core.store.FileSystem; - import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.Property; @@ -17,7 +17,6 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableBooleanValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; - import lombok.Value; import org.apache.commons.io.FileUtils; @@ -66,9 +65,9 @@ public class BrowserTransferModel { items.clear(); } - public void drop(OpenFileSystemModel model, List entries) { + public void drop(OpenFileSystemModel model, List entries) { entries.forEach(entry -> { - var name = FileNames.getFileName(entry.getPath()); + var name = entry.getFileName(); if (items.stream().anyMatch(item -> item.getName().equals(name))) { return; } @@ -89,14 +88,14 @@ public class BrowserTransferModel { try { var paths = entries.stream().map(File::toPath).filter(Files::exists).toList(); for (Path path : paths) { - var entry = FileSystemHelper.getLocal(path); - var name = entry.getName(); + var entry = LocalFileSystem.getLocalBrowserEntry(path); + var name = entry.getFileName(); if (items.stream().anyMatch(item -> item.getName().equals(name))) { return; } var item = new Item(null, name, entry, path); - item.progress.setValue(BrowserTransferProgress.finished(entry.getName(), entry.getSize())); + item.progress.setValue(BrowserTransferProgress.finished(entry.getFileName(), entry.getRawFileEntry().getSize())); items.add(item); } } catch (Exception ex) { @@ -127,16 +126,17 @@ public class BrowserTransferModel { } try { - try (var b = new BooleanScope(downloading).start()) { - FileSystemHelper.dropFilesInto( - FileSystemHelper.getLocal(TEMP), - List.of(item.getFileEntry()), - true, + try (var ignored = new BooleanScope(downloading).start()) { + var op = new BrowserFileTransferOperation( + LocalFileSystem.getLocalFileEntry(TEMP), + List.of(item.getBrowserEntry().getRawFileEntry()), + BrowserFileTransferMode.COPY, false, progress -> { item.getProgress().setValue(progress); item.getOpenFileSystemModel().getProgress().setValue(progress); }); + op.execute(); } } catch (Throwable t) { ErrorEvent.fromThrowable(t).handle(); @@ -151,15 +151,15 @@ public class BrowserTransferModel { public static class Item { OpenFileSystemModel openFileSystemModel; String name; - FileSystem.FileEntry fileEntry; + BrowserEntry browserEntry; Path localFile; Property progress; public Item( - OpenFileSystemModel openFileSystemModel, String name, FileSystem.FileEntry fileEntry, Path localFile) { + OpenFileSystemModel openFileSystemModel, String name, BrowserEntry browserEntry, Path localFile) { this.openFileSystemModel = openFileSystemModel; this.name = name; - this.fileEntry = fileEntry; + this.browserEntry = browserEntry; this.localFile = localFile; this.progress = new SimpleObjectProperty<>(); } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserTransferProgress.java b/app/src/main/java/io/xpipe/app/browser/BrowserTransferProgress.java index 738606419..36d7e2a87 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserTransferProgress.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferProgress.java @@ -2,26 +2,45 @@ package io.xpipe.app.browser; import lombok.Value; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + @Value public class BrowserTransferProgress { String name; long transferred; long total; + Instant start; public static BrowserTransferProgress empty() { - return new BrowserTransferProgress(null, 0, 0); + return new BrowserTransferProgress(null, 0, 0, Instant.now()); } static BrowserTransferProgress empty(String name, long size) { - return new BrowserTransferProgress(name, 0, size); + return new BrowserTransferProgress(name, 0, size, Instant.now()); } public static BrowserTransferProgress finished(String name, long size) { - return new BrowserTransferProgress(name, size, size); + return new BrowserTransferProgress(name, size, size, Instant.now()); } public boolean done() { return transferred >= total; } + + public Duration elapsedTime() { + var now = Instant.now(); + var elapsed = Duration.between(start,now); + return elapsed; + } + + public Duration expectedTimeRemaining() { + var elapsed = elapsedTime(); + var share = (double) transferred / total; + var rest = (1.0 - share) / share; + var restMillis = (long) (elapsed.toMillis() * rest); + return Duration.of(restMillis, ChronoUnit.MILLIS); + } } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserContextMenu.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserContextMenu.java index 538f11044..08e3e5157 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserContextMenu.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserContextMenu.java @@ -32,8 +32,7 @@ public final class BrowserContextMenu extends ContextMenu { ? selected.stream() .map(browserEntry -> new BrowserEntry( browserEntry.getRawFileEntry().resolved(), - browserEntry.getModel(), - browserEntry.isSynthetic())) + browserEntry.getModel())) .toList() : selected; } @@ -44,7 +43,7 @@ public final class BrowserContextMenu extends ContextMenu { var empty = source == null; var selected = new ArrayList<>( empty - ? List.of(new BrowserEntry(model.getCurrentDirectory(), model.getFileList(), false)) + ? List.of(new BrowserEntry(model.getCurrentDirectory(), model.getFileList())) : model.getFileList().getSelection()); if (source != null && !selected.contains(source)) { selected.add(source); diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserEntry.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserEntry.java index 14f523ce0..de226a240 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserEntry.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserEntry.java @@ -13,14 +13,12 @@ public class BrowserEntry { private final BrowserFileListModel model; private final FileSystem.FileEntry rawFileEntry; - private final boolean synthetic; private final BrowserIconFileType fileType; private final BrowserIconDirectoryType directoryType; - public BrowserEntry(FileSystem.FileEntry rawFileEntry, BrowserFileListModel model, boolean synthetic) { + public BrowserEntry(FileSystem.FileEntry rawFileEntry, BrowserFileListModel model) { this.rawFileEntry = rawFileEntry; this.model = model; - this.synthetic = synthetic; this.fileType = fileType(rawFileEntry); this.directoryType = directoryType(rawFileEntry); } @@ -52,6 +50,17 @@ public class BrowserEntry { return null; } + public String getIcon() { + if (fileType != null) { + return fileType.getIcon(); + } else if (directoryType != null) { + return directoryType.getIcon(rawFileEntry, false); + } else { + return rawFileEntry.getKind() == FileKind.DIRECTORY + ? "default_folder.svg" + : "default_file.svg"; + } + } public String getFileName() { return getRawFileEntry().getName(); @@ -61,9 +70,4 @@ public class BrowserEntry { var n = getFileName(); return FileNames.quoteIfNecessary(n); } - - public String getOptionallyQuotedFilePath() { - var n = rawFileEntry.getPath(); - return FileNames.quoteIfNecessary(n); - } } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java index ede8e24c0..b22d5b373 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java @@ -1,7 +1,8 @@ package io.xpipe.app.browser.file; +import atlantafx.base.controls.Spacer; +import atlantafx.base.theme.Styles; import io.xpipe.app.browser.action.BrowserAction; -import io.xpipe.app.browser.icon.FileIconManager; import io.xpipe.app.comp.base.LazyTextFieldComp; import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.SimpleComp; @@ -16,7 +17,6 @@ import io.xpipe.core.process.OsType; import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileSystem; - import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.*; @@ -31,16 +31,11 @@ import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.control.skin.TableViewSkin; import javafx.scene.control.skin.VirtualFlow; -import javafx.scene.input.DragEvent; -import javafx.scene.input.MouseButton; -import javafx.scene.input.MouseEvent; +import javafx.scene.input.*; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; -import atlantafx.base.controls.Spacer; -import atlantafx.base.theme.Styles; - import java.time.Instant; import java.time.ZoneId; import java.util.ArrayList; @@ -82,7 +77,7 @@ public final class BrowserFileListComp extends SimpleComp { : null)); filenameCol.setComparator(Comparator.comparing(String::toLowerCase)); filenameCol.setSortType(ASCENDING); - filenameCol.setCellFactory(col -> new FilenameCell(fileList.getEditing())); + filenameCol.setCellFactory(col -> new FilenameCell(fileList.getEditing(), col.getTableView())); var sizeCol = new TableColumn(); sizeCol.textProperty().bind(AppI18n.observable("size")); @@ -137,29 +132,7 @@ public final class BrowserFileListComp extends SimpleComp { table.getSelectionModel().setCellSelectionEnabled(false); table.getSelectionModel().getSelectedItems().addListener((ListChangeListener) c -> { - var toSelect = new ArrayList<>(c.getList()); - // Explicitly unselect synthetic entries since we can't use a custom selection model as that is bugged in - // JavaFX - toSelect.removeIf(entry -> fileList.getFileSystemModel().getCurrentParentDirectory() != null - && entry.getRawFileEntry() - .getPath() - .equals(fileList.getFileSystemModel() - .getCurrentParentDirectory() - .getPath())); - // Remove unsuitable selection - toSelect.removeIf(browserEntry -> (browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY - && !fileList.getSelectionMode().isAcceptsDirectories()) - || (browserEntry.getRawFileEntry().getKind() != FileKind.DIRECTORY - && !fileList.getSelectionMode().isAcceptsFiles())); - fileList.getSelection().setAll(toSelect); - - Platform.runLater(() -> { - var toUnselect = table.getSelectionModel().getSelectedItems().stream() - .filter(entry -> !toSelect.contains(entry)) - .toList(); - toUnselect.forEach(entry -> table.getSelectionModel() - .clearSelection(table.getItems().indexOf(entry))); - }); + fileList.getSelection().setAll(c.getList()); }); fileList.getSelection().addListener((ListChangeListener) c -> { @@ -174,7 +147,6 @@ public final class BrowserFileListComp extends SimpleComp { } var indices = c.getList().stream() - .skip(1) .mapToInt(entry -> table.getItems().indexOf(entry)) .toArray(); table.getSelectionModel() @@ -196,8 +168,8 @@ public final class BrowserFileListComp extends SimpleComp { ThreadHelper.runFailableAsync(() -> { browserAction.execute(fileList.getFileSystemModel(), selected); }); + event.consume(); }); - event.consume(); }); } @@ -276,10 +248,6 @@ public final class BrowserFileListComp extends SimpleComp { }, null, () -> { - if (row.getItem() != null && row.getItem().isSynthetic()) { - return null; - } - return new BrowserContextMenu(fileList.getFileSystemModel(), row.getItem()); }) .augment(new SimpleCompStructure<>(row)); @@ -348,12 +316,10 @@ public final class BrowserFileListComp extends SimpleComp { TableColumn modeCol) { var lastDir = new SimpleObjectProperty(); Runnable updateHandler = () -> { - PlatformThread.runLaterIfNeeded(() -> { + Platform.runLater(() -> { var newItems = new ArrayList<>(fileList.getShown().getValue()); - var hasModifiedDate = newItems.size() == 0 - || newItems.stream() - .anyMatch(entry -> entry.getRawFileEntry().getDate() != null); + var hasModifiedDate = newItems.size() == 0 || newItems.stream().anyMatch(entry -> entry.getRawFileEntry().getDate() != null); if (!hasModifiedDate) { table.getColumns().remove(mtimeCol); } else { @@ -363,10 +329,7 @@ public final class BrowserFileListComp extends SimpleComp { } if (fileList.getFileSystemModel().getFileSystem() != null) { - var shell = fileList.getFileSystemModel() - .getFileSystem() - .getShell() - .orElseThrow(); + var shell = fileList.getFileSystemModel().getFileSystem().getShell().orElseThrow(); var hasAttributes = !OsType.WINDOWS.equals(shell.getOsType()); if (!hasAttributes) { table.getColumns().remove(modeCol); @@ -388,10 +351,8 @@ public final class BrowserFileListComp extends SimpleComp { if (!Objects.equals(lastDir.get(), currentDirectory)) { TableViewSkin skin = (TableViewSkin) table.getSkin(); if (skin != null) { - VirtualFlow flow = - (VirtualFlow) skin.getChildren().get(1); - ScrollBar vbar = - (ScrollBar) flow.getChildrenUnmodifiable().get(2); + VirtualFlow flow = (VirtualFlow) skin.getChildren().get(1); + ScrollBar vbar = (ScrollBar) flow.getChildrenUnmodifiable().get(2); if (vbar.getValue() != 0.0) { table.scrollTo(0); } @@ -496,7 +457,7 @@ public final class BrowserFileListComp extends SimpleComp { private final BooleanProperty updating = new SimpleBooleanProperty(); - public FilenameCell(Property editing) { + public FilenameCell(Property editing, TableView tableView) { accessibleTextProperty() .bind(Bindings.createStringBinding( () -> { @@ -526,6 +487,10 @@ public final class BrowserFileListComp extends SimpleComp { itemProperty()) .not() .not()) + .focusTraversable(false) + .apply(struc -> struc.get().focusedProperty().addListener((observable, oldValue, newValue) -> { + getTableRow().requestFocus(); + })) .createRegion(); editing.addListener((observable, oldValue, newValue) -> { @@ -554,6 +519,16 @@ public final class BrowserFileListComp extends SimpleComp { HBox.setHgrow(textField, Priority.ALWAYS); graphic.setAlignment(Pos.CENTER_LEFT); setGraphic(graphic); + + tableView.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + if (event.getCode() == KeyCode.RIGHT) { + var selected = fileList.getSelection(); + if (selected.size() == 1 && selected.getFirst() == getTableRow().getItem()) { + ((ButtonBase) quickAccess).fire(); + event.consume(); + } + } + }); } @Override @@ -573,15 +548,7 @@ public final class BrowserFileListComp extends SimpleComp { // Visibility seems to be bugged, so use opacity setOpacity(0.0); } else { - var isParentLink = getTableRow() - .getItem() - .getRawFileEntry() - .equals(fileList.getFileSystemModel().getCurrentParentDirectory()); - img.set(FileIconManager.getFileIcon( - isParentLink - ? fileList.getFileSystemModel().getCurrentDirectory() - : getTableRow().getItem().getRawFileEntry(), - isParentLink)); + img.set(getTableRow().getItem().getIcon()); var isDirectory = getTableRow().getItem().getRawFileEntry().getKind() == FileKind.DIRECTORY; pseudoClassStateChanged(FOLDER, isDirectory); @@ -594,9 +561,8 @@ public final class BrowserFileListComp extends SimpleComp { .resolved() .getPath() : getTableRow().getItem().getFileName(); - var fileName = isParentLink ? ".." : normalName; - var hidden = !isParentLink - && (getTableRow().getItem().getRawFileEntry().isHidden() || fileName.startsWith(".")); + var fileName = normalName; + var hidden = getTableRow().getItem().getRawFileEntry().isHidden() || fileName.startsWith("."); getTableRow().pseudoClassStateChanged(HIDDEN, hidden); text.set(fileName); // Visibility seems to be bugged, so use opacity diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListCompEntry.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListCompEntry.java index c6b5a4b39..d14e6a06d 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListCompEntry.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListCompEntry.java @@ -3,13 +3,11 @@ package io.xpipe.app.browser.file; import io.xpipe.app.browser.BrowserClipboard; import io.xpipe.app.browser.BrowserSelectionListComp; import io.xpipe.core.store.FileKind; - import javafx.geometry.Point2D; import javafx.scene.Node; import javafx.scene.control.TableView; import javafx.scene.image.Image; import javafx.scene.input.*; - import lombok.Getter; import java.io.File; @@ -44,11 +42,13 @@ public class BrowserFileListCompEntry { // Only clear for normal clicks if (t.isStillSincePress()) { model.getSelection().clear(); + tv.requestFocus(); } t.consume(); return; } + row.requestFocus(); if (t.getClickCount() == 2 && t.getButton() == MouseButton.PRIMARY) { model.onDoubleClick(item); t.consume(); @@ -58,7 +58,7 @@ public class BrowserFileListCompEntry { } public void onMouseShiftClick(MouseEvent t) { - if (isSynthetic()) { + if (t.getButton() != MouseButton.PRIMARY) { return; } @@ -87,11 +87,6 @@ public class BrowserFileListCompEntry { t.consume(); } - public boolean isSynthetic() { - return item != null - && item.getRawFileEntry().equals(model.getFileSystemModel().getCurrentParentDirectory()); - } - private boolean acceptsDrop(DragEvent event) { // Accept drops from outside the app window if (event.getGestureSource() == null) { @@ -109,7 +104,7 @@ public class BrowserFileListCompEntry { if (!Objects.equals( model.getFileSystemModel().getFileSystem(), - cb.getEntries().getFirst().getFileSystem())) { + cb.getEntries().getFirst().getRawFileEntry().getFileSystem())) { return true; } @@ -123,7 +118,7 @@ public class BrowserFileListCompEntry { } // Prevent dropping items onto themselves - if (item != null && cb.getEntries().contains(item.getRawFileEntry())) { + if (item != null && cb.getEntries().contains(item)) { return false; } @@ -157,7 +152,7 @@ public class BrowserFileListCompEntry { var target = item != null && item.getRawFileEntry().getKind() == FileKind.DIRECTORY ? item.getRawFileEntry() : model.getFileSystemModel().getCurrentDirectory(); - model.getFileSystemModel().dropFilesIntoAsync(target, files, false); + model.getFileSystemModel().dropFilesIntoAsync(target, files.stream().map(browserEntry -> browserEntry.getRawFileEntry()).toList(), db.getMode()); event.setDropCompleted(true); event.consume(); } @@ -174,17 +169,16 @@ public class BrowserFileListCompEntry { public void startDrag(MouseEvent event) { if (item == null) { - row.startFullDrag(); return; } - if (isSynthetic()) { + if (event.getButton() != MouseButton.PRIMARY) { return; } - var selected = model.getSelectedRaw(); + var selected = model.getSelection(); Dragboard db = row.startDragAndDrop(TransferMode.COPY); - db.setContent(BrowserClipboard.startDrag(model.getFileSystemModel().getCurrentDirectory(), selected)); + db.setContent(BrowserClipboard.startDrag(model.getFileSystemModel().getCurrentDirectory(), selected, event.isAltDown() ? BrowserFileTransferMode.MOVE : BrowserFileTransferMode.NORMAL)); Image image = BrowserSelectionListComp.snapshot(selected); db.setDragView(image, -20, 15); @@ -224,7 +218,7 @@ public class BrowserFileListCompEntry { model.getFileSystemModel().cdAsync(item.getRawFileEntry().getPath()); } }; - DROP_TIMER.schedule(activeTask, 1000); + DROP_TIMER.schedule(activeTask, 1200); } public void onDragEntered(DragEvent event) { @@ -244,7 +238,7 @@ public class BrowserFileListCompEntry { return; } - if (item == null || item.isSynthetic()) { + if (item == null) { return; } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java index 5f2dd0003..3012d44e5 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java @@ -59,10 +59,7 @@ public final class BrowserFileListModel { public void setAll(Stream newFiles) { try (var s = newFiles) { - var parent = fileSystemModel.getCurrentParentDirectory(); - var l = Stream.concat( - parent != null ? Stream.of(new BrowserEntry(parent, this, true)) : Stream.of(), - s.filter(entry -> entry != null).map(entry -> new BrowserEntry(entry, this, false))) + var l = s.filter(entry -> entry != null).map(entry -> new BrowserEntry(entry, this)) .toList(); all.setValue(l); refreshShown(); @@ -94,14 +91,13 @@ public final class BrowserFileListModel { } public Comparator order() { - var syntheticFirst = Comparator.comparing(path -> !path.isSynthetic()); var dirsFirst = Comparator.comparing( path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY); var comp = comparatorProperty.getValue(); Comparator us = comp != null - ? syntheticFirst.thenComparing(dirsFirst).thenComparing(comp) - : syntheticFirst.thenComparing(dirsFirst); + ? dirsFirst.thenComparing(comp) + : dirsFirst; return us; } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOverviewComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOverviewComp.java index e364c8449..863a80d30 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOverviewComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOverviewComp.java @@ -37,7 +37,7 @@ public class BrowserFileOverviewComp extends SimpleComp { var graphic = new HorizontalComp(List.of( icon, new BrowserQuickAccessButtonComp( - () -> new BrowserEntry(entry, model.getFileList(), false), model))); + () -> new BrowserEntry(entry, model.getFileList()), model))); var l = new Button(entry.getPath(), graphic.createRegion()); l.setGraphicTextGap(1); l.setOnAction(event -> { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferMode.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferMode.java new file mode 100644 index 000000000..a01cd5b4f --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferMode.java @@ -0,0 +1,8 @@ +package io.xpipe.app.browser.file; + +public enum BrowserFileTransferMode { + + NORMAL, + COPY, + MOVE +} diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java new file mode 100644 index 000000000..34ca7552b --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java @@ -0,0 +1,300 @@ +package io.xpipe.app.browser.file; + +import io.xpipe.app.browser.BrowserTransferProgress; +import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.core.store.FileKind; +import io.xpipe.core.store.FileNames; +import io.xpipe.core.store.FilePath; +import io.xpipe.core.store.FileSystem; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Path; +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +public class BrowserFileTransferOperation { + + private final FileSystem.FileEntry target; + private final List files; + private final BrowserFileTransferMode transferMode; + private final boolean checkConflicts; + private final Consumer progress; + + BrowserAlerts.FileConflictChoice lastConflictChoice; + + public BrowserFileTransferOperation(FileSystem.FileEntry target, List files, BrowserFileTransferMode transferMode, boolean checkConflicts, + Consumer progress + ) { + this.target = target; + this.files = files; + this.transferMode = transferMode; + this.checkConflicts = checkConflicts; + this.progress = progress; + } + + public static BrowserFileTransferOperation ofLocal(FileSystem.FileEntry target, List files, BrowserFileTransferMode transferMode, boolean checkConflicts, Consumer progress) { + var entries = files.stream() + .map(path -> { + try { + return LocalFileSystem.getLocalFileEntry(path); + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .toList(); + return new BrowserFileTransferOperation(target, entries, transferMode, checkConflicts, progress); + } + + private void updateProgress(BrowserTransferProgress progress) { + this.progress.accept(progress); + } + + private boolean handleChoice( + FileSystem fileSystem, + String target, + boolean multiple) + throws Exception { + if (lastConflictChoice == BrowserAlerts.FileConflictChoice.CANCEL) { + return false; + } + + if (lastConflictChoice == BrowserAlerts.FileConflictChoice.REPLACE_ALL) { + return true; + } + + if (fileSystem.fileExists(target)) { + if (lastConflictChoice == BrowserAlerts.FileConflictChoice.SKIP_ALL) { + return false; + } + + var choice = BrowserAlerts.showFileConflictAlert(target, multiple); + if (choice == BrowserAlerts.FileConflictChoice.CANCEL) { + lastConflictChoice = BrowserAlerts.FileConflictChoice.CANCEL; + return false; + } + + if (choice == BrowserAlerts.FileConflictChoice.SKIP) { + return false; + } + + if (choice == BrowserAlerts.FileConflictChoice.SKIP_ALL) { + lastConflictChoice = BrowserAlerts.FileConflictChoice.SKIP_ALL; + return false; + } + + if (choice == BrowserAlerts.FileConflictChoice.REPLACE_ALL) { + lastConflictChoice = BrowserAlerts.FileConflictChoice.REPLACE_ALL; + return true; + } + } + return true; + } + + public void execute() + throws Exception { + if (files.isEmpty()) { + updateProgress(BrowserTransferProgress.empty()); + return; + } + + var same = files.getFirst().getFileSystem().equals(target.getFileSystem()); + var doesMove = transferMode == BrowserFileTransferMode.MOVE || (same && transferMode == BrowserFileTransferMode.NORMAL); + if (doesMove) { + if (!BrowserAlerts.showMoveAlert(files, target)) { + return; + } + } + + for (var file : files) { + if (same) { + handleSingleOnSameFileSystem(file); + updateProgress(BrowserTransferProgress.finished(file.getName(), file.getSize())); + } else { + handleSingleAcrossFileSystems(file); + } + } + + if (!same && doesMove) { + for (var file : files) { + deleteSingle(file); + } + } + } + + private void handleSingleOnSameFileSystem(FileSystem.FileEntry source) + throws Exception { + // Prevent dropping directory into itself + if (source.getPath().equals(target.getPath())) { + return; + } + + var sourceFile = source.getPath(); + var targetFile = FileNames.join(target.getPath(), FileNames.getFileName(sourceFile)); + + if (sourceFile.equals(targetFile)) { + return; + } + + if (source.getKind() == FileKind.DIRECTORY && target.getFileSystem().directoryExists(targetFile)) { + throw ErrorEvent.expected( + new IllegalArgumentException("Target directory " + targetFile + " does already exist")); + } + + if (checkConflicts && !handleChoice(target.getFileSystem(), targetFile, files.size() > 1)) { + return; + } + + var doesMove = transferMode == BrowserFileTransferMode.MOVE || transferMode == BrowserFileTransferMode.NORMAL; + if (doesMove) { + target.getFileSystem().move(sourceFile, targetFile); + } else { + target.getFileSystem().copy(sourceFile, targetFile); + } + } + + private void handleSingleAcrossFileSystems(FileSystem.FileEntry source) + throws Exception { + if (target.getKind() != FileKind.DIRECTORY) { + throw new IllegalStateException("Target " + target.getPath() + " is not a directory"); + } + + var flatFiles = new LinkedHashMap(); + + // Prevent dropping directory into itself + if (source.getFileSystem().equals(target.getFileSystem()) + && FileNames.startsWith(source.getPath(), target.getPath())) { + return; + } + + AtomicLong totalSize = new AtomicLong(); + if (source.getKind() == FileKind.DIRECTORY) { + var directoryName = FileNames.getFileName(source.getPath()); + flatFiles.put(source, directoryName); + + var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath())); + List list = source.getFileSystem().listFilesRecursively(source.getPath()); + for (FileSystem.FileEntry fileEntry : list) { + var rel = FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath())); + flatFiles.put(fileEntry, rel); + if (fileEntry.getKind() == FileKind.FILE) { + // This one is up-to-date and does not need to be recalculated + totalSize.addAndGet(fileEntry.getSize()); + } + } + } else { + flatFiles.put(source, FileNames.getFileName(source.getPath())); + // Recalculate as it could have been changed meanwhile + totalSize.addAndGet(source.getFileSystem().getFileSize(source.getPath())); + } + + AtomicLong transferred = new AtomicLong(); + for (var e : flatFiles.entrySet()) { + var sourceFile = e.getKey(); + var fixedRelPath = new FilePath(e.getValue()) + .fileSystemCompatible( + target.getFileSystem().getShell().orElseThrow().getOsType()); + var targetFile = FileNames.join(target.getPath(), fixedRelPath.toString()); + if (sourceFile.getFileSystem().equals(target.getFileSystem())) { + throw new IllegalStateException(); + } + + if (sourceFile.getKind() == FileKind.DIRECTORY) { + target.getFileSystem().mkdirs(targetFile); + } else if (sourceFile.getKind() == FileKind.FILE) { + if (checkConflicts + && !handleChoice( + target.getFileSystem(), + targetFile, + files.size() > 1 || flatFiles.size() > 1)) { + continue; + } + + InputStream inputStream = null; + OutputStream outputStream = null; + try { + var fileSize = sourceFile.getFileSystem().getFileSize(sourceFile.getPath()); + inputStream = sourceFile.getFileSystem().openInput(sourceFile.getPath()); + outputStream = target.getFileSystem().openOutput(targetFile, fileSize); + transferFile(sourceFile, inputStream, outputStream, transferred, totalSize); + inputStream.transferTo(OutputStream.nullOutputStream()); + } catch (Exception ex) { + // Mark progress as finished to reset any progress display + updateProgress(BrowserTransferProgress.finished(sourceFile.getName(), transferred.get())); + + if (inputStream != null) { + try { + inputStream.close(); + } catch (Exception om) { + // This is expected as the process control has to be killed + // When calling close, it will throw an exception when it has to kill + // ErrorEvent.fromThrowable(om).handle(); + } + } + if (outputStream != null) { + try { + outputStream.close(); + } catch (Exception om) { + // This is expected as the process control has to be killed + // When calling close, it will throw an exception when it has to kill + // ErrorEvent.fromThrowable(om).handle(); + } + } + throw ex; + } + + updateProgress(BrowserTransferProgress.finished(sourceFile.getName(), transferred.get())); + Exception exception = null; + try { + inputStream.close(); + } catch (Exception om) { + exception = om; + } + try { + outputStream.close(); + } catch (Exception om) { + if (exception != null) { + ErrorEvent.fromThrowable(om).handle(); + } else { + exception = om; + } + } + if (exception != null) { + throw exception; + } + } + } + updateProgress(BrowserTransferProgress.finished(source.getName(), totalSize.get())); + } + + private void deleteSingle(FileSystem.FileEntry source) throws Exception { + source.getFileSystem().delete(source.getPath()); + } + + private static final int DEFAULT_BUFFER_SIZE = 1024; + + private void transferFile( + FileSystem.FileEntry sourceFile, + InputStream inputStream, + OutputStream outputStream, + AtomicLong transferred, + AtomicLong total) + throws IOException { + // Initialize progress immediately prior to reading anything + var now = Instant.now(); + updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), now)); + + var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, sourceFile.getSize()); + byte[] buffer = new byte[bs]; + int read; + while ((read = inputStream.read(buffer, 0, bs)) > 0) { + outputStream.write(buffer, 0, read); + transferred.addAndGet(read); + updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), now)); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java index 066df2691..41260cdfb 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java @@ -101,7 +101,7 @@ public class BrowserQuickAccessContextMenu extends ContextMenu { newItems.add(empty); } else { var browserEntries = list.stream() - .map(fileEntry -> new BrowserEntry(fileEntry, model.getFileList(), false)) + .map(fileEntry -> new BrowserEntry(fileEntry, model.getFileList())) .toList(); var menus = browserEntries.stream() .sorted(model.getFileList().order()) diff --git a/app/src/main/java/io/xpipe/app/browser/file/FileSystemHelper.java b/app/src/main/java/io/xpipe/app/browser/file/FileSystemHelper.java index 1dabdd698..26a5a8dfc 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/FileSystemHelper.java +++ b/app/src/main/java/io/xpipe/app/browser/file/FileSystemHelper.java @@ -1,28 +1,17 @@ package io.xpipe.app.browser.file; -import io.xpipe.app.browser.BrowserTransferProgress; import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.core.process.OsType; -import io.xpipe.core.store.*; +import io.xpipe.core.store.FileKind; +import io.xpipe.core.store.FileNames; +import io.xpipe.core.store.FileSystem; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Path; import java.time.Instant; -import java.util.LinkedHashMap; import java.util.List; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; public class FileSystemHelper { - private static final int DEFAULT_BUFFER_SIZE = 1024; - private static FileSystem localFileSystem; - public static String adjustPath(OpenFileSystemModel model, String path) { if (path == null) { return null; @@ -134,23 +123,6 @@ public class FileSystemHelper { } } - public static FileSystem.FileEntry getLocal(Path file) throws Exception { - if (localFileSystem == null) { - localFileSystem = new LocalStore().createFileSystem(); - localFileSystem.open(); - } - - return new FileSystem.FileEntry( - localFileSystem, - file.toString(), - Files.getLastModifiedTime(file).toInstant(), - Files.isHidden(file), - Files.isExecutable(file), - Files.size(file), - null, - Files.isDirectory(file) ? FileKind.DIRECTORY : FileKind.FILE); - } - public static FileSystem.FileEntry getRemoteWrapper(FileSystem fileSystem, String file) throws Exception { return new FileSystem.FileEntry( fileSystem, @@ -163,24 +135,6 @@ public class FileSystemHelper { fileSystem.directoryExists(file) ? FileKind.DIRECTORY : FileKind.FILE); } - public static void dropLocalFilesInto( - FileSystem.FileEntry entry, - List files, - Consumer progress, - boolean checkConflicts) - throws Exception { - var entries = files.stream() - .map(path -> { - try { - return getLocal(path); - } catch (Exception e) { - throw new RuntimeException(e); - } - }) - .toList(); - dropFilesInto(entry, entries, false, checkConflicts, progress); - } - public static void delete(List files) { if (files.isEmpty()) { return; @@ -194,255 +148,4 @@ public class FileSystemHelper { } } } - - public static void dropFilesInto( - FileSystem.FileEntry target, - List files, - boolean explicitCopy, - boolean checkConflicts, - Consumer progress) - throws Exception { - if (files.isEmpty()) { - progress.accept(BrowserTransferProgress.empty()); - return; - } - - var same = files.getFirst().getFileSystem().equals(target.getFileSystem()); - if (same && !explicitCopy) { - if (!BrowserAlerts.showMoveAlert(files, target)) { - return; - } - } - - AtomicReference lastConflictChoice = new AtomicReference<>(); - for (var file : files) { - if (file.getFileSystem().equals(target.getFileSystem())) { - dropFileAcrossSameFileSystem( - target, file, explicitCopy, lastConflictChoice, files.size() > 1, checkConflicts); - progress.accept(BrowserTransferProgress.finished(file.getName(), file.getSize())); - } else { - dropFileAcrossFileSystems(target, file, progress, lastConflictChoice, files.size() > 1, checkConflicts); - } - } - } - - private static void dropFileAcrossSameFileSystem( - FileSystem.FileEntry target, - FileSystem.FileEntry source, - boolean explicitCopy, - AtomicReference lastConflictChoice, - boolean multiple, - boolean checkConflicts) - throws Exception { - // Prevent dropping directory into itself - if (source.getPath().equals(target.getPath())) { - return; - } - - var sourceFile = source.getPath(); - var targetFile = FileNames.join(target.getPath(), FileNames.getFileName(sourceFile)); - - if (sourceFile.equals(targetFile)) { - return; - } - - if (source.getKind() == FileKind.DIRECTORY && target.getFileSystem().directoryExists(targetFile)) { - throw ErrorEvent.expected( - new IllegalArgumentException("Target directory " + targetFile + " does already exist")); - } - - if (checkConflicts && !handleChoice(lastConflictChoice, target.getFileSystem(), targetFile, multiple)) { - return; - } - - if (explicitCopy) { - target.getFileSystem().copy(sourceFile, targetFile); - } else { - target.getFileSystem().move(sourceFile, targetFile); - } - } - - private static void dropFileAcrossFileSystems( - FileSystem.FileEntry target, - FileSystem.FileEntry source, - Consumer progress, - AtomicReference lastConflictChoice, - boolean multiple, - boolean checkConflicts) - throws Exception { - if (target.getKind() != FileKind.DIRECTORY) { - throw new IllegalStateException("Target " + target.getPath() + " is not a directory"); - } - - var flatFiles = new LinkedHashMap(); - - // Prevent dropping directory into itself - if (source.getFileSystem().equals(target.getFileSystem()) - && FileNames.startsWith(source.getPath(), target.getPath())) { - return; - } - - AtomicLong totalSize = new AtomicLong(); - if (source.getKind() == FileKind.DIRECTORY) { - var directoryName = FileNames.getFileName(source.getPath()); - flatFiles.put(source, directoryName); - - var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath())); - List list = source.getFileSystem().listFilesRecursively(source.getPath()); - for (FileSystem.FileEntry fileEntry : list) { - var rel = FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath())); - flatFiles.put(fileEntry, rel); - if (fileEntry.getKind() == FileKind.FILE) { - // This one is up-to-date and does not need to be recalculated - totalSize.addAndGet(fileEntry.getSize()); - } - } - } else { - flatFiles.put(source, FileNames.getFileName(source.getPath())); - // Recalculate as it could have been changed meanwhile - totalSize.addAndGet(source.getFileSystem().getFileSize(source.getPath())); - } - - AtomicLong transferred = new AtomicLong(); - for (var e : flatFiles.entrySet()) { - var sourceFile = e.getKey(); - var fixedRelPath = new FilePath(e.getValue()) - .fileSystemCompatible( - target.getFileSystem().getShell().orElseThrow().getOsType()); - var targetFile = FileNames.join(target.getPath(), fixedRelPath.toString()); - if (sourceFile.getFileSystem().equals(target.getFileSystem())) { - throw new IllegalStateException(); - } - - if (sourceFile.getKind() == FileKind.DIRECTORY) { - target.getFileSystem().mkdirs(targetFile); - } else if (sourceFile.getKind() == FileKind.FILE) { - if (checkConflicts - && !handleChoice( - lastConflictChoice, - target.getFileSystem(), - targetFile, - multiple || flatFiles.size() > 1)) { - continue; - } - - InputStream inputStream = null; - OutputStream outputStream = null; - try { - var fileSize = sourceFile.getFileSystem().getFileSize(sourceFile.getPath()); - inputStream = sourceFile.getFileSystem().openInput(sourceFile.getPath()); - outputStream = target.getFileSystem().openOutput(targetFile, fileSize); - transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, progress); - inputStream.transferTo(OutputStream.nullOutputStream()); - } catch (Exception ex) { - // Mark progress as finished to reset any progress display - progress.accept(BrowserTransferProgress.finished(sourceFile.getName(), transferred.get())); - - if (inputStream != null) { - try { - inputStream.close(); - } catch (Exception om) { - // This is expected as the process control has to be killed - // When calling close, it will throw an exception when it has to kill - // ErrorEvent.fromThrowable(om).handle(); - } - } - if (outputStream != null) { - try { - outputStream.close(); - } catch (Exception om) { - // This is expected as the process control has to be killed - // When calling close, it will throw an exception when it has to kill - // ErrorEvent.fromThrowable(om).handle(); - } - } - throw ex; - } - - progress.accept(BrowserTransferProgress.finished(sourceFile.getName(), transferred.get())); - Exception exception = null; - try { - inputStream.close(); - } catch (Exception om) { - exception = om; - } - try { - outputStream.close(); - } catch (Exception om) { - if (exception != null) { - ErrorEvent.fromThrowable(om).handle(); - } else { - exception = om; - } - } - if (exception != null) { - throw exception; - } - } - } - progress.accept(BrowserTransferProgress.finished(source.getName(), totalSize.get())); - } - - private static boolean handleChoice( - AtomicReference previous, - FileSystem fileSystem, - String target, - boolean multiple) - throws Exception { - if (previous.get() == BrowserAlerts.FileConflictChoice.CANCEL) { - return false; - } - - if (previous.get() == BrowserAlerts.FileConflictChoice.REPLACE_ALL) { - return true; - } - - if (fileSystem.fileExists(target)) { - if (previous.get() == BrowserAlerts.FileConflictChoice.SKIP_ALL) { - return false; - } - - var choice = BrowserAlerts.showFileConflictAlert(target, multiple); - if (choice == BrowserAlerts.FileConflictChoice.CANCEL) { - previous.set(BrowserAlerts.FileConflictChoice.CANCEL); - return false; - } - - if (choice == BrowserAlerts.FileConflictChoice.SKIP) { - return false; - } - - if (choice == BrowserAlerts.FileConflictChoice.SKIP_ALL) { - previous.set(BrowserAlerts.FileConflictChoice.SKIP_ALL); - return false; - } - - if (choice == BrowserAlerts.FileConflictChoice.REPLACE_ALL) { - previous.set(BrowserAlerts.FileConflictChoice.REPLACE_ALL); - return true; - } - } - return true; - } - - private static void transferFile( - FileSystem.FileEntry sourceFile, - InputStream inputStream, - OutputStream outputStream, - AtomicLong transferred, - AtomicLong total, - Consumer progress) - throws IOException { - // Initialize progress immediately prior to reading anything - progress.accept(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get())); - - var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, sourceFile.getSize()); - byte[] buffer = new byte[bs]; - int read; - while ((read = inputStream.read(buffer, 0, bs)) > 0) { - outputStream.write(buffer, 0, read); - transferred.addAndGet(read); - progress.accept(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get())); - } - } } diff --git a/app/src/main/java/io/xpipe/app/browser/file/LocalFileSystem.java b/app/src/main/java/io/xpipe/app/browser/file/LocalFileSystem.java new file mode 100644 index 000000000..2fe56c18e --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/file/LocalFileSystem.java @@ -0,0 +1,42 @@ +package io.xpipe.app.browser.file; + +import io.xpipe.core.store.FileKind; +import io.xpipe.core.store.FileSystem; +import io.xpipe.core.store.LocalStore; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class LocalFileSystem { + + private static FileSystem localFileSystem; + + public static void init() throws Exception { + if (localFileSystem == null) { + localFileSystem = new LocalStore().createFileSystem(); + localFileSystem.open(); + } + } + + public static FileSystem.FileEntry getLocalFileEntry(Path file) throws IOException { + if (localFileSystem == null) { + throw new IllegalStateException(); + } + + return new FileSystem.FileEntry( + localFileSystem, + file.toString(), + Files.getLastModifiedTime(file).toInstant(), + Files.isHidden(file), + Files.isExecutable(file), + Files.size(file), + null, + Files.isDirectory(file) ? FileKind.DIRECTORY : FileKind.FILE); + } + + public static BrowserEntry getLocalBrowserEntry(Path file) throws Exception { + var e = getLocalFileEntry(file); + return new BrowserEntry(e,null); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemModel.java b/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemModel.java index 125f9585a..4c45c7d98 100644 --- a/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemModel.java +++ b/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemModel.java @@ -4,6 +4,8 @@ import io.xpipe.app.browser.BrowserSavedState; import io.xpipe.app.browser.BrowserTransferProgress; import io.xpipe.app.browser.action.BrowserAction; import io.xpipe.app.browser.file.BrowserFileListModel; +import io.xpipe.app.browser.file.BrowserFileTransferMode; +import io.xpipe.app.browser.file.BrowserFileTransferOperation; import io.xpipe.app.browser.file.FileSystemHelper; import io.xpipe.app.browser.session.BrowserAbstractSessionModel; import io.xpipe.app.browser.session.BrowserSessionModel; @@ -22,10 +24,8 @@ import io.xpipe.core.process.ShellDialects; import io.xpipe.core.process.ShellOpenFunction; import io.xpipe.core.store.*; import io.xpipe.core.util.FailableConsumer; - import javafx.beans.binding.Bindings; import javafx.beans.property.*; - import lombok.Getter; import lombok.SneakyThrows; @@ -343,14 +343,16 @@ public final class OpenFileSystemModel extends BrowserSessionTab files, boolean explicitCopy) { + FileSystem.FileEntry target, List files, BrowserFileTransferMode mode + ) { // We don't have to do anything in this case if (files.isEmpty()) { return; @@ -363,9 +365,8 @@ public final class OpenFileSystemModel extends BrowserSessionTab { - progress.setValue(browserTransferProgress); - }); + var op = new BrowserFileTransferOperation(target, files, mode,true, progress::setValue); + op.execute(); refreshSync(); }); }); diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java index abb64a973..f7d340802 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java @@ -389,7 +389,7 @@ public abstract class StoreEntryComp extends SimpleComp { .getSortedCategories(wrapper.getCategory().getValue().getRoot()) .forEach(storeCategoryWrapper -> { MenuItem m = new MenuItem(); - m.textProperty().bind(storeCategoryWrapper.nameProperty()); + m.textProperty().setValue(" ".repeat(storeCategoryWrapper.getDepth()) + storeCategoryWrapper.getName().getValue()); m.setOnAction(event -> { wrapper.moveTo(storeCategoryWrapper.getCategory()); event.consume(); diff --git a/app/src/main/java/io/xpipe/app/core/AppI18n.java b/app/src/main/java/io/xpipe/app/core/AppI18n.java index 2d3f634b9..d0a978abe 100644 --- a/app/src/main/java/io/xpipe/app/core/AppI18n.java +++ b/app/src/main/java/io/xpipe/app/core/AppI18n.java @@ -6,21 +6,17 @@ import io.xpipe.app.fxcomps.impl.TooltipAugment; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.prefs.AppPrefs; -import io.xpipe.app.prefs.SupportedLocale; import io.xpipe.app.util.OptionsBuilder; import io.xpipe.app.util.Translatable; import io.xpipe.core.util.ModuleHelper; import io.xpipe.core.util.XPipeInstallation; - import javafx.beans.binding.Bindings; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; - import lombok.SneakyThrows; import lombok.Value; import org.apache.commons.io.FilenameUtils; -import org.ocpsoft.prettytime.PrettyTime; import java.io.IOException; import java.io.InputStreamReader; @@ -131,7 +127,7 @@ public class AppI18n { } } - private LoadedTranslations getLoaded() { + public LoadedTranslations getLoaded() { return currentLanguage.getValue() != null ? currentLanguage.getValue() : english; } @@ -278,21 +274,15 @@ public class AppI18n { }); } - var prettyTime = new PrettyTime( - AppPrefs.get() != null - ? AppPrefs.get().language().getValue().getLocale() - : SupportedLocale.getEnglish().getLocale()); - - return new LoadedTranslations(l, translations, markdownDocumentations, prettyTime); + return new LoadedTranslations(l, translations, markdownDocumentations); } @Value - static class LoadedTranslations { + public static class LoadedTranslations { Locale locale; Map translations; Map markdownDocumentations; - PrettyTime prettyTime; } @SuppressWarnings("removal") diff --git a/app/src/main/java/io/xpipe/app/core/mode/GuiMode.java b/app/src/main/java/io/xpipe/app/core/mode/GuiMode.java index b0e1096c1..528d69b78 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/GuiMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/GuiMode.java @@ -1,5 +1,6 @@ package io.xpipe.app.core.mode; +import io.xpipe.app.browser.file.LocalFileSystem; import io.xpipe.app.browser.icon.FileIconManager; import io.xpipe.app.core.App; import io.xpipe.app.core.AppGreetings; @@ -49,9 +50,13 @@ public class GuiMode extends PlatformMode { }); TrackEvent.info("Window setup complete"); - ThreadHelper.runAsync(() -> { + // Can be loaded async + ThreadHelper.runFailableAsync(() -> { FileIconManager.loadIfNecessary(); }); + ThreadHelper.runFailableAsync(() -> { + LocalFileSystem.init(); + }); UpdateChangelogAlert.showIfNeeded(); } diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorage.java b/app/src/main/java/io/xpipe/app/storage/DataStorage.java index 7f813ea78..922d5b962 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -120,7 +120,6 @@ public abstract class DataStorage { } private synchronized void dispose() { - onReset(); save(true); } @@ -183,8 +182,6 @@ public abstract class DataStorage { }); } - protected void onReset() {} - protected Path getStoresDir() { return dir.resolve("stores"); } diff --git a/app/src/main/java/io/xpipe/app/util/HumanReadableFormat.java b/app/src/main/java/io/xpipe/app/util/HumanReadableFormat.java index 9fb9b1095..27767610a 100644 --- a/app/src/main/java/io/xpipe/app/util/HumanReadableFormat.java +++ b/app/src/main/java/io/xpipe/app/util/HumanReadableFormat.java @@ -2,6 +2,7 @@ package io.xpipe.app.util; import java.text.CharacterIterator; import java.text.StringCharacterIterator; +import java.time.Duration; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; @@ -80,4 +81,13 @@ public final class HumanReadableFormat { private static int getWeekNumber(LocalDateTime date) { return date.get(WeekFields.of(Locale.getDefault()).weekOfYear()); } + + + public static String duration(Duration duration) { + return duration.toString() + .substring(2) + .replaceAll("(\\d[HMS])(?!$)", "$1 ") + .replaceAll("\\.\\d+", "") + .toLowerCase(); + } } diff --git a/app/src/main/java/module-info.java b/app/src/main/java/module-info.java index 8faa5e355..a0d3e8280 100644 --- a/app/src/main/java/module-info.java +++ b/app/src/main/java/module-info.java @@ -49,7 +49,6 @@ open module io.xpipe.app { requires org.slf4j; requires org.slf4j.jdk.platform.logging; requires atlantafx.base; - requires org.ocpsoft.prettytime; requires com.vladsch.flexmark; requires com.fasterxml.jackson.core; requires com.fasterxml.jackson.databind; diff --git a/build.gradle b/build.gradle index 629463e1a..3906a2da6 100644 --- a/build.gradle +++ b/build.gradle @@ -103,7 +103,6 @@ project.ext { authors = 'Christopher Schnick' javafxVersion = '22.0.1' platformName = getPlatformName() - artifactChecksums = new HashMap() languages = ["en", "nl", "es", "fr", "de", "it", "pt", "ru", "ja", "zh", "tr", "da"] jvmRunArgs = [ "--add-opens", "java.base/java.lang=io.xpipe.app", diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyAction.java index 194c3de41..e1a2ab314 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyAction.java @@ -22,7 +22,7 @@ public class CopyAction implements LeafAction { public void execute(OpenFileSystemModel model, List entries) { BrowserClipboard.startCopy( model.getCurrentDirectory(), - entries.stream().map(entry -> entry.getRawFileEntry()).toList()); + entries); } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/PasteAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/PasteAction.java index 48f1809dc..4ce2c9b02 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/PasteAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/PasteAction.java @@ -3,6 +3,7 @@ package io.xpipe.ext.base.browser; import io.xpipe.app.browser.BrowserClipboard; import io.xpipe.app.browser.action.LeafAction; import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.file.BrowserFileTransferMode; import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.core.AppI18n; import io.xpipe.core.store.FileKind; @@ -34,7 +35,7 @@ public class PasteAction implements LeafAction { return; } - model.dropFilesIntoAsync(target, files, true); + model.dropFilesIntoAsync(target, files.stream().map(browserEntry -> browserEntry.getRawFileEntry()).toList(), BrowserFileTransferMode.COPY); } @Override