Merge branch browser-model into master

This commit is contained in:
crschnick 2024-05-07 07:31:09 +00:00
parent b98ac6b4ea
commit 9d4c4fe97d
28 changed files with 520 additions and 497 deletions

View file

@ -43,7 +43,6 @@ dependencies {
api 'info.picocli:picocli:4.7.5' api 'info.picocli:picocli:4.7.5'
api 'org.kohsuke:github-api:1.321' api 'org.kohsuke:github-api:1.321'
api 'io.sentry:sentry:7.8.0' 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 '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.core', name: 'jackson-databind', version: "2.17.0"
api group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: "2.17.0" api group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: "2.17.0"

View file

@ -1,17 +1,17 @@
package io.xpipe.app.browser; 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.issue.ErrorEvent;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.ProcessControlProvider; import io.xpipe.core.process.ProcessControlProvider;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileSystem;
import io.xpipe.core.util.FailableRunnable; import io.xpipe.core.util.FailableRunnable;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.input.ClipboardContent; import javafx.scene.input.ClipboardContent;
import javafx.scene.input.Dragboard; import javafx.scene.input.Dragboard;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.Value; import lombok.Value;
@ -45,17 +45,17 @@ public class BrowserClipboard {
List<File> data = (List<File>) clipboard.getData(DataFlavor.javaFileListFlavor); List<File> data = (List<File>) clipboard.getData(DataFlavor.javaFileListFlavor);
var files = var files =
data.stream().map(string -> string.toPath()).toList(); data.stream().map(f -> f.toPath()).toList();
if (files.size() == 0) { if (files.size() == 0) {
return; return;
} }
var entries = new ArrayList<FileSystem.FileEntry>(); var entries = new ArrayList<BrowserEntry>();
for (Path file : files) { 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) { } catch (Exception e) {
ErrorEvent.fromThrowable(e).expected().omit().handle(); ErrorEvent.fromThrowable(e).expected().omit().handle();
} }
@ -64,27 +64,27 @@ public class BrowserClipboard {
} }
@SneakyThrows @SneakyThrows
public static ClipboardContent startDrag(FileSystem.FileEntry base, List<FileSystem.FileEntry> selected) { public static ClipboardContent startDrag(FileSystem.FileEntry base, List<BrowserEntry> selected, BrowserFileTransferMode mode) {
if (selected.isEmpty()) { if (selected.isEmpty()) {
return null; return null;
} }
var content = new ClipboardContent(); var content = new ClipboardContent();
var id = UUID.randomUUID(); 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()); content.putString(currentDragClipboard.toClipboardString());
return content; return content;
} }
@SneakyThrows @SneakyThrows
public static void startCopy(FileSystem.FileEntry base, List<FileSystem.FileEntry> selected) { public static void startCopy(FileSystem.FileEntry base, List<BrowserEntry> selected) {
if (selected.isEmpty()) { if (selected.isEmpty()) {
currentCopyClipboard.setValue(null); currentCopyClipboard.setValue(null);
return; return;
} }
var id = UUID.randomUUID(); 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() { public static Instance retrieveCopy() {
@ -118,11 +118,12 @@ public class BrowserClipboard {
public static class Instance { public static class Instance {
UUID uuid; UUID uuid;
FileSystem.FileEntry baseDirectory; FileSystem.FileEntry baseDirectory;
List<FileSystem.FileEntry> entries; List<BrowserEntry> entries;
BrowserFileTransferMode mode;
public String toClipboardString() { public String toClipboardString() {
return entries.stream() return entries.stream()
.map(fileEntry -> "\"" + fileEntry.getPath() + "\"") .map(fileEntry -> "\"" + fileEntry.getRawFileEntry().getPath() + "\"")
.collect(Collectors.joining(ProcessControlProvider.get() .collect(Collectors.joining(ProcessControlProvider.get()
.getEffectiveLocalDialect() .getEffectiveLocalDialect()
.getNewLine() .getNewLine()

View file

@ -1,6 +1,6 @@
package io.xpipe.app.browser; 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.comp.base.ListBoxViewComp;
import io.xpipe.app.core.AppStyle; import io.xpipe.app.core.AppStyle;
import io.xpipe.app.core.AppWindowHelper; 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.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; 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.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
@ -21,7 +18,6 @@ import javafx.scene.control.OverrunStyle;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Value; import lombok.Value;
@ -33,14 +29,14 @@ import java.util.function.Function;
@AllArgsConstructor @AllArgsConstructor
public class BrowserSelectionListComp extends SimpleComp { public class BrowserSelectionListComp extends SimpleComp {
ObservableList<FileSystem.FileEntry> list; ObservableList<BrowserEntry> list;
Function<FileSystem.FileEntry, ObservableValue<String>> nameTransformation; Function<BrowserEntry, ObservableValue<String>> nameTransformation;
public BrowserSelectionListComp(ObservableList<FileSystem.FileEntry> list) { public BrowserSelectionListComp(ObservableList<BrowserEntry> list) {
this(list, entry -> new SimpleStringProperty(FileNames.getFileName(entry.getPath()))); this(list, entry -> new SimpleStringProperty(entry.getFileName()));
} }
public static Image snapshot(ObservableList<FileSystem.FileEntry> list) { public static Image snapshot(ObservableList<BrowserEntry> list) {
var r = new BrowserSelectionListComp(list).styleClass("drag").createRegion(); var r = new BrowserSelectionListComp(list).styleClass("drag").createRegion();
var scene = new Scene(r); var scene = new Scene(r);
AppWindowHelper.setupStylesheets(scene); AppWindowHelper.setupStylesheets(scene);
@ -54,7 +50,7 @@ public class BrowserSelectionListComp extends SimpleComp {
protected Region createSimple() { protected Region createSimple() {
var c = new ListBoxViewComp<>(list, list, entry -> { var c = new ListBoxViewComp<>(list, list, entry -> {
return Comp.of(() -> { return Comp.of(() -> {
var image = PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(entry, false), 24) var image = PrettyImageHelper.ofFixedSizeSquare(entry.getIcon(), 24)
.createRegion(); .createRegion();
var l = new Label(null, image); var l = new Label(null, image);
l.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); l.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);

View file

@ -1,5 +1,6 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import atlantafx.base.controls.Spacer;
import io.xpipe.app.browser.file.BrowserContextMenu; import io.xpipe.app.browser.file.BrowserContextMenu;
import io.xpipe.app.browser.file.BrowserFileListCompEntry; import io.xpipe.app.browser.file.BrowserFileListCompEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel; 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.impl.LabelComp;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.util.HumanReadableFormat; import io.xpipe.app.util.HumanReadableFormat;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.scene.control.ToolBar; import javafx.scene.control.ToolBar;
import javafx.scene.input.MouseButton; import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import atlantafx.base.controls.Spacer;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Value; import lombok.Value;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
@Value @Value
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class BrowserStatusBarComp extends SimpleComp { public class BrowserStatusBarComp extends SimpleComp {
@ -56,7 +57,9 @@ public class BrowserStatusBarComp extends SimpleComp {
var transferred = HumanReadableFormat.progressByteCount(p.getTransferred()); var transferred = HumanReadableFormat.progressByteCount(p.getTransferred());
var all = HumanReadableFormat.byteCount(p.getTotal()); var all = HumanReadableFormat.byteCount(p.getTotal());
var name = (p.getName() != null ? " @ " + p.getName() + " " : ""); 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"); var progressComp = new LabelComp(text).styleClass("progress");
@ -87,9 +90,7 @@ public class BrowserStatusBarComp extends SimpleComp {
var allCount = Bindings.createIntegerBinding( var allCount = Bindings.createIntegerBinding(
() -> { () -> {
return (int) model.getFileList().getAll().getValue().stream() return model.getFileList().getAll().getValue().size();
.filter(entry -> !entry.isSynthetic())
.count();
}, },
model.getFileList().getAll()); model.getFileList().getAll());
var selectedComp = new LabelComp(Bindings.createStringBinding( var selectedComp = new LabelComp(Bindings.createStringBinding(

View file

@ -1,5 +1,6 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import io.xpipe.app.browser.file.BrowserFileTransferMode;
import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.comp.base.LoadingOverlayComp; import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.core.AppI18n; 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.impl.*;
import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileNames;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.geometry.Insets; import javafx.geometry.Insets;
@ -21,7 +19,6 @@ import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode; import javafx.scene.input.TransferMode;
import javafx.scene.layout.AnchorPane; import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import java.io.File; import java.io.File;
@ -50,25 +47,21 @@ public class BrowserTransferComp extends SimpleComp {
var backgroundStack = var backgroundStack =
new StackComp(List.of(background)).grow(true, true).styleClass("download-background"); 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( var list = new BrowserSelectionListComp(
binding, binding,
entry -> Bindings.createStringBinding( entry -> Bindings.createStringBinding(
() -> { () -> {
var sourceItem = syncItems.stream() var sourceItem = syncItems.stream()
.filter(item -> item.getFileEntry() == entry) .filter(item -> item.getBrowserEntry() == entry)
.findAny(); .findAny();
if (sourceItem.isEmpty()) { if (sourceItem.isEmpty()) {
return "?"; return "?";
} }
var name = var name = entry.getModel() == null || sourceItem.get().downloadFinished().get()
sourceItem.get().downloadFinished().get()
? "Local" ? "Local"
: DataStorage.get() : entry.getModel().getFileSystemModel().getName();
.getStoreDisplayName(entry.getFileSystem() return entry.getFileName() + " (" + name + ")";
.getStore())
.orElse("?");
return FileNames.getFileName(entry.getPath()) + " (" + name + ")";
}, },
syncAllDownloaded)) syncAllDownloaded))
.apply(struc -> struc.get().setMinHeight(150)) .apply(struc -> struc.get().setMinHeight(150))
@ -154,11 +147,11 @@ public class BrowserTransferComp extends SimpleComp {
} }
var selected = syncItems.stream() var selected = syncItems.stream()
.map(BrowserTransferModel.Item::getFileEntry) .map(item -> item.getBrowserEntry())
.toList(); .toList();
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY); Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
var cc = BrowserClipboard.startDrag(null, selected); var cc = BrowserClipboard.startDrag(null, selected, BrowserFileTransferMode.NORMAL);
if (cc == null) { if (cc == null) {
return; return;
} }

View file

@ -1,14 +1,14 @@
package io.xpipe.app.browser; 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.fs.OpenFileSystemModel;
import io.xpipe.app.browser.session.BrowserSessionModel; import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ShellTemp; 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.binding.Bindings;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property; import javafx.beans.property.Property;
@ -17,7 +17,6 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableBooleanValue; import javafx.beans.value.ObservableBooleanValue;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import lombok.Value; import lombok.Value;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
@ -66,9 +65,9 @@ public class BrowserTransferModel {
items.clear(); items.clear();
} }
public void drop(OpenFileSystemModel model, List<FileSystem.FileEntry> entries) { public void drop(OpenFileSystemModel model, List<BrowserEntry> entries) {
entries.forEach(entry -> { entries.forEach(entry -> {
var name = FileNames.getFileName(entry.getPath()); var name = entry.getFileName();
if (items.stream().anyMatch(item -> item.getName().equals(name))) { if (items.stream().anyMatch(item -> item.getName().equals(name))) {
return; return;
} }
@ -89,14 +88,14 @@ public class BrowserTransferModel {
try { try {
var paths = entries.stream().map(File::toPath).filter(Files::exists).toList(); var paths = entries.stream().map(File::toPath).filter(Files::exists).toList();
for (Path path : paths) { for (Path path : paths) {
var entry = FileSystemHelper.getLocal(path); var entry = LocalFileSystem.getLocalBrowserEntry(path);
var name = entry.getName(); var name = entry.getFileName();
if (items.stream().anyMatch(item -> item.getName().equals(name))) { if (items.stream().anyMatch(item -> item.getName().equals(name))) {
return; return;
} }
var item = new Item(null, name, entry, path); 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); items.add(item);
} }
} catch (Exception ex) { } catch (Exception ex) {
@ -127,16 +126,17 @@ public class BrowserTransferModel {
} }
try { try {
try (var b = new BooleanScope(downloading).start()) { try (var ignored = new BooleanScope(downloading).start()) {
FileSystemHelper.dropFilesInto( var op = new BrowserFileTransferOperation(
FileSystemHelper.getLocal(TEMP), LocalFileSystem.getLocalFileEntry(TEMP),
List.of(item.getFileEntry()), List.of(item.getBrowserEntry().getRawFileEntry()),
true, BrowserFileTransferMode.COPY,
false, false,
progress -> { progress -> {
item.getProgress().setValue(progress); item.getProgress().setValue(progress);
item.getOpenFileSystemModel().getProgress().setValue(progress); item.getOpenFileSystemModel().getProgress().setValue(progress);
}); });
op.execute();
} }
} catch (Throwable t) { } catch (Throwable t) {
ErrorEvent.fromThrowable(t).handle(); ErrorEvent.fromThrowable(t).handle();
@ -151,15 +151,15 @@ public class BrowserTransferModel {
public static class Item { public static class Item {
OpenFileSystemModel openFileSystemModel; OpenFileSystemModel openFileSystemModel;
String name; String name;
FileSystem.FileEntry fileEntry; BrowserEntry browserEntry;
Path localFile; Path localFile;
Property<BrowserTransferProgress> progress; Property<BrowserTransferProgress> progress;
public Item( public Item(
OpenFileSystemModel openFileSystemModel, String name, FileSystem.FileEntry fileEntry, Path localFile) { OpenFileSystemModel openFileSystemModel, String name, BrowserEntry browserEntry, Path localFile) {
this.openFileSystemModel = openFileSystemModel; this.openFileSystemModel = openFileSystemModel;
this.name = name; this.name = name;
this.fileEntry = fileEntry; this.browserEntry = browserEntry;
this.localFile = localFile; this.localFile = localFile;
this.progress = new SimpleObjectProperty<>(); this.progress = new SimpleObjectProperty<>();
} }

View file

@ -2,26 +2,45 @@ package io.xpipe.app.browser;
import lombok.Value; import lombok.Value;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
@Value @Value
public class BrowserTransferProgress { public class BrowserTransferProgress {
String name; String name;
long transferred; long transferred;
long total; long total;
Instant start;
public static BrowserTransferProgress empty() { 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) { 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) { 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() { public boolean done() {
return transferred >= total; 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);
}
} }

View file

@ -32,8 +32,7 @@ public final class BrowserContextMenu extends ContextMenu {
? selected.stream() ? selected.stream()
.map(browserEntry -> new BrowserEntry( .map(browserEntry -> new BrowserEntry(
browserEntry.getRawFileEntry().resolved(), browserEntry.getRawFileEntry().resolved(),
browserEntry.getModel(), browserEntry.getModel()))
browserEntry.isSynthetic()))
.toList() .toList()
: selected; : selected;
} }
@ -44,7 +43,7 @@ public final class BrowserContextMenu extends ContextMenu {
var empty = source == null; var empty = source == null;
var selected = new ArrayList<>( var selected = new ArrayList<>(
empty empty
? List.of(new BrowserEntry(model.getCurrentDirectory(), model.getFileList(), false)) ? List.of(new BrowserEntry(model.getCurrentDirectory(), model.getFileList()))
: model.getFileList().getSelection()); : model.getFileList().getSelection());
if (source != null && !selected.contains(source)) { if (source != null && !selected.contains(source)) {
selected.add(source); selected.add(source);

View file

@ -13,14 +13,12 @@ public class BrowserEntry {
private final BrowserFileListModel model; private final BrowserFileListModel model;
private final FileSystem.FileEntry rawFileEntry; private final FileSystem.FileEntry rawFileEntry;
private final boolean synthetic;
private final BrowserIconFileType fileType; private final BrowserIconFileType fileType;
private final BrowserIconDirectoryType directoryType; private final BrowserIconDirectoryType directoryType;
public BrowserEntry(FileSystem.FileEntry rawFileEntry, BrowserFileListModel model, boolean synthetic) { public BrowserEntry(FileSystem.FileEntry rawFileEntry, BrowserFileListModel model) {
this.rawFileEntry = rawFileEntry; this.rawFileEntry = rawFileEntry;
this.model = model; this.model = model;
this.synthetic = synthetic;
this.fileType = fileType(rawFileEntry); this.fileType = fileType(rawFileEntry);
this.directoryType = directoryType(rawFileEntry); this.directoryType = directoryType(rawFileEntry);
} }
@ -52,6 +50,17 @@ public class BrowserEntry {
return null; 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() { public String getFileName() {
return getRawFileEntry().getName(); return getRawFileEntry().getName();
@ -61,9 +70,4 @@ public class BrowserEntry {
var n = getFileName(); var n = getFileName();
return FileNames.quoteIfNecessary(n); return FileNames.quoteIfNecessary(n);
} }
public String getOptionallyQuotedFilePath() {
var n = rawFileEntry.getPath();
return FileNames.quoteIfNecessary(n);
}
} }

View file

@ -1,7 +1,8 @@
package io.xpipe.app.browser.file; 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.action.BrowserAction;
import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.comp.base.LazyTextFieldComp; import io.xpipe.app.comp.base.LazyTextFieldComp;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp; 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.FileKind;
import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileSystem;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.*; import javafx.beans.property.*;
@ -31,16 +31,11 @@ import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.control.skin.TableViewSkin; import javafx.scene.control.skin.TableViewSkin;
import javafx.scene.control.skin.VirtualFlow; import javafx.scene.control.skin.VirtualFlow;
import javafx.scene.input.DragEvent; import javafx.scene.input.*;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import java.time.Instant; import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.ArrayList; import java.util.ArrayList;
@ -82,7 +77,7 @@ public final class BrowserFileListComp extends SimpleComp {
: null)); : null));
filenameCol.setComparator(Comparator.comparing(String::toLowerCase)); filenameCol.setComparator(Comparator.comparing(String::toLowerCase));
filenameCol.setSortType(ASCENDING); filenameCol.setSortType(ASCENDING);
filenameCol.setCellFactory(col -> new FilenameCell(fileList.getEditing())); filenameCol.setCellFactory(col -> new FilenameCell(fileList.getEditing(), col.getTableView()));
var sizeCol = new TableColumn<BrowserEntry, Number>(); var sizeCol = new TableColumn<BrowserEntry, Number>();
sizeCol.textProperty().bind(AppI18n.observable("size")); sizeCol.textProperty().bind(AppI18n.observable("size"));
@ -137,29 +132,7 @@ public final class BrowserFileListComp extends SimpleComp {
table.getSelectionModel().setCellSelectionEnabled(false); table.getSelectionModel().setCellSelectionEnabled(false);
table.getSelectionModel().getSelectedItems().addListener((ListChangeListener<? super BrowserEntry>) c -> { table.getSelectionModel().getSelectedItems().addListener((ListChangeListener<? super BrowserEntry>) c -> {
var toSelect = new ArrayList<>(c.getList()); fileList.getSelection().setAll(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().addListener((ListChangeListener<? super BrowserEntry>) c -> { fileList.getSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
@ -174,7 +147,6 @@ public final class BrowserFileListComp extends SimpleComp {
} }
var indices = c.getList().stream() var indices = c.getList().stream()
.skip(1)
.mapToInt(entry -> table.getItems().indexOf(entry)) .mapToInt(entry -> table.getItems().indexOf(entry))
.toArray(); .toArray();
table.getSelectionModel() table.getSelectionModel()
@ -196,9 +168,9 @@ public final class BrowserFileListComp extends SimpleComp {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
browserAction.execute(fileList.getFileSystemModel(), selected); browserAction.execute(fileList.getFileSystemModel(), selected);
}); });
});
event.consume(); event.consume();
}); });
});
} }
private void prepareTableEntries(TableView<BrowserEntry> table) { private void prepareTableEntries(TableView<BrowserEntry> table) {
@ -276,10 +248,6 @@ public final class BrowserFileListComp extends SimpleComp {
}, },
null, null,
() -> { () -> {
if (row.getItem() != null && row.getItem().isSynthetic()) {
return null;
}
return new BrowserContextMenu(fileList.getFileSystemModel(), row.getItem()); return new BrowserContextMenu(fileList.getFileSystemModel(), row.getItem());
}) })
.augment(new SimpleCompStructure<>(row)); .augment(new SimpleCompStructure<>(row));
@ -348,12 +316,10 @@ public final class BrowserFileListComp extends SimpleComp {
TableColumn<BrowserEntry, String> modeCol) { TableColumn<BrowserEntry, String> modeCol) {
var lastDir = new SimpleObjectProperty<FileSystem.FileEntry>(); var lastDir = new SimpleObjectProperty<FileSystem.FileEntry>();
Runnable updateHandler = () -> { Runnable updateHandler = () -> {
PlatformThread.runLaterIfNeeded(() -> { Platform.runLater(() -> {
var newItems = new ArrayList<>(fileList.getShown().getValue()); var newItems = new ArrayList<>(fileList.getShown().getValue());
var hasModifiedDate = newItems.size() == 0 var hasModifiedDate = newItems.size() == 0 || newItems.stream().anyMatch(entry -> entry.getRawFileEntry().getDate() != null);
|| newItems.stream()
.anyMatch(entry -> entry.getRawFileEntry().getDate() != null);
if (!hasModifiedDate) { if (!hasModifiedDate) {
table.getColumns().remove(mtimeCol); table.getColumns().remove(mtimeCol);
} else { } else {
@ -363,10 +329,7 @@ public final class BrowserFileListComp extends SimpleComp {
} }
if (fileList.getFileSystemModel().getFileSystem() != null) { if (fileList.getFileSystemModel().getFileSystem() != null) {
var shell = fileList.getFileSystemModel() var shell = fileList.getFileSystemModel().getFileSystem().getShell().orElseThrow();
.getFileSystem()
.getShell()
.orElseThrow();
var hasAttributes = !OsType.WINDOWS.equals(shell.getOsType()); var hasAttributes = !OsType.WINDOWS.equals(shell.getOsType());
if (!hasAttributes) { if (!hasAttributes) {
table.getColumns().remove(modeCol); table.getColumns().remove(modeCol);
@ -388,10 +351,8 @@ public final class BrowserFileListComp extends SimpleComp {
if (!Objects.equals(lastDir.get(), currentDirectory)) { if (!Objects.equals(lastDir.get(), currentDirectory)) {
TableViewSkin<?> skin = (TableViewSkin<?>) table.getSkin(); TableViewSkin<?> skin = (TableViewSkin<?>) table.getSkin();
if (skin != null) { if (skin != null) {
VirtualFlow<?> flow = VirtualFlow<?> flow = (VirtualFlow<?>) skin.getChildren().get(1);
(VirtualFlow<?>) skin.getChildren().get(1); ScrollBar vbar = (ScrollBar) flow.getChildrenUnmodifiable().get(2);
ScrollBar vbar =
(ScrollBar) flow.getChildrenUnmodifiable().get(2);
if (vbar.getValue() != 0.0) { if (vbar.getValue() != 0.0) {
table.scrollTo(0); table.scrollTo(0);
} }
@ -496,7 +457,7 @@ public final class BrowserFileListComp extends SimpleComp {
private final BooleanProperty updating = new SimpleBooleanProperty(); private final BooleanProperty updating = new SimpleBooleanProperty();
public FilenameCell(Property<BrowserEntry> editing) { public FilenameCell(Property<BrowserEntry> editing, TableView<BrowserEntry> tableView) {
accessibleTextProperty() accessibleTextProperty()
.bind(Bindings.createStringBinding( .bind(Bindings.createStringBinding(
() -> { () -> {
@ -526,6 +487,10 @@ public final class BrowserFileListComp extends SimpleComp {
itemProperty()) itemProperty())
.not() .not()
.not()) .not())
.focusTraversable(false)
.apply(struc -> struc.get().focusedProperty().addListener((observable, oldValue, newValue) -> {
getTableRow().requestFocus();
}))
.createRegion(); .createRegion();
editing.addListener((observable, oldValue, newValue) -> { editing.addListener((observable, oldValue, newValue) -> {
@ -554,6 +519,16 @@ public final class BrowserFileListComp extends SimpleComp {
HBox.setHgrow(textField, Priority.ALWAYS); HBox.setHgrow(textField, Priority.ALWAYS);
graphic.setAlignment(Pos.CENTER_LEFT); graphic.setAlignment(Pos.CENTER_LEFT);
setGraphic(graphic); 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 @Override
@ -573,15 +548,7 @@ public final class BrowserFileListComp extends SimpleComp {
// Visibility seems to be bugged, so use opacity // Visibility seems to be bugged, so use opacity
setOpacity(0.0); setOpacity(0.0);
} else { } else {
var isParentLink = getTableRow() img.set(getTableRow().getItem().getIcon());
.getItem()
.getRawFileEntry()
.equals(fileList.getFileSystemModel().getCurrentParentDirectory());
img.set(FileIconManager.getFileIcon(
isParentLink
? fileList.getFileSystemModel().getCurrentDirectory()
: getTableRow().getItem().getRawFileEntry(),
isParentLink));
var isDirectory = getTableRow().getItem().getRawFileEntry().getKind() == FileKind.DIRECTORY; var isDirectory = getTableRow().getItem().getRawFileEntry().getKind() == FileKind.DIRECTORY;
pseudoClassStateChanged(FOLDER, isDirectory); pseudoClassStateChanged(FOLDER, isDirectory);
@ -594,9 +561,8 @@ public final class BrowserFileListComp extends SimpleComp {
.resolved() .resolved()
.getPath() .getPath()
: getTableRow().getItem().getFileName(); : getTableRow().getItem().getFileName();
var fileName = isParentLink ? ".." : normalName; var fileName = normalName;
var hidden = !isParentLink var hidden = getTableRow().getItem().getRawFileEntry().isHidden() || fileName.startsWith(".");
&& (getTableRow().getItem().getRawFileEntry().isHidden() || fileName.startsWith("."));
getTableRow().pseudoClassStateChanged(HIDDEN, hidden); getTableRow().pseudoClassStateChanged(HIDDEN, hidden);
text.set(fileName); text.set(fileName);
// Visibility seems to be bugged, so use opacity // Visibility seems to be bugged, so use opacity

View file

@ -3,13 +3,11 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.browser.BrowserClipboard; import io.xpipe.app.browser.BrowserClipboard;
import io.xpipe.app.browser.BrowserSelectionListComp; import io.xpipe.app.browser.BrowserSelectionListComp;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileKind;
import javafx.geometry.Point2D; import javafx.geometry.Point2D;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.TableView; import javafx.scene.control.TableView;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.input.*; import javafx.scene.input.*;
import lombok.Getter; import lombok.Getter;
import java.io.File; import java.io.File;
@ -44,11 +42,13 @@ public class BrowserFileListCompEntry {
// Only clear for normal clicks // Only clear for normal clicks
if (t.isStillSincePress()) { if (t.isStillSincePress()) {
model.getSelection().clear(); model.getSelection().clear();
tv.requestFocus();
} }
t.consume(); t.consume();
return; return;
} }
row.requestFocus();
if (t.getClickCount() == 2 && t.getButton() == MouseButton.PRIMARY) { if (t.getClickCount() == 2 && t.getButton() == MouseButton.PRIMARY) {
model.onDoubleClick(item); model.onDoubleClick(item);
t.consume(); t.consume();
@ -58,7 +58,7 @@ public class BrowserFileListCompEntry {
} }
public void onMouseShiftClick(MouseEvent t) { public void onMouseShiftClick(MouseEvent t) {
if (isSynthetic()) { if (t.getButton() != MouseButton.PRIMARY) {
return; return;
} }
@ -87,11 +87,6 @@ public class BrowserFileListCompEntry {
t.consume(); t.consume();
} }
public boolean isSynthetic() {
return item != null
&& item.getRawFileEntry().equals(model.getFileSystemModel().getCurrentParentDirectory());
}
private boolean acceptsDrop(DragEvent event) { private boolean acceptsDrop(DragEvent event) {
// Accept drops from outside the app window // Accept drops from outside the app window
if (event.getGestureSource() == null) { if (event.getGestureSource() == null) {
@ -109,7 +104,7 @@ public class BrowserFileListCompEntry {
if (!Objects.equals( if (!Objects.equals(
model.getFileSystemModel().getFileSystem(), model.getFileSystemModel().getFileSystem(),
cb.getEntries().getFirst().getFileSystem())) { cb.getEntries().getFirst().getRawFileEntry().getFileSystem())) {
return true; return true;
} }
@ -123,7 +118,7 @@ public class BrowserFileListCompEntry {
} }
// Prevent dropping items onto themselves // Prevent dropping items onto themselves
if (item != null && cb.getEntries().contains(item.getRawFileEntry())) { if (item != null && cb.getEntries().contains(item)) {
return false; return false;
} }
@ -157,7 +152,7 @@ public class BrowserFileListCompEntry {
var target = item != null && item.getRawFileEntry().getKind() == FileKind.DIRECTORY var target = item != null && item.getRawFileEntry().getKind() == FileKind.DIRECTORY
? item.getRawFileEntry() ? item.getRawFileEntry()
: model.getFileSystemModel().getCurrentDirectory(); : 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.setDropCompleted(true);
event.consume(); event.consume();
} }
@ -174,17 +169,16 @@ public class BrowserFileListCompEntry {
public void startDrag(MouseEvent event) { public void startDrag(MouseEvent event) {
if (item == null) { if (item == null) {
row.startFullDrag();
return; return;
} }
if (isSynthetic()) { if (event.getButton() != MouseButton.PRIMARY) {
return; return;
} }
var selected = model.getSelectedRaw(); var selected = model.getSelection();
Dragboard db = row.startDragAndDrop(TransferMode.COPY); 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); Image image = BrowserSelectionListComp.snapshot(selected);
db.setDragView(image, -20, 15); db.setDragView(image, -20, 15);
@ -224,7 +218,7 @@ public class BrowserFileListCompEntry {
model.getFileSystemModel().cdAsync(item.getRawFileEntry().getPath()); model.getFileSystemModel().cdAsync(item.getRawFileEntry().getPath());
} }
}; };
DROP_TIMER.schedule(activeTask, 1000); DROP_TIMER.schedule(activeTask, 1200);
} }
public void onDragEntered(DragEvent event) { public void onDragEntered(DragEvent event) {
@ -244,7 +238,7 @@ public class BrowserFileListCompEntry {
return; return;
} }
if (item == null || item.isSynthetic()) { if (item == null) {
return; return;
} }

View file

@ -59,10 +59,7 @@ public final class BrowserFileListModel {
public void setAll(Stream<FileSystem.FileEntry> newFiles) { public void setAll(Stream<FileSystem.FileEntry> newFiles) {
try (var s = newFiles) { try (var s = newFiles) {
var parent = fileSystemModel.getCurrentParentDirectory(); var l = s.filter(entry -> entry != null).map(entry -> new BrowserEntry(entry, this))
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)))
.toList(); .toList();
all.setValue(l); all.setValue(l);
refreshShown(); refreshShown();
@ -94,14 +91,13 @@ public final class BrowserFileListModel {
} }
public Comparator<BrowserEntry> order() { public Comparator<BrowserEntry> order() {
var syntheticFirst = Comparator.<BrowserEntry, Boolean>comparing(path -> !path.isSynthetic());
var dirsFirst = Comparator.<BrowserEntry, Boolean>comparing( var dirsFirst = Comparator.<BrowserEntry, Boolean>comparing(
path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY); path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
var comp = comparatorProperty.getValue(); var comp = comparatorProperty.getValue();
Comparator<BrowserEntry> us = comp != null Comparator<BrowserEntry> us = comp != null
? syntheticFirst.thenComparing(dirsFirst).thenComparing(comp) ? dirsFirst.thenComparing(comp)
: syntheticFirst.thenComparing(dirsFirst); : dirsFirst;
return us; return us;
} }

View file

@ -37,7 +37,7 @@ public class BrowserFileOverviewComp extends SimpleComp {
var graphic = new HorizontalComp(List.of( var graphic = new HorizontalComp(List.of(
icon, icon,
new BrowserQuickAccessButtonComp( new BrowserQuickAccessButtonComp(
() -> new BrowserEntry(entry, model.getFileList(), false), model))); () -> new BrowserEntry(entry, model.getFileList()), model)));
var l = new Button(entry.getPath(), graphic.createRegion()); var l = new Button(entry.getPath(), graphic.createRegion());
l.setGraphicTextGap(1); l.setGraphicTextGap(1);
l.setOnAction(event -> { l.setOnAction(event -> {

View file

@ -0,0 +1,8 @@
package io.xpipe.app.browser.file;
public enum BrowserFileTransferMode {
NORMAL,
COPY,
MOVE
}

View file

@ -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<FileSystem.FileEntry> files;
private final BrowserFileTransferMode transferMode;
private final boolean checkConflicts;
private final Consumer<BrowserTransferProgress> progress;
BrowserAlerts.FileConflictChoice lastConflictChoice;
public BrowserFileTransferOperation(FileSystem.FileEntry target, List<FileSystem.FileEntry> files, BrowserFileTransferMode transferMode, boolean checkConflicts,
Consumer<BrowserTransferProgress> progress
) {
this.target = target;
this.files = files;
this.transferMode = transferMode;
this.checkConflicts = checkConflicts;
this.progress = progress;
}
public static BrowserFileTransferOperation ofLocal(FileSystem.FileEntry target, List<Path> files, BrowserFileTransferMode transferMode, boolean checkConflicts, Consumer<BrowserTransferProgress> 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<FileSystem.FileEntry, String>();
// 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<FileSystem.FileEntry> 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));
}
}
}

View file

@ -101,7 +101,7 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
newItems.add(empty); newItems.add(empty);
} else { } else {
var browserEntries = list.stream() var browserEntries = list.stream()
.map(fileEntry -> new BrowserEntry(fileEntry, model.getFileList(), false)) .map(fileEntry -> new BrowserEntry(fileEntry, model.getFileList()))
.toList(); .toList();
var menus = browserEntries.stream() var menus = browserEntries.stream()
.sorted(model.getFileList().order()) .sorted(model.getFileList().order())

View file

@ -1,28 +1,17 @@
package io.xpipe.app.browser.file; package io.xpipe.app.browser.file;
import io.xpipe.app.browser.BrowserTransferProgress;
import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.process.OsType; 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.time.Instant;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
public class FileSystemHelper { public class FileSystemHelper {
private static final int DEFAULT_BUFFER_SIZE = 1024;
private static FileSystem localFileSystem;
public static String adjustPath(OpenFileSystemModel model, String path) { public static String adjustPath(OpenFileSystemModel model, String path) {
if (path == null) { if (path == null) {
return 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 { public static FileSystem.FileEntry getRemoteWrapper(FileSystem fileSystem, String file) throws Exception {
return new FileSystem.FileEntry( return new FileSystem.FileEntry(
fileSystem, fileSystem,
@ -163,24 +135,6 @@ public class FileSystemHelper {
fileSystem.directoryExists(file) ? FileKind.DIRECTORY : FileKind.FILE); fileSystem.directoryExists(file) ? FileKind.DIRECTORY : FileKind.FILE);
} }
public static void dropLocalFilesInto(
FileSystem.FileEntry entry,
List<Path> files,
Consumer<BrowserTransferProgress> 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<FileSystem.FileEntry> files) { public static void delete(List<FileSystem.FileEntry> files) {
if (files.isEmpty()) { if (files.isEmpty()) {
return; return;
@ -194,255 +148,4 @@ public class FileSystemHelper {
} }
} }
} }
public static void dropFilesInto(
FileSystem.FileEntry target,
List<FileSystem.FileEntry> files,
boolean explicitCopy,
boolean checkConflicts,
Consumer<BrowserTransferProgress> 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<BrowserAlerts.FileConflictChoice> 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<BrowserAlerts.FileConflictChoice> 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<BrowserTransferProgress> progress,
AtomicReference<BrowserAlerts.FileConflictChoice> 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<FileSystem.FileEntry, String>();
// 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<FileSystem.FileEntry> 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<BrowserAlerts.FileConflictChoice> 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<BrowserTransferProgress> 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()));
}
}
} }

View file

@ -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);
}
}

View file

@ -4,6 +4,8 @@ import io.xpipe.app.browser.BrowserSavedState;
import io.xpipe.app.browser.BrowserTransferProgress; import io.xpipe.app.browser.BrowserTransferProgress;
import io.xpipe.app.browser.action.BrowserAction; import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.file.BrowserFileListModel; 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.file.FileSystemHelper;
import io.xpipe.app.browser.session.BrowserAbstractSessionModel; import io.xpipe.app.browser.session.BrowserAbstractSessionModel;
import io.xpipe.app.browser.session.BrowserSessionModel; 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.process.ShellOpenFunction;
import io.xpipe.core.store.*; import io.xpipe.core.store.*;
import io.xpipe.core.util.FailableConsumer; import io.xpipe.core.util.FailableConsumer;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.*; import javafx.beans.property.*;
import lombok.Getter; import lombok.Getter;
import lombok.SneakyThrows; import lombok.SneakyThrows;
@ -343,14 +343,16 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
} }
startIfNeeded(); startIfNeeded();
FileSystemHelper.dropLocalFilesInto(entry, files, progress::setValue, true); var op = BrowserFileTransferOperation.ofLocal(entry, files,BrowserFileTransferMode.COPY,true, progress::setValue);
op.execute();
refreshSync(); refreshSync();
}); });
}); });
} }
public void dropFilesIntoAsync( public void dropFilesIntoAsync(
FileSystem.FileEntry target, List<FileSystem.FileEntry> files, boolean explicitCopy) { FileSystem.FileEntry target, List<FileSystem.FileEntry> files, BrowserFileTransferMode mode
) {
// We don't have to do anything in this case // We don't have to do anything in this case
if (files.isEmpty()) { if (files.isEmpty()) {
return; return;
@ -363,9 +365,8 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
} }
startIfNeeded(); startIfNeeded();
FileSystemHelper.dropFilesInto(target, files, explicitCopy, true, browserTransferProgress -> { var op = new BrowserFileTransferOperation(target, files, mode,true, progress::setValue);
progress.setValue(browserTransferProgress); op.execute();
});
refreshSync(); refreshSync();
}); });
}); });

View file

@ -389,7 +389,7 @@ public abstract class StoreEntryComp extends SimpleComp {
.getSortedCategories(wrapper.getCategory().getValue().getRoot()) .getSortedCategories(wrapper.getCategory().getValue().getRoot())
.forEach(storeCategoryWrapper -> { .forEach(storeCategoryWrapper -> {
MenuItem m = new MenuItem(); MenuItem m = new MenuItem();
m.textProperty().bind(storeCategoryWrapper.nameProperty()); m.textProperty().setValue(" ".repeat(storeCategoryWrapper.getDepth()) + storeCategoryWrapper.getName().getValue());
m.setOnAction(event -> { m.setOnAction(event -> {
wrapper.moveTo(storeCategoryWrapper.getCategory()); wrapper.moveTo(storeCategoryWrapper.getCategory());
event.consume(); event.consume();

View file

@ -6,21 +6,17 @@ import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.prefs.SupportedLocale;
import io.xpipe.app.util.OptionsBuilder; import io.xpipe.app.util.OptionsBuilder;
import io.xpipe.app.util.Translatable; import io.xpipe.app.util.Translatable;
import io.xpipe.core.util.ModuleHelper; import io.xpipe.core.util.ModuleHelper;
import io.xpipe.core.util.XPipeInstallation; import io.xpipe.core.util.XPipeInstallation;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.Value; import lombok.Value;
import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FilenameUtils;
import org.ocpsoft.prettytime.PrettyTime;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@ -131,7 +127,7 @@ public class AppI18n {
} }
} }
private LoadedTranslations getLoaded() { public LoadedTranslations getLoaded() {
return currentLanguage.getValue() != null ? currentLanguage.getValue() : english; return currentLanguage.getValue() != null ? currentLanguage.getValue() : english;
} }
@ -278,21 +274,15 @@ public class AppI18n {
}); });
} }
var prettyTime = new PrettyTime( return new LoadedTranslations(l, translations, markdownDocumentations);
AppPrefs.get() != null
? AppPrefs.get().language().getValue().getLocale()
: SupportedLocale.getEnglish().getLocale());
return new LoadedTranslations(l, translations, markdownDocumentations, prettyTime);
} }
@Value @Value
static class LoadedTranslations { public static class LoadedTranslations {
Locale locale; Locale locale;
Map<String, String> translations; Map<String, String> translations;
Map<String, String> markdownDocumentations; Map<String, String> markdownDocumentations;
PrettyTime prettyTime;
} }
@SuppressWarnings("removal") @SuppressWarnings("removal")

View file

@ -1,5 +1,6 @@
package io.xpipe.app.core.mode; package io.xpipe.app.core.mode;
import io.xpipe.app.browser.file.LocalFileSystem;
import io.xpipe.app.browser.icon.FileIconManager; import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.core.App; import io.xpipe.app.core.App;
import io.xpipe.app.core.AppGreetings; import io.xpipe.app.core.AppGreetings;
@ -49,9 +50,13 @@ public class GuiMode extends PlatformMode {
}); });
TrackEvent.info("Window setup complete"); TrackEvent.info("Window setup complete");
ThreadHelper.runAsync(() -> { // Can be loaded async
ThreadHelper.runFailableAsync(() -> {
FileIconManager.loadIfNecessary(); FileIconManager.loadIfNecessary();
}); });
ThreadHelper.runFailableAsync(() -> {
LocalFileSystem.init();
});
UpdateChangelogAlert.showIfNeeded(); UpdateChangelogAlert.showIfNeeded();
} }

View file

@ -120,7 +120,6 @@ public abstract class DataStorage {
} }
private synchronized void dispose() { private synchronized void dispose() {
onReset();
save(true); save(true);
} }
@ -183,8 +182,6 @@ public abstract class DataStorage {
}); });
} }
protected void onReset() {}
protected Path getStoresDir() { protected Path getStoresDir() {
return dir.resolve("stores"); return dir.resolve("stores");
} }

View file

@ -2,6 +2,7 @@ package io.xpipe.app.util;
import java.text.CharacterIterator; import java.text.CharacterIterator;
import java.text.StringCharacterIterator; import java.text.StringCharacterIterator;
import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
@ -80,4 +81,13 @@ public final class HumanReadableFormat {
private static int getWeekNumber(LocalDateTime date) { private static int getWeekNumber(LocalDateTime date) {
return date.get(WeekFields.of(Locale.getDefault()).weekOfYear()); 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();
}
} }

View file

@ -49,7 +49,6 @@ open module io.xpipe.app {
requires org.slf4j; requires org.slf4j;
requires org.slf4j.jdk.platform.logging; requires org.slf4j.jdk.platform.logging;
requires atlantafx.base; requires atlantafx.base;
requires org.ocpsoft.prettytime;
requires com.vladsch.flexmark; requires com.vladsch.flexmark;
requires com.fasterxml.jackson.core; requires com.fasterxml.jackson.core;
requires com.fasterxml.jackson.databind; requires com.fasterxml.jackson.databind;

View file

@ -103,7 +103,6 @@ project.ext {
authors = 'Christopher Schnick' authors = 'Christopher Schnick'
javafxVersion = '22.0.1' javafxVersion = '22.0.1'
platformName = getPlatformName() platformName = getPlatformName()
artifactChecksums = new HashMap<String, String>()
languages = ["en", "nl", "es", "fr", "de", "it", "pt", "ru", "ja", "zh", "tr", "da"] languages = ["en", "nl", "es", "fr", "de", "it", "pt", "ru", "ja", "zh", "tr", "da"]
jvmRunArgs = [ jvmRunArgs = [
"--add-opens", "java.base/java.lang=io.xpipe.app", "--add-opens", "java.base/java.lang=io.xpipe.app",

View file

@ -22,7 +22,7 @@ public class CopyAction implements LeafAction {
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) { public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) {
BrowserClipboard.startCopy( BrowserClipboard.startCopy(
model.getCurrentDirectory(), model.getCurrentDirectory(),
entries.stream().map(entry -> entry.getRawFileEntry()).toList()); entries);
} }
@Override @Override

View file

@ -3,6 +3,7 @@ package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.BrowserClipboard; import io.xpipe.app.browser.BrowserClipboard;
import io.xpipe.app.browser.action.LeafAction; import io.xpipe.app.browser.action.LeafAction;
import io.xpipe.app.browser.file.BrowserEntry; 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.browser.fs.OpenFileSystemModel;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileKind;
@ -34,7 +35,7 @@ public class PasteAction implements LeafAction {
return; return;
} }
model.dropFilesIntoAsync(target, files, true); model.dropFilesIntoAsync(target, files.stream().map(browserEntry -> browserEntry.getRawFileEntry()).toList(), BrowserFileTransferMode.COPY);
} }
@Override @Override