diff --git a/LICENSE.md b/LICENSE.md index a71e62a8f..a820047ce 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -187,7 +187,8 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2024 Christopher Schnick +Copyright 2023 Christopher Schnick +Copyright 2023 XPipe UG (haftungsbeschränkt) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 444c543e8..b13d7aff6 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,9 @@ are not able to resolve and install any dependency packages. ### RHEL-based distros +The rpm releases are signed with the GPG key https://xpipe.io/signatures/crschnick.asc. +You can import it via `rpm --import https://xpipe.io/signatures/crschnick.asc` to allow your rpm-based package manager to verify the release signature. + The following rpm installers are available: - [Linux .rpm Installer (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-linux-x86_64.rpm) diff --git a/app/build.gradle b/app/build.gradle index 28760a4e1..6aa569d65 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ dependencies { api project(':beacon') compileOnly 'org.hamcrest:hamcrest:3.0' - compileOnly 'org.junit.jupiter:junit-jupiter-api:5.11.3' - compileOnly 'org.junit.jupiter:junit-jupiter-params:5.11.3' + compileOnly 'org.junit.jupiter:junit-jupiter-api:5.11.4' + compileOnly 'org.junit.jupiter:junit-jupiter-params:5.11.4' api 'com.vladsch.flexmark:flexmark:0.64.8' api 'com.vladsch.flexmark:flexmark-util:0.64.8' @@ -56,10 +56,10 @@ dependencies { exclude group: 'org.apache.commons', module: 'commons-lang3' } api 'org.apache.commons:commons-lang3:3.17.0' - api 'io.sentry:sentry:7.18.0' + api 'io.sentry:sentry:7.20.0' api 'commons-io:commons-io:2.18.0' - api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.18.1" - api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.18.1" + api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.18.2" + api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.18.2" api group: 'org.kordamp.ikonli', name: 'ikonli-material2-pack', version: "12.2.0" api group: 'org.kordamp.ikonli', name: 'ikonli-materialdesign2-pack', version: "12.2.0" api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0" @@ -105,6 +105,9 @@ run { workingDir = rootDir jvmArgs += ['-XX:+EnableDynamicAgentLoading'] + + def exts = files(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0].outputs.files.singleFile).toList()); + classpath += exts } task runAttachedDebugger(type: JavaExec) { @@ -120,6 +123,9 @@ task runAttachedDebugger(type: JavaExec) { ) jvmArgs += ['-XX:+EnableDynamicAgentLoading'] systemProperties run.systemProperties + + def exts = files(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0].outputs.files.singleFile).toList()); + classpath += exts } processResources { diff --git a/app/src/main/java/io/xpipe/app/Main.java b/app/src/main/java/io/xpipe/app/Main.java index 39716c264..1f87356fb 100644 --- a/app/src/main/java/io/xpipe/app/Main.java +++ b/app/src/main/java/io/xpipe/app/Main.java @@ -7,7 +7,7 @@ public class Main { public static void main(String[] args) { if (args.length == 1 && args[0].equals("version")) { - AppProperties.init(); + AppProperties.init(args); System.out.println(AppProperties.get().getVersion()); return; } diff --git a/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java b/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java index 0a391696d..43a9cfc04 100644 --- a/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java @@ -96,7 +96,15 @@ public class BeaconRequestHandler implements HttpHandler { } } } - response = beaconInterface.handle(exchange, object); + + var sync = beaconInterface.getSynchronizationObject(); + if (sync != null) { + synchronized (sync) { + response = beaconInterface.handle(exchange, object); + } + } else { + response = beaconInterface.handle(exchange, object); + } } catch (BeaconClientException clientException) { ErrorEvent.fromThrowable(clientException).omit().expected().handle(); writeError(exchange, new BeaconClientErrorResponse(clientException.getMessage()), 400); @@ -193,7 +201,7 @@ public class BeaconRequestHandler implements HttpHandler { && method.getParameters()[0].getType().equals(byte[].class)) .findFirst() .orElseThrow(); - setMethod.invoke(b, s); + setMethod.invoke(b, (Object) s); var m = b.getClass().getDeclaredMethod("build"); m.setAccessible(true); diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java index 4213c0c7c..b6972cee1 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java @@ -1,11 +1,14 @@ package io.xpipe.app.beacon.impl; +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.prefs.ExternalApplicationType; import io.xpipe.app.terminal.TerminalView; import io.xpipe.app.util.AskpassAlert; import io.xpipe.app.util.SecretManager; import io.xpipe.app.util.SecretQueryState; import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.api.AskpassExchange; +import io.xpipe.core.process.OsType; import com.sun.net.httpserver.HttpExchange; @@ -50,17 +53,24 @@ public class AskpassExchangeImpl extends AskpassExchange { } var term = TerminalView.get().getTerminalInstances().stream() - .filter(instance -> - instance.getTerminalProcess().equals(found.get().getTerminal())) + .filter(instance -> instance.equals(found.get().getTerminal())) .findFirst(); if (term.isEmpty()) { return; } var control = term.get().controllable(); - control.ifPresent(controllableTerminalSession -> { - controllableTerminalSession.focus(); - }); + if (control.isPresent()) { + control.get().focus(); + } else { + if (OsType.getLocal() == OsType.MACOS) { + // Just focus the app, this is correct most of the time + var terminalType = AppPrefs.get().terminalType().getValue(); + if (terminalType instanceof ExternalApplicationType.MacApplication m) { + m.focus(); + } + } + } } @Override diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/CategoryAddExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/CategoryAddExchangeImpl.java new file mode 100644 index 000000000..09eeb872c --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/impl/CategoryAddExchangeImpl.java @@ -0,0 +1,34 @@ +package io.xpipe.app.beacon.impl; + +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreCategory; +import io.xpipe.beacon.BeaconClientException; +import io.xpipe.beacon.api.CategoryAddExchange; + +import com.sun.net.httpserver.HttpExchange; + +public class CategoryAddExchangeImpl extends CategoryAddExchange { + + @Override + public Object handle(HttpExchange exchange, Request msg) throws Throwable { + if (DataStorage.get().getStoreCategoryIfPresent(msg.getParent()).isEmpty()) { + throw new BeaconClientException("Parent category with id " + msg.getParent() + " does not exist"); + } + + if (DataStorage.get().getStoreCategories().stream() + .anyMatch(dataStoreCategory -> msg.getParent().equals(dataStoreCategory.getParentCategory()) + && msg.getName().equals(dataStoreCategory.getName()))) { + throw new BeaconClientException( + "Category with name " + msg.getName() + " already exists in parent category"); + } + + var cat = DataStoreCategory.createNew(msg.getParent(), msg.getName()); + DataStorage.get().addStoreCategory(cat); + return Response.builder().category(cat.getUuid()).build(); + } + + @Override + public Object getSynchronizationObject() { + return DataStorage.get(); + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java index 7cf0c747f..05390a17c 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java @@ -3,6 +3,7 @@ package io.xpipe.app.beacon.impl; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.api.ConnectionAddExchange; import io.xpipe.core.util.ValidationException; @@ -17,7 +18,17 @@ public class ConnectionAddExchangeImpl extends ConnectionAddExchange { return Response.builder().connection(found.get().getUuid()).build(); } + if (msg.getCategory() != null + && DataStorage.get() + .getStoreCategoryIfPresent(msg.getCategory()) + .isEmpty()) { + throw new BeaconClientException("Category with id " + msg.getCategory() + " does not exist"); + } + var entry = DataStoreEntry.createNew(msg.getName(), msg.getData()); + if (msg.getCategory() != null) { + entry.setCategoryUuid(msg.getCategory()); + } try { DataStorage.get().addStoreEntryInProgress(entry); if (msg.getValidate()) { @@ -35,6 +46,22 @@ public class ConnectionAddExchangeImpl extends ConnectionAddExchange { DataStorage.get().removeStoreEntryInProgress(entry); } DataStorage.get().addStoreEntryIfNotPresent(entry); + + // Explicitly assign category + if (msg.getCategory() != null) { + DataStorage.get() + .updateCategory( + entry, + DataStorage.get() + .getStoreCategoryIfPresent(msg.getCategory()) + .orElseThrow()); + } + return Response.builder().connection(entry.getUuid()).build(); } + + @Override + public Object getSynchronizationObject() { + return DataStorage.get(); + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionBrowseExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionBrowseExchangeImpl.java index 5624122a1..d99ab55d5 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionBrowseExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionBrowseExchangeImpl.java @@ -24,4 +24,9 @@ public class ConnectionBrowseExchangeImpl extends ConnectionBrowseExchange { AppLayoutModel.get().selectBrowser(); return Response.builder().build(); } + + @Override + public Object getSynchronizationObject() { + return DataStorage.get(); + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionInfoExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionInfoExchangeImpl.java index a4c454b63..a7e519bf9 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionInfoExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionInfoExchangeImpl.java @@ -56,19 +56,8 @@ public class ConnectionInfoExchangeImpl extends ConnectionInfoExchange { return Response.builder().infos(list).build(); } - private Class toWrapper(Class clazz) { - if (!clazz.isPrimitive()) return clazz; - - if (clazz == Integer.TYPE) return Integer.class; - if (clazz == Long.TYPE) return Long.class; - if (clazz == Boolean.TYPE) return Boolean.class; - if (clazz == Byte.TYPE) return Byte.class; - if (clazz == Character.TYPE) return Character.class; - if (clazz == Float.TYPE) return Float.class; - if (clazz == Double.TYPE) return Double.class; - if (clazz == Short.TYPE) return Short.class; - if (clazz == Void.TYPE) return Void.class; - - return clazz; + @Override + public Object getSynchronizationObject() { + return DataStorage.get(); } } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionQueryExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionQueryExchangeImpl.java index f0f3e65d1..58df33e30 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionQueryExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionQueryExchangeImpl.java @@ -56,6 +56,11 @@ public class ConnectionQueryExchangeImpl extends ConnectionQueryExchange { .build(); } + @Override + public Object getSynchronizationObject() { + return DataStorage.get(); + } + private String toRegex(String pattern) { // https://stackoverflow.com/a/17369948/6477761 StringBuilder sb = new StringBuilder(pattern.length()); diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRefreshExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRefreshExchangeImpl.java index 5fa336528..1d83efda1 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRefreshExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRefreshExchangeImpl.java @@ -21,4 +21,9 @@ public class ConnectionRefreshExchangeImpl extends ConnectionRefreshExchange { } return Response.builder().build(); } + + @Override + public Object getSynchronizationObject() { + return DataStorage.get(); + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRemoveExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRemoveExchangeImpl.java index ed7c65907..1fb6d4176 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRemoveExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRemoveExchangeImpl.java @@ -24,4 +24,9 @@ public class ConnectionRemoveExchangeImpl extends ConnectionRemoveExchange { DataStorage.get().deleteWithChildren(entries.toArray(DataStoreEntry[]::new)); return Response.builder().build(); } + + @Override + public Object getSynchronizationObject() { + return DataStorage.get(); + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionTerminalExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionTerminalExchangeImpl.java index d88fa48e1..a4cbb4007 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionTerminalExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionTerminalExchangeImpl.java @@ -22,4 +22,9 @@ public class ConnectionTerminalExchangeImpl extends ConnectionTerminalExchange { TerminalLauncher.open(e, e.getName(), msg.getDirectory(), sc); return Response.builder().build(); } + + @Override + public Object getSynchronizationObject() { + return DataStorage.get(); + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionToggleExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionToggleExchangeImpl.java index 7d3f48dd1..6025ce0eb 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionToggleExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionToggleExchangeImpl.java @@ -24,4 +24,9 @@ public class ConnectionToggleExchangeImpl extends ConnectionToggleExchange { } return Response.builder().build(); } + + @Override + public Object getSynchronizationObject() { + return DataStorage.get(); + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java index 75f9e4b7f..0ff4917f7 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java @@ -1,24 +1,39 @@ package io.xpipe.app.beacon.impl; -import io.xpipe.app.core.launcher.LauncherInput; +import io.xpipe.app.core.AppOpenArguments; import io.xpipe.app.core.mode.OperationMode; -import io.xpipe.app.util.PlatformState; +import io.xpipe.app.util.PlatformInit; import io.xpipe.beacon.BeaconServerException; import io.xpipe.beacon.api.DaemonOpenExchange; +import io.xpipe.core.process.OsType; import com.sun.net.httpserver.HttpExchange; public class DaemonOpenExchangeImpl extends DaemonOpenExchange { + private int openCounter = 0; + @Override public Object handle(HttpExchange exchange, Request msg) throws BeaconServerException { if (msg.getArguments().isEmpty()) { - if (!OperationMode.switchToSyncIfPossible(OperationMode.GUI)) { - throw new BeaconServerException(PlatformState.getLastError()); + try { + // At this point we are already loading this on another thread + // so this call will only perform the waiting + PlatformInit.init(true); + } catch (Throwable t) { + throw new BeaconServerException(t); } - } - LauncherInput.handle(msg.getArguments()); + // The open command is used as a default opener on Linux + // We don't want to overwrite the default startup mode + if (OsType.getLocal() == OsType.LINUX && openCounter++ == 0) { + return Response.builder().build(); + } + + OperationMode.switchToAsync(OperationMode.GUI); + } else { + AppOpenArguments.handle(msg.getArguments()); + } return Response.builder().build(); } @@ -26,4 +41,9 @@ public class DaemonOpenExchangeImpl extends DaemonOpenExchange { public boolean requiresEnabledApi() { return false; } + + @Override + public boolean requiresCompletedStartup() { + return false; + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java index 6d18435b5..496d54dec 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java @@ -20,11 +20,10 @@ public class FsScriptExchangeImpl extends FsScriptExchange { try (var in = BlobManager.get().getBlob(msg.getBlob())) { data = new String(in.readAllBytes(), StandardCharsets.UTF_8); } + data = shell.getControl().getShellDialect().prepareScriptContent(data); var file = ScriptHelper.getExecScriptFile(shell.getControl()); - shell.getControl() - .getShellDialect() - .createScriptTextFileWriteCommand(shell.getControl(), data, file.toString()) - .execute(); + shell.getControl().view().writeScriptFile(file, data); + file = ScriptHelper.fixScriptPermissions(shell.getControl(), file); return Response.builder().path(file).build(); } } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java index b0f6c25c4..e332c2b70 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java @@ -30,6 +30,8 @@ import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import javafx.scene.shape.Rectangle; import javafx.stage.Stage; +import javafx.stage.Window; +import javafx.stage.WindowEvent; import java.util.List; import java.util.function.BiConsumer; @@ -50,8 +52,14 @@ public class BrowserFileChooserSessionComp extends DialogComp { public static void openSingleFile( Supplier> store, Consumer file, boolean save) { PlatformThread.runLaterIfNeeded(() -> { + var lastWindow = Window.getWindows().stream() + .filter(window -> window.isFocused()) + .findFirst(); var model = new BrowserFileChooserSessionModel(BrowserFileSystemTabModel.SelectionMode.SINGLE_FILE); DialogComp.showWindow(save ? "saveFileTitle" : "openFileTitle", stage -> { + stage.addEventFilter(WindowEvent.WINDOW_HIDDEN, event -> { + lastWindow.ifPresent(window -> window.requestFocus()); + }); var comp = new BrowserFileChooserSessionComp(stage, model); comp.apply(struc -> struc.get().setPrefSize(1200, 700)) .apply(struc -> AppFont.normal(struc.get())) @@ -116,7 +124,8 @@ public class BrowserFileChooserSessionComp extends DialogComp { var bookmarkTopBar = new BrowserConnectionListFilterComp(); var bookmarksList = new BrowserConnectionListComp( - BindingsHelper.map(model.getSelectedEntry(), v -> v.getEntry().get()), + BindingsHelper.map( + model.getSelectedEntry(), v -> v != null ? v.getEntry().get() : null), applicable, action, bookmarkTopBar.getCategory(), diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionModel.java index 37ad05d36..01988bfa9 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionModel.java @@ -34,20 +34,14 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel { - DEFAULT.openSync(tab, null); - DEFAULT.pinTab(tab); - }); + DEFAULT.openSync(tab, null); + DEFAULT.pinTab(tab); } } @@ -62,6 +56,10 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel { var current = selectedEntry.getValue(); + if (current == null) { + return null; + } + if (!current.isCloseable()) { return null; } @@ -176,6 +174,10 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel(sessionEntries); for (var o : all) { // Don't close busy connections gracefully @@ -242,7 +244,7 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel browserModel; - protected final String name; protected final Property splitTab = new SimpleObjectProperty<>(); - public BrowserSessionTab(BrowserAbstractSessionModel browserModel, String name) { + public BrowserSessionTab(BrowserAbstractSessionModel browserModel) { this.browserModel = browserModel; - this.name = name; } public abstract Comp comp(); @@ -31,6 +30,8 @@ public abstract class BrowserSessionTab { public abstract void close(); + public abstract ObservableValue getName(); + public abstract String getIcon(); public abstract DataColor getColor(); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserSessionTabsComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserSessionTabsComp.java index bf805dcf9..3d4556746 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserSessionTabsComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserSessionTabsComp.java @@ -276,7 +276,7 @@ public class BrowserSessionTabsComp extends SimpleComp { var cm = ContextMenuHelper.create(); if (tabModel.isCloseable()) { - var unpin = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("unpinTab")); + var unpin = ContextMenuHelper.item(LabelGraphic.none(), "unpinTab"); unpin.visibleProperty() .bind(PlatformThread.sync(Bindings.createBooleanBinding( () -> { @@ -290,7 +290,7 @@ public class BrowserSessionTabsComp extends SimpleComp { }); cm.getItems().add(unpin); - var pin = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("pinTab")); + var pin = ContextMenuHelper.item(LabelGraphic.none(), "pinTab"); pin.visibleProperty() .bind(PlatformThread.sync(Bindings.createBooleanBinding( () -> { @@ -304,7 +304,7 @@ public class BrowserSessionTabsComp extends SimpleComp { cm.getItems().add(pin); } - var select = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("selectTab")); + var select = ContextMenuHelper.item(LabelGraphic.none(), "selectTab"); select.acceleratorProperty() .bind(Bindings.createObjectBinding( () -> { @@ -325,7 +325,7 @@ public class BrowserSessionTabsComp extends SimpleComp { cm.getItems().add(new SeparatorMenuItem()); - var close = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeTab")); + var close = ContextMenuHelper.item(LabelGraphic.none(), "closeTab"); close.setAccelerator(new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN)); close.setOnAction(event -> { if (tab.isClosable()) { @@ -335,7 +335,7 @@ public class BrowserSessionTabsComp extends SimpleComp { }); cm.getItems().add(close); - var closeOthers = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeOtherTabs")); + var closeOthers = ContextMenuHelper.item(LabelGraphic.none(), "closeOtherTabs"); closeOthers.setOnAction(event -> { tabs.getTabs() .removeAll(tabs.getTabs().stream() @@ -345,7 +345,7 @@ public class BrowserSessionTabsComp extends SimpleComp { }); cm.getItems().add(closeOthers); - var closeLeft = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeLeftTabs")); + var closeLeft = ContextMenuHelper.item(LabelGraphic.none(), "closeLeftTabs"); closeLeft.setOnAction(event -> { var index = tabs.getTabs().indexOf(tab); tabs.getTabs() @@ -356,7 +356,7 @@ public class BrowserSessionTabsComp extends SimpleComp { }); cm.getItems().add(closeLeft); - var closeRight = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeRightTabs")); + var closeRight = ContextMenuHelper.item(LabelGraphic.none(), "closeRightTabs"); closeRight.setOnAction(event -> { var index = tabs.getTabs().indexOf(tab); tabs.getTabs() @@ -367,7 +367,7 @@ public class BrowserSessionTabsComp extends SimpleComp { }); cm.getItems().add(closeRight); - var closeAll = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeAllTabs")); + var closeAll = ContextMenuHelper.item(LabelGraphic.none(), "closeAllTabs"); closeAll.setAccelerator( new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN)); closeAll.setOnAction(event -> { @@ -425,13 +425,16 @@ public class BrowserSessionTabsComp extends SimpleComp { tab.textProperty() .bind(Bindings.createStringBinding( () -> { - return tabModel.getName() + var n = tabModel.getName().getValue(); + return (AppPrefs.get().censorMode().get() ? "*".repeat(n.length()) : n) + (global.getValue() == tabModel ? " (" + AppI18n.get("pinned") + ")" : ""); }, + tabModel.getName(), global, - AppPrefs.get().language())); + AppPrefs.get().language(), + AppPrefs.get().censorMode())); } else { - tab.setText(tabModel.getName()); + tab.textProperty().bind(tabModel.getName()); } Comp comp = tabModel.comp(); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserStoreSessionTab.java b/app/src/main/java/io/xpipe/app/browser/BrowserStoreSessionTab.java index a9320fa15..7842a30c3 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserStoreSessionTab.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserStoreSessionTab.java @@ -6,16 +6,26 @@ import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.core.store.DataStore; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ObservableValue; + import lombok.Getter; @Getter public abstract class BrowserStoreSessionTab extends BrowserSessionTab { protected final DataStoreEntryRef entry; + private final String name; public BrowserStoreSessionTab(BrowserAbstractSessionModel browserModel, DataStoreEntryRef entry) { - super(browserModel, DataStorage.get().getStoreEntryDisplayName(entry.get())); + super(browserModel); this.entry = entry; + this.name = DataStorage.get().getStoreEntryDisplayName(entry.get()); + } + + @Override + public ObservableValue getName() { + return new SimpleStringProperty(name); } public abstract Comp comp(); 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 78784f670..a8a4b08fc 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 @@ -4,7 +4,6 @@ import io.xpipe.app.browser.action.BrowserAction; import io.xpipe.app.comp.SimpleComp; import io.xpipe.app.core.AppI18n; import io.xpipe.app.util.*; -import io.xpipe.app.util.PlatformThread; import io.xpipe.core.process.OsType; import io.xpipe.core.store.FileEntry; import io.xpipe.core.store.FileInfo; @@ -29,10 +28,7 @@ import atlantafx.base.theme.Styles; import java.time.Duration; import java.time.Instant; import java.time.ZoneId; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.Objects; +import java.util.*; import java.util.concurrent.atomic.AtomicReference; import static io.xpipe.app.util.HumanReadableFormat.byteCount; @@ -283,12 +279,21 @@ public final class BrowserFileListComp extends SimpleComp { } try (var ignored = updateFromModel) { - fileList.getSelection().setAll(c.getList()); + // Attempt to preserve ordering. Works at least when selecting single entries + var existing = new HashSet<>(fileList.getSelection()); + c.getList().forEach(browserEntry -> { + if (!existing.contains(browserEntry)) { + fileList.getSelection().add(browserEntry); + } + }); + fileList.getSelection().removeIf(browserEntry -> !c.getList().contains(browserEntry)); } }); fileList.getSelection().addListener((ListChangeListener) c -> { - if (c.getList().equals(table.getSelectionModel().getSelectedItems())) { + var existing = new HashSet<>(fileList.getSelection()); + var toApply = new HashSet<>(c.getList()); + if (existing.equals(toApply)) { return; } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemCache.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemCache.java index 746f588c0..434e7ca4a 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemCache.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemCache.java @@ -1,5 +1,6 @@ package io.xpipe.app.browser.file; +import io.xpipe.app.util.PasswdFile; import io.xpipe.app.util.ShellControlCache; import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.OsType; @@ -16,7 +17,7 @@ public class BrowserFileSystemCache extends ShellControlCache { private final BrowserFileSystemTabModel model; private final String username; - private final Map users = new LinkedHashMap<>(); + private final PasswdFile passwdFile; private final Map groups = new LinkedHashMap<>(); public BrowserFileSystemCache(BrowserFileSystemTabModel model) throws Exception { @@ -27,16 +28,16 @@ public class BrowserFileSystemCache extends ShellControlCache { ShellDialect d = sc.getShellDialect(); // If there is no id command, we should still be fine with just assuming root username = d.printUsernameCommand(sc).readStdoutIfPossible().orElse("root"); - loadUsers(); + passwdFile = PasswdFile.parse(sc); loadGroups(); } + public Map getUsers() { + return passwdFile.getUsers(); + } + public int getUidForUser(String name) { - return users.entrySet().stream() - .filter(e -> e.getValue().equals(name)) - .findFirst() - .map(e -> e.getKey()) - .orElse(0); + return passwdFile.getUidForUser(name); } public int getGidForGroup(String name) { @@ -47,28 +48,6 @@ public class BrowserFileSystemCache extends ShellControlCache { .orElse(0); } - private void loadUsers() throws Exception { - var sc = model.getFileSystem().getShell().orElseThrow(); - if (sc.getOsType() == OsType.WINDOWS || sc.getOsType() == OsType.MACOS) { - return; - } - - var lines = sc.command(CommandBuilder.of().add("cat").addFile("/etc/passwd")) - .readStdoutIfPossible() - .orElse(""); - lines.lines().forEach(s -> { - var split = s.split(":"); - try { - users.putIfAbsent(Integer.parseInt(split[2]), split[0]); - } catch (Exception ignored) { - } - }); - - if (users.isEmpty()) { - users.put(0, "root"); - } - } - private void loadGroups() throws Exception { var sc = model.getFileSystem().getShell().orElseThrow(); if (sc.getOsType() == OsType.WINDOWS || sc.getOsType() == OsType.MACOS) { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHelper.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHelper.java index 9f289e7e2..0fd542e03 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHelper.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHelper.java @@ -51,15 +51,16 @@ public class BrowserFileSystemHelper { } var shell = model.getFileSystem().getShell(); - if (shell.isEmpty() || !shell.get().isRunning()) { + if (shell.isEmpty() || !shell.get().isRunning(true)) { return path; } try { - return shell.get() + var r = shell.get() .getShellDialect() .evaluateExpression(shell.get(), path) .readStdoutOrThrow(); + return !r.isBlank() ? r : null; } catch (Exception ex) { ErrorEvent.expected(ex); throw ex; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemSavedState.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemSavedState.java index 9040040c0..a7385c22a 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemSavedState.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemSavedState.java @@ -65,7 +65,7 @@ public class BrowserFileSystemSavedState { return state; } - public void save() { + public synchronized void save() { if (model == null) { return; } @@ -107,7 +107,7 @@ public class BrowserFileSystemSavedState { } } - private void updateRecent(String dir) { + private synchronized void updateRecent(String dir) { var without = FileNames.removeTrailingSlash(dir); var with = FileNames.toDirectory(dir); recentDirectories.removeIf(recentEntry -> diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabComp.java index 571036563..0d0ee4951 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabComp.java @@ -43,8 +43,7 @@ public class BrowserFileSystemTabComp extends SimpleComp { @Override protected Region createSimple() { - var alertOverlay = new ModalOverlayComp(Comp.of(() -> createContent()), model.getOverlay()); - return alertOverlay.createRegion(); + return createContent(); } private Region createContent() { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java index a68e4b891..d8f6f4d2b 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java @@ -5,7 +5,6 @@ import io.xpipe.app.browser.BrowserFullSessionModel; import io.xpipe.app.browser.BrowserStoreSessionTab; import io.xpipe.app.browser.action.BrowserAction; import io.xpipe.app.comp.Comp; -import io.xpipe.app.comp.base.ModalOverlayComp; import io.xpipe.app.core.window.AppMainWindow; import io.xpipe.app.ext.ProcessControlProvider; import io.xpipe.app.ext.ShellStore; @@ -44,10 +43,10 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab currentPath = new ReadOnlyObjectWrapper<>(); private final BrowserFileSystemHistory history = new BrowserFileSystemHistory(); - private final Property overlay = new SimpleObjectProperty<>(); private final BooleanProperty inOverview = new SimpleBooleanProperty(); private final Property progress = new SimpleObjectProperty<>(); private final ObservableList terminalRequests = FXCollections.observableArrayList(); + private final BooleanProperty transferCancelled = new SimpleBooleanProperty(); private FileSystem fileSystem; private BrowserFileSystemSavedState savedState; private BrowserFileSystemCache cache; @@ -65,6 +64,14 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab findFile(String path) { + return getFileList().getAll().getValue().stream() + .filter(browserEntry -> browserEntry.getFileName().equals(path) + || browserEntry.getRawFileEntry().getPath().equals(path)) + .findFirst() + .map(browserEntry -> browserEntry.getRawFileEntry()); + } + @Override public Comp comp() { return new BrowserFileSystemTabComp(this, true); @@ -142,6 +149,14 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab c, boolean refresh) { ThreadHelper.runFailableAsync(() -> { if (fileSystem == null) { @@ -384,7 +399,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab { BooleanScope.executeExclusive(busy, () -> { if (fileSystem == null) { @@ -491,10 +503,6 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab r, boolean refresh) { - if (name == null || name.isBlank()) { - return; - } - ThreadHelper.runFailableAsync(() -> { BooleanScope.executeExclusive(busy, () -> { if (fileSystem == null) { @@ -566,7 +574,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab progress; + private final BooleanProperty cancelled; BrowserAlerts.FileConflictChoice lastConflictChoice; @@ -28,12 +31,14 @@ public class BrowserFileTransferOperation { List files, BrowserFileTransferMode transferMode, boolean checkConflicts, - Consumer progress) { + Consumer progress, + BooleanProperty cancelled) { this.target = target; this.files = files; this.transferMode = transferMode; this.checkConflicts = checkConflicts; this.progress = progress; + this.cancelled = cancelled; } public static BrowserFileTransferOperation ofLocal( @@ -41,7 +46,8 @@ public class BrowserFileTransferOperation { List files, BrowserFileTransferMode transferMode, boolean checkConflicts, - Consumer progress) { + Consumer progress, + BooleanProperty cancelled) { var entries = files.stream() .map(path -> { if (!Files.exists(path)) { @@ -56,7 +62,7 @@ public class BrowserFileTransferOperation { }) .filter(entry -> entry != null) .toList(); - return new BrowserFileTransferOperation(target, entries, transferMode, checkConflicts, progress); + return new BrowserFileTransferOperation(target, entries, transferMode, checkConflicts, progress, cancelled); } private void updateProgress(BrowserTransferProgress progress) { @@ -112,12 +118,18 @@ public class BrowserFileTransferOperation { return BrowserAlerts.FileConflictChoice.REPLACE; } + private boolean cancelled() { + return cancelled.get(); + } + public void execute() throws Exception { if (files.isEmpty()) { updateProgress(null); return; } + cancelled.set(false); + var same = files.getFirst().getFileSystem().equals(target.getFileSystem()); var doesMove = transferMode == BrowserFileTransferMode.MOVE || (same && transferMode == BrowserFileTransferMode.NORMAL); @@ -129,6 +141,10 @@ public class BrowserFileTransferOperation { try { for (var file : files) { + if (cancelled()) { + break; + } + if (same) { handleSingleOnSameFileSystem(file); } else { @@ -138,6 +154,10 @@ public class BrowserFileTransferOperation { if (!same && doesMove) { for (var file : files) { + if (cancelled()) { + break; + } + deleteSingle(file); } } @@ -207,7 +227,7 @@ public class BrowserFileTransferOperation { var newFile = targetFile.getParent().join(matcher.group(1) + " (" + (number + 1) + ")." + matcher.group(3)); return newFile.toString(); - } catch (NumberFormatException e) { + } catch (NumberFormatException ignored) { } } @@ -242,6 +262,10 @@ public class BrowserFileTransferOperation { var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath())); List list = source.getFileSystem().listFilesRecursively(source.getPath()); for (FileEntry fileEntry : list) { + if (cancelled()) { + return; + } + var rel = FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath())); flatFiles.put(fileEntry, rel); if (fileEntry.getKind() == FileKind.FILE) { @@ -264,6 +288,10 @@ public class BrowserFileTransferOperation { var start = Instant.now(); AtomicLong transferred = new AtomicLong(); for (var e : flatFiles.entrySet()) { + if (cancelled()) { + return; + } + var sourceFile = e.getKey(); var fixedRelPath = new FilePath(e.getValue()) .fileSystemCompatible( @@ -298,6 +326,10 @@ public class BrowserFileTransferOperation { private void transfer( FileEntry sourceFile, String targetFile, AtomicLong transferred, AtomicLong totalSize, Instant start) throws Exception { + if (cancelled()) { + return; + } + InputStream inputStream = null; OutputStream outputStream = null; try { @@ -377,7 +409,7 @@ public class BrowserFileTransferOperation { AtomicLong transferred, AtomicLong total, Instant start) - throws IOException { + throws Exception { // Initialize progress immediately prior to reading anything updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start)); @@ -385,9 +417,48 @@ public class BrowserFileTransferOperation { byte[] buffer = new byte[bs]; int read; while ((read = inputStream.read(buffer, 0, bs)) > 0) { + if (cancelled()) { + killStreams(); + break; + } + + if (!checkTransferValidity()) { + killStreams(); + break; + } + outputStream.write(buffer, 0, read); transferred.addAndGet(read); updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start)); } } + + private boolean checkTransferValidity() { + var sourceFs = files.getFirst().getFileSystem(); + var targetFs = target.getFileSystem(); + var same = files.getFirst().getFileSystem().equals(target.getFileSystem()); + if (!same) { + var sourceShell = sourceFs.getShell().orElseThrow(); + var targetShell = targetFs.getShell().orElseThrow(); + return !sourceShell.getStdout().isClosed() + && !targetShell.getStdin().isClosed(); + } else { + return true; + } + } + + private void killStreams() throws Exception { + var sourceFs = files.getFirst().getFileSystem(); + var targetFs = target.getFileSystem(); + var same = files.getFirst().getFileSystem().equals(target.getFileSystem()); + if (!same) { + var sourceShell = sourceFs.getShell().orElseThrow(); + var targetShell = targetFs.getShell().orElseThrow(); + try { + sourceShell.closeStdout(); + } finally { + targetShell.closeStdin(); + } + } + } } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserHistoryTabComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserHistoryTabComp.java index 57aa3fce3..018833398 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserHistoryTabComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserHistoryTabComp.java @@ -8,10 +8,10 @@ import io.xpipe.app.comp.base.HorizontalComp; import io.xpipe.app.comp.base.LabelComp; import io.xpipe.app.comp.base.ListBoxViewComp; import io.xpipe.app.comp.base.PrettyImageHelper; -import io.xpipe.app.comp.base.PrettySvgComp; import io.xpipe.app.comp.base.TileButtonComp; import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.BindingsHelper; import io.xpipe.app.util.DerivedObservableList; @@ -20,7 +20,6 @@ import io.xpipe.app.util.ThreadHelper; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleStringProperty; import javafx.geometry.Insets; import javafx.geometry.Orientation; import javafx.geometry.Pos; @@ -52,7 +51,7 @@ public class BrowserHistoryTabComp extends SimpleComp { var vbox = new VBox(welcome, new Spacer(4, Orientation.VERTICAL)); vbox.setAlignment(Pos.CENTER_LEFT); - var img = new PrettySvgComp(new SimpleStringProperty("graphics/Hips.svg"), 50, 75) + var img = PrettyImageHelper.ofSpecificFixedSize("graphics/Hips.svg", 50, 61) .padding(new Insets(5, 0, 0, 0)) .createRegion(); @@ -148,17 +147,20 @@ public class BrowserHistoryTabComp extends SimpleComp { var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid()); var graphic = entry.get().getEffectiveIconFile(); var view = PrettyImageHelper.ofFixedSize(graphic, 22, 16); - return new ButtonComp( - new SimpleStringProperty(DataStorage.get().getStoreEntryDisplayName(entry.get())), - view.createRegion(), - () -> { - ThreadHelper.runAsync(() -> { - var storageEntry = DataStorage.get().getStoreEntryIfPresent(e.getUuid()); - if (storageEntry.isPresent()) { - model.openFileSystemAsync(storageEntry.get().ref(), null, disable); - } - }); - }) + var name = Bindings.createStringBinding( + () -> { + var n = DataStorage.get().getStoreEntryDisplayName(entry.get()); + return AppPrefs.get().censorMode().get() ? "*".repeat(n.length()) : n; + }, + AppPrefs.get().censorMode()); + return new ButtonComp(name, view.createRegion(), () -> { + ThreadHelper.runAsync(() -> { + var storageEntry = DataStorage.get().getStoreEntryIfPresent(e.getUuid()); + if (storageEntry.isPresent()) { + model.openFileSystemAsync(storageEntry.get().ref(), null, disable); + } + }); + }) .minWidth(300) .accessibleText(DataStorage.get().getStoreEntryDisplayName(entry.get())) .disable(disable) @@ -168,7 +170,13 @@ public class BrowserHistoryTabComp extends SimpleComp { } private Comp dirButton(BrowserHistorySavedState.Entry e, BooleanProperty disable) { - return new ButtonComp(new SimpleStringProperty(e.getPath()), null, () -> { + var name = Bindings.createStringBinding( + () -> { + var n = e.getPath(); + return AppPrefs.get().censorMode().get() ? "*".repeat(n.length()) : n; + }, + AppPrefs.get().censorMode()); + return new ButtonComp(name, () -> { ThreadHelper.runAsync(() -> { model.restoreStateAsync(e, disable); }); diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserHistoryTabModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserHistoryTabModel.java index 0a585eee1..eeee7072b 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserHistoryTabModel.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserHistoryTabModel.java @@ -7,10 +7,12 @@ import io.xpipe.app.comp.Comp; import io.xpipe.app.core.AppI18n; import io.xpipe.app.storage.DataColor; +import javafx.beans.value.ObservableValue; + public final class BrowserHistoryTabModel extends BrowserSessionTab { public BrowserHistoryTabModel(BrowserAbstractSessionModel browserModel) { - super(browserModel, " " + AppI18n.get("history") + " "); + super(browserModel); } @Override @@ -24,11 +26,16 @@ public final class BrowserHistoryTabModel extends BrowserSessionTab { } @Override - public void init() throws Exception {} + public void init() {} @Override public void close() {} + @Override + public ObservableValue getName() { + return AppI18n.observable("history").map(s -> " " + s + " "); + } + @Override public String getIcon() { return null; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java index 67fcbd893..13a34a747 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java @@ -118,8 +118,10 @@ public class BrowserNavBarComp extends Comp { path.addListener((observable, oldValue, newValue) -> { ThreadHelper.runFailableAsync(() -> { BooleanScope.executeExclusive(model.getBusy(), () -> { - var changed = model.cdSyncOrRetry(newValue, true); - changed.ifPresent(s -> Platform.runLater(() -> path.set(s))); + var changed = model.cdSyncOrRetry(newValue != null && !newValue.isBlank() ? newValue : null, true); + changed.ifPresent(s -> { + Platform.runLater(() -> path.set(!s.isBlank() ? s : null)); + }); }); }); }); @@ -148,8 +150,6 @@ public class BrowserNavBarComp extends Comp { INVISIBLE, !val && !struc.get().isFocused()); }); }); - - struc.get().setPromptText("Overview of " + model.getName()); }) .accessibleText("Current path"); return pathBar; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserOverviewComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserOverviewComp.java index 3b9091fa2..536a2852c 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserOverviewComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserOverviewComp.java @@ -60,20 +60,20 @@ public class BrowserOverviewComp extends SimpleComp { }); }); var commonOverview = new BrowserFileOverviewComp(model, commonPlatform, false); - var commonPane = new SimpleTitledPaneComp(AppI18n.observable("common"), commonOverview) + var commonPane = new SimpleTitledPaneComp(AppI18n.observable("common"), commonOverview, false) .apply(struc -> VBox.setVgrow(struc.get(), Priority.NEVER)); var roots = model.getFileSystem().listRoots().stream() .map(s -> FileEntry.ofDirectory(model.getFileSystem(), s)) .toList(); var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots), false); - var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview); + var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview, false); var recent = new DerivedObservableList<>(model.getSavedState().getRecentDirectories(), true) .mapped(s -> FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory())) .getList(); var recentOverview = new BrowserFileOverviewComp(model, recent, true); - var recentPane = new SimpleTitledPaneComp(AppI18n.observable("recent"), recentOverview); + var recentPane = new SimpleTitledPaneComp(AppI18n.observable("recent"), recentOverview, false); var vbox = new VerticalComp(List.of(recentPane, commonPane, rootsPane)).styleClass("overview"); var r = vbox.createRegion(); 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 5d4c028f1..8239646f9 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 @@ -312,7 +312,10 @@ public class BrowserQuickAccessContextMenu extends ContextMenu { browserActionMenu.show(menu.getStyleableNode(), Side.RIGHT, 0, 0); shownBrowserActionsMenu = browserActionMenu; Platform.runLater(() -> { - browserActionMenu.getItems().getFirst().getStyleableNode().requestFocus(); + var items = browserActionMenu.getItems(); + if (items.size() > 0) { + items.getFirst().getStyleableNode().requestFocus(); + } }); } } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java index f61f79963..af565eda5 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java @@ -5,10 +5,13 @@ import io.xpipe.app.comp.SimpleComp; import io.xpipe.app.comp.SimpleCompStructure; import io.xpipe.app.comp.augment.ContextMenuAugment; import io.xpipe.app.comp.base.HorizontalComp; +import io.xpipe.app.comp.base.IconButtonComp; import io.xpipe.app.comp.base.LabelComp; import io.xpipe.app.core.AppFont; import io.xpipe.app.util.BindingsHelper; import io.xpipe.app.util.HumanReadableFormat; +import io.xpipe.app.util.PlatformThread; +import io.xpipe.app.util.ThreadHelper; import javafx.beans.binding.Bindings; import javafx.geometry.Pos; @@ -36,7 +39,8 @@ public class BrowserStatusBarComp extends SimpleComp { createProgressEstimateStatus(), Comp.hspacer(), createClipboardStatus(), - createSelectionStatus())); + createSelectionStatus(), + createKillButton())); bar.spacing(15); bar.styleClass("status-bar"); @@ -50,6 +54,33 @@ public class BrowserStatusBarComp extends SimpleComp { return r; } + private Comp createKillButton() { + var button = new IconButtonComp("mdi2s-stop", () -> { + ThreadHelper.runAsync(() -> { + model.killTransfer(); + }); + }); + button.accessibleText("Kill").tooltipKey("killTransfer"); + var cancel = PlatformThread.sync(model.getTransferCancelled()); + var hide = Bindings.createBooleanBinding( + () -> { + if (model.getProgress().getValue() == null + || model.getProgress().getValue().done()) { + return true; + } + + if (cancel.getValue()) { + return true; + } + + return false; + }, + cancel, + model.getProgress()); + button.hide(hide); + return button; + } + private Comp createProgressEstimateStatus() { var text = BindingsHelper.map(model.getProgress(), p -> { if (p == null) { @@ -97,7 +128,7 @@ public class BrowserStatusBarComp extends SimpleComp { var progressComp = new LabelComp(text) .styleClass("progress") .apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)) - .prefWidth(180); + .hgrow(); return progressComp; } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserTerminalDockTabModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserTerminalDockTabModel.java index f94a9db5f..945a13f49 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserTerminalDockTabModel.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserTerminalDockTabModel.java @@ -17,6 +17,7 @@ import io.xpipe.app.util.ThreadHelper; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.value.ObservableBooleanValue; +import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; import java.util.Optional; @@ -35,7 +36,7 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab { BrowserAbstractSessionModel browserModel, BrowserSessionTab origin, ObservableList terminalRequests) { - super(browserModel, AppI18n.get("terminal")); + super(browserModel); this.origin = origin; this.terminalRequests = terminalRequests; } @@ -154,6 +155,11 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab { dockModel.onClose(); } + @Override + public ObservableValue getName() { + return AppI18n.observable("terminal"); + } + @Override public String getIcon() { return null; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferComp.java index 88b192ff2..6761e5cd5 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferComp.java @@ -13,12 +13,14 @@ import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; import javafx.css.PseudoClass; import javafx.geometry.Insets; +import javafx.scene.control.ContentDisplay; import javafx.scene.image.Image; import javafx.scene.input.ClipboardContent; import javafx.scene.input.DragEvent; import javafx.scene.input.Dragboard; import javafx.scene.input.TransferMode; import javafx.scene.layout.Region; +import javafx.scene.text.TextAlignment; import org.kordamp.ikonli.javafx.FontIcon; @@ -41,6 +43,8 @@ public class BrowserTransferComp extends SimpleComp { var background = new LabelComp(AppI18n.observable("transferDescription")) .apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline"))) .apply(struc -> struc.get().setWrapText(true)) + .apply(struc -> struc.get().setTextAlignment(TextAlignment.CENTER)) + .apply(struc -> struc.get().setContentDisplay(ContentDisplay.TOP)) .visible(model.getEmpty()); var backgroundStack = new StackComp(List.of(background)) .grow(true, true) diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferModel.java index 1e5cf1f2f..eeef401c7 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferModel.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferModel.java @@ -120,8 +120,8 @@ public class BrowserTransferModel { return; } - if (item.getOpenFileSystemModel() != null - && item.getOpenFileSystemModel().isClosed()) { + var itemModel = item.getOpenFileSystemModel(); + if (itemModel == null || itemModel.isClosed()) { return; } @@ -134,15 +134,16 @@ public class BrowserTransferModel { progress -> { // Don't update item progress to keep it as finished if (progress == null) { - item.getOpenFileSystemModel().getProgress().setValue(null); + itemModel.getProgress().setValue(null); return; } synchronized (item.getProgress()) { item.getProgress().setValue(progress); } - item.getOpenFileSystemModel().getProgress().setValue(progress); - }); + itemModel.getProgress().setValue(progress); + }, + itemModel.getTransferCancelled()); op.execute(); } catch (Throwable t) { ErrorEvent.fromThrowable(t).handle(); diff --git a/app/src/main/java/io/xpipe/app/comp/Comp.java b/app/src/main/java/io/xpipe/app/comp/Comp.java index 6d02a00ce..318f09e31 100644 --- a/app/src/main/java/io/xpipe/app/comp/Comp.java +++ b/app/src/main/java/io/xpipe/app/comp/Comp.java @@ -93,6 +93,18 @@ public abstract class Comp> { })); } + public void focusOnShow() { + onSceneAssign(struc -> { + Platform.runLater(() -> { + Platform.runLater(() -> { + Platform.runLater(() -> { + struc.get().requestFocus(); + }); + }); + }); + }); + } + public Comp minWidth(double width) { return apply(struc -> struc.get().setMinWidth(width)); } diff --git a/app/src/main/java/io/xpipe/app/comp/base/AppLayoutComp.java b/app/src/main/java/io/xpipe/app/comp/base/AppLayoutComp.java index 6a350f010..a35b229d3 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/AppLayoutComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/AppLayoutComp.java @@ -2,32 +2,35 @@ package io.xpipe.app.comp.base; import io.xpipe.app.comp.Comp; import io.xpipe.app.comp.CompStructure; -import io.xpipe.app.comp.SimpleCompStructure; import io.xpipe.app.comp.store.StoreViewState; import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.util.PlatformThread; import javafx.beans.binding.Bindings; import javafx.beans.value.ObservableValue; +import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.control.ButtonBase; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyEvent; import javafx.scene.layout.BorderPane; -import javafx.scene.layout.Pane; +import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.stream.Collectors; -public class AppLayoutComp extends Comp> { +public class AppLayoutComp extends Comp { private final AppLayoutModel model = AppLayoutModel.get(); @Override - public CompStructure createBase() { + public Structure createBase() { Map, ObservableValue> map = model.getEntries().stream() .filter(entry -> entry.comp() != null) .collect(Collectors.toMap( @@ -67,6 +70,28 @@ public class AppLayoutComp extends Comp> { }); AppFont.normal(pane); pane.getStyleClass().add("layout"); - return new SimpleCompStructure<>(pane); + return new Structure(pane, multiR, sidebarR, new ArrayList<>(multiR.getChildren())); + } + + public record Structure(BorderPane pane, StackPane stack, Region sidebar, List children) + implements CompStructure { + + public void prepareAddition() { + stack.getChildren().clear(); + sidebar.setDisable(true); + } + + public void show() { + for (var child : children) { + stack.getChildren().add(child); + PlatformThread.runNestedLoopIteration(); + } + sidebar.setDisable(false); + } + + @Override + public BorderPane get() { + return pane; + } } } diff --git a/app/src/main/java/io/xpipe/app/comp/base/AppMainWindowContentComp.java b/app/src/main/java/io/xpipe/app/comp/base/AppMainWindowContentComp.java new file mode 100644 index 000000000..3c98e64b2 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/base/AppMainWindowContentComp.java @@ -0,0 +1,122 @@ +package io.xpipe.app.comp.base; + +import io.xpipe.app.comp.Comp; +import io.xpipe.app.comp.SimpleComp; +import io.xpipe.app.core.AppFont; +import io.xpipe.app.core.AppProperties; +import io.xpipe.app.core.window.AppDialog; +import io.xpipe.app.core.window.AppMainWindow; +import io.xpipe.app.resources.AppImages; +import io.xpipe.app.resources.AppResources; +import io.xpipe.app.util.PlatformThread; +import io.xpipe.core.process.OsType; + +import javafx.animation.Animation; +import javafx.application.Platform; +import javafx.collections.ListChangeListener; +import javafx.geometry.Pos; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; +import javafx.stage.Window; + +import atlantafx.base.util.Animations; + +public class AppMainWindowContentComp extends SimpleComp { + + private final Stage stage; + + public AppMainWindowContentComp(Stage stage) { + this.stage = stage; + } + + @Override + protected Region createSimple() { + var overlay = AppDialog.getModalOverlay(); + var loaded = AppMainWindow.getLoadedContent(); + var bg = Comp.of(() -> { + var loadingIcon = new ImageView(); + loadingIcon.setFitWidth(64); + loadingIcon.setFitHeight(64); + + var anim = Animations.pulse(loadingIcon, 1.1); + if (OsType.getLocal() != OsType.LINUX) { + anim.setRate(0.85); + anim.setCycleCount(Animation.INDEFINITE); + anim.play(); + } + + // This allows for assigning logos even if AppImages has not been initialized yet + var dir = "img/logo/"; + AppResources.with(AppResources.XPIPE_MODULE, dir, path -> { + loadingIcon.setImage(AppImages.loadImage(path.resolve("loading.png"))); + }); + + var version = new LabelComp((AppProperties.get().isStaging() ? "XPipe PTB" : "XPipe") + " " + + AppProperties.get().getVersion()); + version.apply(struc -> { + AppFont.setSize(struc.get(), 1); + struc.get().setOpacity(0.6); + }); + + var text = new LabelComp(AppMainWindow.getLoadingText()); + text.apply(struc -> { + struc.get().setOpacity(0.8); + }); + + var vbox = new VBox( + Comp.vspacer().createRegion(), + loadingIcon, + Comp.vspacer(19).createRegion(), + version.createRegion(), + Comp.vspacer().createRegion(), + text.createRegion(), + Comp.vspacer(20).createRegion()); + vbox.setAlignment(Pos.CENTER); + + var pane = new StackPane(vbox); + pane.setAlignment(Pos.CENTER); + pane.getStyleClass().add("background"); + + loaded.subscribe(struc -> { + if (struc != null) { + PlatformThread.runNestedLoopIteration(); + struc.prepareAddition(); + anim.stop(); + pane.getChildren().add(struc.get()); + struc.show(); + pane.getChildren().remove(vbox); + pane.getStyleClass().remove("background"); + } + }); + + overlay.addListener((ListChangeListener) c -> { + if (c.next() && c.wasAdded()) { + stage.requestFocus(); + + // Close blocking modal windows + var childWindows = Window.getWindows().stream() + .filter(window -> window instanceof Stage s && stage.equals(s.getOwner())) + .toList(); + childWindows.forEach(window -> { + ((Stage) window).close(); + }); + } + }); + + loaded.addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + Platform.runLater(() -> { + stage.requestFocus(); + }); + } + }); + + return pane; + }); + var modal = new ModalOverlayStackComp(bg, overlay); + return modal.createRegion(); + } +} diff --git a/app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java b/app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java index eed6cedf5..aab6f8f50 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java @@ -3,9 +3,9 @@ package io.xpipe.app.comp.base; import io.xpipe.app.comp.Comp; import io.xpipe.app.comp.CompStructure; import io.xpipe.app.comp.SimpleCompStructure; +import io.xpipe.app.util.LabelGraphic; import io.xpipe.app.util.PlatformThread; -import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; import javafx.css.Size; @@ -13,14 +13,16 @@ import javafx.css.SizeUnits; import javafx.scene.Node; import javafx.scene.control.Button; +import lombok.AllArgsConstructor; import lombok.Getter; import org.kordamp.ikonli.javafx.FontIcon; @Getter +@AllArgsConstructor public class ButtonComp extends Comp> { private final ObservableValue name; - private final ObjectProperty graphic; + private final ObservableValue graphic; private final Runnable listener; public ButtonComp(ObservableValue name, Runnable listener) { @@ -31,33 +33,39 @@ public class ButtonComp extends Comp> { public ButtonComp(ObservableValue name, Node graphic, Runnable listener) { this.name = name; - this.graphic = new SimpleObjectProperty<>(graphic); + this.graphic = new SimpleObjectProperty<>(new LabelGraphic.NodeGraphic(() -> graphic)); this.listener = listener; } - public Node getGraphic() { - return graphic.get(); - } - - public ObjectProperty graphicProperty() { - return graphic; - } - @Override public CompStructure