Merge branch prefs into master

The changes have been squashed as the commit history and messages were not very carefully crafted. There isn't that much value in preserving random commit messages.

Also due to diverging branches, rebasing or merging it was difficult.
This commit is contained in:
crschnick 2024-02-28 07:36:31 +00:00
parent ce45ff9ec6
commit 3e7fbe89ac
442 changed files with 11614 additions and 6865 deletions

5
.gitignore vendored
View file

@ -7,7 +7,8 @@ lib/
dev.properties dev.properties
extensions.txt extensions.txt
dev_storage dev_storage
local*/ local/
local_*/
.vs .vs
.vscode .vscode
obj obj
@ -15,3 +16,5 @@ out
bin bin
.DS_Store .DS_Store
ComponentsGenerated.wxs ComponentsGenerated.wxs
!dist/javafx/**/lib
!dist/javafx/**/bin

View file

@ -18,7 +18,7 @@ There are no real formal contribution guidelines right now, they will maybe come
All XPipe components target [Java 21](https://openjdk.java.net/projects/jdk/20/) and make full use of the Java Module System (JPMS). All XPipe components target [Java 21](https://openjdk.java.net/projects/jdk/20/) and make full use of the Java Module System (JPMS).
All components are modularized, including all their dependencies. All components are modularized, including all their dependencies.
In case a dependency is (sadly) not modularized yet, module information is manually added using [moditect](https://github.com/moditect/moditect-gradle-plugin). In case a dependency is (sadly) not modularized yet, module information is manually added using [extra-java-module-info](https://github.com/gradlex-org/extra-java-module-info).
Further, note that as this is a pretty complicated Java project that fully utilizes modularity, Further, note that as this is a pretty complicated Java project that fully utilizes modularity,
many IDEs still have problems building this project properly. many IDEs still have problems building this project properly.

View file

@ -159,7 +159,7 @@ Alternatively, you can also use [Homebrew](https://github.com/xpipe-io/homebrew-
XPipe utilizes an open core model, which essentially means that the main application is open source while certain other components are not. Select parts are not open source yet, but may be added to this repository in the future. XPipe utilizes an open core model, which essentially means that the main application is open source while certain other components are not. Select parts are not open source yet, but may be added to this repository in the future.
This mainly concerns the features only available in the professional tier and the shell handling library implementation. Furthermore, some tests and especially test environments and that run on private servers are also not included in this repository. This mainly concerns the features only available in the professional tier and the shell handling library implementation. Furthermore, some CI pipelines and tests that run on private servers are also not included in this repository.
## More links ## More links

View file

@ -2,15 +2,11 @@ plugins {
id 'java-library' id 'java-library'
id 'maven-publish' id 'maven-publish'
id 'signing' id 'signing'
id "org.moditect.gradleplugin" version "1.0.0-rc3"
} }
apply from: "$rootDir/gradle/gradle_scripts/java.gradle" apply from: "$rootDir/gradle/gradle_scripts/java.gradle"
apply from: "$rootDir/gradle/gradle_scripts/junit.gradle" apply from: "$rootDir/gradle/gradle_scripts/junit.gradle"
System.setProperty('excludeExtensionLibrary', 'true')
apply from: "$rootDir/gradle/gradle_scripts/extension_test.gradle"
version = rootProject.versionString version = rootProject.versionString
group = 'io.xpipe' group = 'io.xpipe'
archivesBaseName = 'xpipe-api' archivesBaseName = 'xpipe-api'
@ -19,14 +15,14 @@ repositories {
mavenCentral() mavenCentral()
} }
test { dependencies {
enabled = false testImplementation project(':api')
} }
dependencies { dependencies {
api project(':core') api project(':core')
implementation project(':beacon') implementation project(':beacon')
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.15.2" implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.16.1"
} }
configurations { configurations {
@ -38,6 +34,5 @@ task dist(type: Copy) {
into "${project(':dist').buildDir}/dist/libraries" into "${project(':dist').buildDir}/dist/libraries"
} }
apply from: 'publish.gradle' apply from: 'publish.gradle'
apply from: "$rootDir/gradle/gradle_scripts/publish-base.gradle" apply from: "$rootDir/gradle/gradle_scripts/publish-base.gradle"

View file

@ -1,6 +1,6 @@
package io.xpipe.api.test; package io.xpipe.api.test;
import io.xpipe.beacon.BeaconDaemonController; import io.xpipe.beacon.test.BeaconDaemonController;
import io.xpipe.core.util.XPipeDaemonMode; import io.xpipe.core.util.XPipeDaemonMode;
import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;

View file

@ -0,0 +1,14 @@
package io.xpipe.api.test;
import io.xpipe.beacon.test.BeaconDaemonController;
import io.xpipe.core.util.XPipeDaemonMode;
import org.junit.jupiter.api.Test;
public class StartupTest {
@Test
public void test() throws Exception {
BeaconDaemonController.start(XPipeDaemonMode.TRAY);
BeaconDaemonController.stop();
}
}

View file

@ -1,131 +1,70 @@
plugins { plugins {
id 'application' id 'application'
id "org.moditect.gradleplugin" version "1.0.0-rc3" id 'jvm-test-suite'
id 'java-library'
} }
repositories { repositories {
mavenCentral() mavenCentral()
} }
configurations {
dep
}
apply from: "$rootDir/gradle/gradle_scripts/java.gradle" apply from: "$rootDir/gradle/gradle_scripts/java.gradle"
apply from: "$rootDir/gradle/gradle_scripts/javafx.gradle" apply from: "$rootDir/gradle/gradle_scripts/javafx.gradle"
apply from: "$projectDir/gradle_scripts/richtextfx.gradle"
apply from: "$rootDir/gradle/gradle_scripts/commons.gradle"
apply from: "$rootDir/gradle/gradle_scripts/prettytime.gradle"
apply from: "$projectDir/gradle_scripts/sentry.gradle"
apply from: "$rootDir/gradle/gradle_scripts/lombok.gradle" apply from: "$rootDir/gradle/gradle_scripts/lombok.gradle"
apply from: "$projectDir/gradle_scripts/github-api.gradle"
apply from: "$projectDir/gradle_scripts/flexmark.gradle"
apply from: "$rootDir/gradle/gradle_scripts/picocli.gradle"
apply from: "$rootDir/gradle/gradle_scripts/versioncompare.gradle"
apply from: "$rootDir/gradle/gradle_scripts/markdowngenerator.gradle"
configurations { configurations {
implementation.extendsFrom(dep) implementation.extendsFrom(javafx)
} }
dependencies { dependencies {
compileOnly project(':api') api project(':core')
implementation project(':core') api project(':beacon')
implementation project(':beacon')
compileOnly 'org.hamcrest:hamcrest:2.2' compileOnly 'org.hamcrest:hamcrest:2.2'
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.9.3' compileOnly 'org.junit.jupiter:junit-jupiter-api:5.10.2'
compileOnly 'org.junit.jupiter:junit-jupiter-params:5.9.3' compileOnly 'org.junit.jupiter:junit-jupiter-params:5.10.2'
implementation 'net.java.dev.jna:jna-jpms:5.13.0' api 'com.vladsch.flexmark:flexmark:0.64.0'
implementation 'net.java.dev.jna:jna-platform-jpms:5.13.0' api 'com.vladsch.flexmark:flexmark-util-data:0.64.0'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.15.2" api 'com.vladsch.flexmark:flexmark-util-ast:0.64.0'
implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: "2.15.2" api 'com.vladsch.flexmark:flexmark-util-builder:0.64.0'
implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.15.2" api 'com.vladsch.flexmark:flexmark-util-sequence:0.64.0'
implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jdk8', version: "2.15.2" api 'com.vladsch.flexmark:flexmark-util-misc:0.64.0'
implementation group: 'org.kordamp.ikonli', name: 'ikonli-material2-pack', version: "12.2.0" api 'com.vladsch.flexmark:flexmark-util-dependency:0.64.0'
implementation group: 'org.kordamp.ikonli', name: 'ikonli-materialdesign2-pack', version: "12.2.0" api 'com.vladsch.flexmark:flexmark-util-collection:0.64.0'
implementation group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0" api 'com.vladsch.flexmark:flexmark-util-format:0.64.0'
implementation group: 'org.kordamp.ikonli', name: 'ikonli-material-pack', version: "12.2.0" api 'com.vladsch.flexmark:flexmark-util-html:0.64.0'
implementation group: 'org.kordamp.ikonli', name: 'ikonli-feather-pack', version: "12.2.0" api 'com.vladsch.flexmark:flexmark-util-visitor:0.64.0'
implementation (name: 'preferencesfx-core-11.15.0')
implementation (group: 'com.dlsc.formsfx', name: 'formsfx-core', version: '11.6.0') { api files("$rootDir/gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar")
exclude group: 'org.openjfx', module: 'javafx-controls' api 'info.picocli:picocli:4.7.5'
exclude group: 'org.openjfx', module: 'javafx-fxml' api 'org.kohsuke:github-api:1.318'
} api 'io.sentry:sentry:7.3.0'
implementation group: 'org.slf4j', name: 'slf4j-api', version: '2.0.7' api 'org.ocpsoft.prettytime:prettytime:5.0.2.Final'
implementation 'io.xpipe:modulefs:0.1.4' api 'commons-io:commons-io:2.15.1'
implementation 'com.jfoenix:jfoenix:9.0.10' api 'net.java.dev.jna:jna-jpms:5.14.0'
implementation 'org.controlsfx:controlsfx:11.1.2' api 'net.java.dev.jna:jna-platform-jpms:5.14.0'
implementation 'net.synedra:validatorfx:0.4.2' api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.16.1"
implementation ('io.github.mkpaz:atlantafx-base:2.0.1') { api group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: "2.16.1"
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.16.1"
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jdk8', version: "2.16.1"
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"
api group: 'org.kordamp.ikonli', name: 'ikonli-material-pack', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-feather-pack', version: "12.2.0"
api group: 'org.slf4j', name: 'slf4j-api', version: '2.0.11'
api 'io.xpipe:modulefs:0.1.5'
api 'net.synedra:validatorfx:0.4.2'
api ('io.github.mkpaz:atlantafx-base:2.0.1') {
exclude group: 'org.openjfx', module: 'javafx-base' exclude group: 'org.openjfx', module: 'javafx-base'
exclude group: 'org.openjfx', module: 'javafx-controls' exclude group: 'org.openjfx', module: 'javafx-controls'
} }
implementation name: 'jSystemThemeDetector-3.8'
implementation group: 'com.github.oshi', name: 'oshi-core-java11', version: '6.4.2'
implementation 'org.jetbrains:annotations:24.0.1'
implementation ('de.jangassen:jfa:1.2.0') {
exclude group: 'net.java.dev.jna', module: 'jna'
}
} }
apply from: "$rootDir/gradle/gradle_scripts/junit.gradle" apply from: "$rootDir/gradle/gradle_scripts/local_junit_suite.gradle"
sourceSets {
main {
output.resourcesDir("${project.layout.buildDirectory.get()}/classes/java/main")
}
}
dependencies {
testImplementation project(':api')
testImplementation project(':core')
}
project.allExtensions.forEach((Project p) -> {
dependencies {
testCompileOnly p
}
})
project.ext {
jvmRunArgs = [
"--add-exports", "javafx.graphics/com.sun.javafx.scene=com.jfoenix",
"--add-exports", "javafx.graphics/com.sun.javafx.stage=com.jfoenix",
"--add-exports", "javafx.base/com.sun.javafx.binding=com.jfoenix",
"--add-exports", "javafx.base/com.sun.javafx.event=com.jfoenix",
"--add-exports", "javafx.controls/com.sun.javafx.scene.control=com.jfoenix",
"--add-exports", "javafx.controls/com.sun.javafx.scene.control.behavior=com.jfoenix",
"--add-exports", "javafx.graphics/com.sun.javafx.scene.traversal=org.controlsfx.controls",
"--add-exports", "javafx.graphics/com.sun.javafx.scene=org.controlsfx.controls",
"--add-exports", "org.apache.commons.lang3/org.apache.commons.lang3.math=io.xpipe.app",
"--add-opens", "java.base/java.lang=io.xpipe.app",
"--add-opens", "java.base/java.nio.file=io.xpipe.app",
"--add-opens", "java.base/java.lang.reflect=com.jfoenix",
"--add-opens", "java.base/java.lang.reflect=com.jfoenix",
"--add-opens", "java.base/java.lang=io.xpipe.core",
"--add-opens", "java.desktop/java.awt=io.xpipe.app",
"--add-opens", "net.synedra.validatorfx/net.synedra.validatorfx=io.xpipe.app",
"--add-opens", 'com.dlsc.preferencesfx/com.dlsc.preferencesfx.view=io.xpipe.app',
"--add-opens", 'com.dlsc.preferencesfx/com.dlsc.preferencesfx.model=io.xpipe.app',
"-Xmx8g",
"-Dio.xpipe.app.arch=$rootProject.arch",
"-Dfile.encoding=UTF-8",
// Disable this for now as it requires Windows 10+
// '-XX:+UseZGC',
"-Dvisualvm.display.name=XPipe"
]
}
import org.gradle.internal.os.OperatingSystem
if (OperatingSystem.current() == OperatingSystem.LINUX) {
jvmRunArgs.addAll("--add-opens", "java.desktop/sun.awt.X11=io.xpipe.app")
}
def extensionJarDepList = project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)).toList(); def extensionJarDepList = project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)).toList();
jar { jar {
finalizedBy(extensionJarDepList) finalizedBy(extensionJarDepList)
} }
@ -146,14 +85,19 @@ run {
systemProperty 'io.xpipe.app.developerMode', "true" systemProperty 'io.xpipe.app.developerMode', "true"
systemProperty 'io.xpipe.app.logLevel', "trace" systemProperty 'io.xpipe.app.logLevel', "trace"
systemProperty 'io.xpipe.app.fullVersion', rootProject.fullVersion systemProperty 'io.xpipe.app.fullVersion', rootProject.fullVersion
systemProperty 'io.xpipe.app.showcase', 'false' systemProperty 'io.xpipe.app.showcase', 'true'
systemProperty 'io.xpipe.app.staging', isStage
// systemProperty "io.xpipe.beacon.port", "21724" // systemProperty "io.xpipe.beacon.port", "21724"
// systemProperty "io.xpipe.beacon.printMessages", "true" // systemProperty "io.xpipe.beacon.printMessages", "true"
// systemProperty 'io.xpipe.app.debugPlatform', "true" // systemProperty 'io.xpipe.app.debugPlatform', "true"
// systemProperty "io.xpipe.beacon.localProxy", "true" // Apply passed xpipe properties
for (final def e in System.getProperties().entrySet()) {
if (e.getKey().toString().contains("xpipe")) {
systemProperty e.getKey().toString(), e.getValue()
}
}
systemProperty 'java.library.path', "./lib"
workingDir = rootDir workingDir = rootDir
} }
@ -181,7 +125,7 @@ processResources {
javaexec { javaexec {
workingDir = project.projectDir workingDir = project.projectDir
jvmArgs += "--module-path=$sourceSets.main.runtimeClasspath.asPath," jvmArgs += "--module-path=${configurations.javafx.asFileTree.asPath},"
jvmArgs += "--add-modules=javafx.graphics" jvmArgs += "--add-modules=javafx.graphics"
main = "com.sun.javafx.css.parser.Css2Bin" main = "com.sun.javafx.css.parser.Css2Bin"
args css args css

View file

@ -0,0 +1,4 @@
open module io.xpipe.app.localTest {
requires org.junit.jupiter.api;
requires io.xpipe.app;
}

View file

@ -0,0 +1,13 @@
package test;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.test.LocalExtensionTest;
public class Test extends LocalExtensionTest {
@org.junit.jupiter.api.Test
public void test() {
System.out.println("a");
System.out.println(DataStorage.get().getStoreEntries());
}
}

View file

@ -14,10 +14,10 @@ public class Main {
// Since this is not marked as a console application, it will not print anything when you run it in a console // Since this is not marked as a console application, it will not print anything when you run it in a console
// So sadly there can't be a help command // So sadly there can't be a help command
// if (args.length == 1 && args[0].equals("--help")) { // if (args.length == 1 && args[0].equals("--help")) {
// System.out.println("HELP"); // System.out.println("HELP");
// return; // return;
// } // }
OperationMode.init(args); OperationMode.init(args);
} }

View file

@ -5,12 +5,42 @@ import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileSystem;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class BrowserAlerts { public class BrowserAlerts {
public static FileConflictChoice showFileConflictAlert(String file, boolean multiple) {
var map = new LinkedHashMap<ButtonType, FileConflictChoice>();
map.put(new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE), FileConflictChoice.CANCEL);
if (multiple) {
map.put(new ButtonType("Skip", ButtonBar.ButtonData.OTHER), FileConflictChoice.SKIP);
map.put(new ButtonType("Skip All", ButtonBar.ButtonData.OTHER), FileConflictChoice.SKIP_ALL);
}
map.put(new ButtonType("Replace", ButtonBar.ButtonData.OTHER), FileConflictChoice.REPLACE);
if (multiple) {
map.put(new ButtonType("Replace All", ButtonBar.ButtonData.OTHER), FileConflictChoice.REPLACE_ALL);
}
return AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("fileConflictAlertTitle"));
alert.setHeaderText(AppI18n.get("fileConflictAlertHeader"));
AppWindowHelper.setContent(
alert,
AppI18n.get(
multiple ? "fileConflictAlertContentMultiple" : "fileConflictAlertContent", file));
alert.setAlertType(Alert.AlertType.CONFIRMATION);
alert.getButtonTypes().clear();
map.sequencedKeySet()
.forEach(buttonType -> alert.getButtonTypes().add(buttonType));
})
.map(map::get)
.orElse(FileConflictChoice.CANCEL);
}
public static boolean showMoveAlert(List<FileSystem.FileEntry> source, FileSystem.FileEntry target) { public static boolean showMoveAlert(List<FileSystem.FileEntry> source, FileSystem.FileEntry target) {
if (source.stream().noneMatch(entry -> entry.getKind() == FileKind.DIRECTORY)) { if (source.stream().noneMatch(entry -> entry.getKind() == FileKind.DIRECTORY)) {
return true; return true;
@ -52,4 +82,12 @@ public class BrowserAlerts {
} }
return names; return names;
} }
public enum FileConflictChoice {
CANCEL,
SKIP,
SKIP_ALL,
REPLACE,
REPLACE_ALL
}
} }

View file

@ -0,0 +1,137 @@
package io.xpipe.app.browser;
import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.comp.store.StoreSection;
import io.xpipe.app.comp.store.StoreSectionMiniComp;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FilterComp;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.DataStoreCategoryChoiceComp;
import io.xpipe.app.util.FixedHierarchyStore;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.ShellStore;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Point2D;
import javafx.scene.input.DragEvent;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.function.Predicate;
final class BrowserBookmarkComp extends SimpleComp {
public static final Timer DROP_TIMER = new Timer("dnd", true);
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private final BrowserModel model;
private Point2D lastOver = new Point2D(-1, -1);
private TimerTask activeTask;
BrowserBookmarkComp(BrowserModel model) {
this.model = model;
}
@Override
protected Region createSimple() {
var filterText = new SimpleStringProperty();
var open = PlatformThread.sync(model.getSelected());
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore
|| storeEntryWrapper.getEntry().getStore() instanceof FixedHierarchyStore)
&& storeEntryWrapper.getEntry().getValidity().isUsable();
};
var selectedCategory = new SimpleObjectProperty<>(
StoreViewState.get().getActiveCategory().getValue());
var section = StoreSectionMiniComp.createList(
StoreSection.createTopLevel(
StoreViewState.get().getAllEntries(), storeEntryWrapper -> true, filterText, selectedCategory),
(s, comp) -> {
BooleanProperty busy = new SimpleBooleanProperty(false);
comp.disable(Bindings.createBooleanBinding(
() -> {
return busy.get() || !applicable.test(s.getWrapper());
},
busy));
comp.apply(struc -> {
open.addListener((observable, oldValue, newValue) -> {
struc.get()
.pseudoClassStateChanged(
SELECTED,
newValue != null
&& newValue.getEntry()
.get()
.equals(s.getWrapper()
.getEntry()));
});
struc.get().setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
var entry = s.getWrapper().getEntry();
if (!entry.getValidity().isUsable()) {
return;
}
if (entry.getStore() instanceof ShellStore fileSystem) {
model.openFileSystemAsync(entry.ref(), null, busy);
} else if (entry.getStore() instanceof FixedHierarchyStore) {
BooleanScope.execute(busy, () -> {
s.getWrapper().refreshChildren();
});
}
});
event.consume();
});
});
});
var category = new DataStoreCategoryChoiceComp(
StoreViewState.get().getAllConnectionsCategory(),
StoreViewState.get().getActiveCategory(),
selectedCategory)
.styleClass(Styles.LEFT_PILL);
var filter =
new FilterComp(filterText).styleClass(Styles.RIGHT_PILL).hgrow().apply(struc -> {});
var top = new HorizontalComp(List.of(category.minWidth(Region.USE_PREF_SIZE), filter.hgrow()))
.styleClass("categories")
.apply(struc -> {
AppFont.medium(struc.get());
struc.get().setFillHeight(true);
})
.createRegion();
var r = section.vgrow().createRegion();
var content = new VBox(top, r);
content.setFillWidth(true);
content.getStyleClass().add("bookmark-list");
return content;
}
private void handleHoverTimer(DataStore store, DragEvent event) {
if (lastOver.getX() == event.getX() && lastOver.getY() == event.getY()) {
return;
}
lastOver = (new Point2D(event.getX(), event.getY()));
activeTask = new TimerTask() {
@Override
public void run() {
if (activeTask != this) {}
// Platform.runLater(() -> model.openExistingFileSystemIfPresent(store.asNeeded()));
}
};
DROP_TIMER.schedule(activeTask, 500);
}
}

View file

@ -66,7 +66,7 @@ public class BrowserBreadcrumbBar extends SimpleComp {
var elements = FileNames.splitHierarchy(val); var elements = FileNames.splitHierarchy(val);
var modifiedElements = new ArrayList<>(elements); var modifiedElements = new ArrayList<>(elements);
if (val.startsWith("/")) { if (val.startsWith("/")) {
modifiedElements.add(0, "/"); modifiedElements.addFirst("/");
} }
Breadcrumbs.BreadCrumbItem<String> items = Breadcrumbs.BreadCrumbItem<String> items =
Breadcrumbs.buildTreeModel(modifiedElements.toArray(String[]::new)); Breadcrumbs.buildTreeModel(modifiedElements.toArray(String[]::new));

View file

@ -2,7 +2,7 @@ package io.xpipe.app.browser;
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.ShellDialects; 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;
@ -24,18 +24,6 @@ import java.util.stream.Collectors;
public class BrowserClipboard { public class BrowserClipboard {
@Value
public static class Instance {
UUID uuid;
FileSystem.FileEntry baseDirectory;
List<FileSystem.FileEntry> entries;
public String toClipboardString() {
return entries.stream().map(fileEntry -> "\"" + fileEntry.getPath() + "\"").collect(
Collectors.joining(ShellDialects.getPlatformDefault().getNewLine().getNewLineString()));
}
}
public static final Property<Instance> currentCopyClipboard = new SimpleObjectProperty<>(); public static final Property<Instance> currentCopyClipboard = new SimpleObjectProperty<>();
public static Instance currentDragClipboard; public static Instance currentDragClipboard;
@ -45,7 +33,7 @@ public class BrowserClipboard {
.addFlavorListener(e -> ThreadHelper.runFailableAsync(new FailableRunnable<>() { .addFlavorListener(e -> ThreadHelper.runFailableAsync(new FailableRunnable<>() {
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void run() throws Throwable { public void run() {
Clipboard clipboard = (Clipboard) e.getSource(); Clipboard clipboard = (Clipboard) e.getSource();
try { try {
if (!clipboard.isDataFlavorAvailable(DataFlavor.javaFileListFlavor)) { if (!clipboard.isDataFlavorAvailable(DataFlavor.javaFileListFlavor)) {
@ -53,7 +41,8 @@ public class BrowserClipboard {
} }
List<File> data = (List<File>) clipboard.getData(DataFlavor.javaFileListFlavor); List<File> data = (List<File>) clipboard.getData(DataFlavor.javaFileListFlavor);
var files = data.stream().map(string -> string.toPath()).toList(); var files =
data.stream().map(string -> string.toPath()).toList();
if (files.size() == 0) { if (files.size() == 0) {
return; return;
} }
@ -121,4 +110,20 @@ public class BrowserClipboard {
return null; return null;
} }
@Value
public static class Instance {
UUID uuid;
FileSystem.FileEntry baseDirectory;
List<FileSystem.FileEntry> entries;
public String toClipboardString() {
return entries.stream()
.map(fileEntry -> "\"" + fileEntry.getPath() + "\"")
.collect(Collectors.joining(ProcessControlProvider.get()
.getEffectiveLocalDialect()
.getNewLine()
.getNewLineString()));
}
}
} }

View file

@ -58,9 +58,10 @@ public class BrowserComp extends SimpleComp {
FileIconManager.loadIfNecessary(); FileIconManager.loadIfNecessary();
}); });
var bookmarksList = new BrowserBookmarkList(model).vgrow(); var bookmarksList = new BrowserBookmarkComp(model).vgrow();
var localDownloadStage = new BrowserTransferComp(model.getLocalTransfersStage()).hide( var localDownloadStage = new BrowserTransferComp(model.getLocalTransfersStage())
PlatformThread.sync(Bindings.createBooleanBinding(() -> { .hide(PlatformThread.sync(Bindings.createBooleanBinding(
() -> {
if (model.getOpenFileSystems().size() == 0) { if (model.getOpenFileSystems().size() == 0) {
return true; return true;
} }
@ -69,20 +70,18 @@ public class BrowserComp extends SimpleComp {
return true; return true;
} }
// Also show on local
if (model.getSelected().getValue() != null) {
// return model.getSelected().getValue().isLocal();
}
return false; return false;
}, model.getOpenFileSystems(), model.getSelected()))); },
model.getOpenFileSystems(),
model.getSelected())));
localDownloadStage.prefHeight(200); localDownloadStage.prefHeight(200);
localDownloadStage.maxHeight(200); localDownloadStage.maxHeight(200);
var vertical = new VerticalComp(List.of(bookmarksList, localDownloadStage)); var vertical = new VerticalComp(List.of(bookmarksList, localDownloadStage));
var splitPane = new SideSplitPaneComp(vertical, createTabs()).withInitialWidth( var splitPane = new SideSplitPaneComp(vertical, createTabs())
AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth()).withOnDividerChange( .withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
AppLayoutModel.get().getSavedState()::setBrowserConnectionsWidth).apply(struc -> { .withOnDividerChange(AppLayoutModel.get().getSavedState()::setBrowserConnectionsWidth)
.apply(struc -> {
struc.getLeft().setMinWidth(200); struc.getLeft().setMinWidth(200);
struc.getLeft().setMaxWidth(500); struc.getLeft().setMaxWidth(500);
}); });
@ -104,12 +103,16 @@ public class BrowserComp extends SimpleComp {
selected.setSpacing(10); selected.setSpacing(10);
model.getSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> { model.getSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {
selected.getChildren().setAll(c.getList().stream().map(s -> { selected.getChildren()
var field = new TextField(s.getRawFileEntry().getPath()); .setAll(c.getList().stream()
.map(s -> {
var field =
new TextField(s.getRawFileEntry().getPath());
field.setEditable(false); field.setEditable(false);
field.setPrefWidth(500); field.setPrefWidth(500);
return field; return field;
}).toList()); })
.toList());
}); });
}); });
var spacer = new Spacer(Orientation.HORIZONTAL); var spacer = new Spacer(Orientation.HORIZONTAL);
@ -128,12 +131,16 @@ public class BrowserComp extends SimpleComp {
} }
private Comp<?> createTabs() { private Comp<?> createTabs() {
var multi = new MultiContentComp(Map.<Comp<?>, ObservableValue<Boolean>>of(Comp.of(() -> createTabPane()), var multi = new MultiContentComp(Map.<Comp<?>, ObservableValue<Boolean>>of(
Comp.of(() -> createTabPane()),
BindingsHelper.persist(Bindings.isNotEmpty(model.getOpenFileSystems())), BindingsHelper.persist(Bindings.isNotEmpty(model.getOpenFileSystems())),
new BrowserWelcomeComp(model).apply(struc -> StackPane.setAlignment(struc.get(), Pos.CENTER_LEFT)), new BrowserWelcomeComp(model).apply(struc -> StackPane.setAlignment(struc.get(), Pos.CENTER_LEFT)),
Bindings.createBooleanBinding(() -> { Bindings.createBooleanBinding(
return model.getOpenFileSystems().size() == 0 && !model.getMode().isChooser(); () -> {
}, model.getOpenFileSystems()))); return model.getOpenFileSystems().size() == 0
&& !model.getMode().isChooser();
},
model.getOpenFileSystems())));
return multi; return multi;
} }
@ -154,7 +161,8 @@ public class BrowserComp extends SimpleComp {
map.put(v, t); map.put(v, t);
tabs.getTabs().add(t); tabs.getTabs().add(t);
}); });
tabs.getSelectionModel().select(model.getOpenFileSystems().indexOf(model.getSelected().getValue())); tabs.getSelectionModel()
.select(model.getOpenFileSystems().indexOf(model.getSelected().getValue()));
// Used for ignoring changes by the tabpane when new tabs are added. We want to perform the selections manually! // Used for ignoring changes by the tabpane when new tabs are added. We want to perform the selections manually!
var modifying = new SimpleBooleanProperty(); var modifying = new SimpleBooleanProperty();
@ -170,9 +178,9 @@ public class BrowserComp extends SimpleComp {
return; return;
} }
var source = map.entrySet() var source = map.entrySet().stream()
.stream() .filter(openFileSystemModelTabEntry ->
.filter(openFileSystemModelTabEntry -> openFileSystemModelTabEntry.getValue().equals(newValue)) openFileSystemModelTabEntry.getValue().equals(newValue))
.findAny() .findAny()
.map(Map.Entry::getKey) .map(Map.Entry::getKey)
.orElse(null); .orElse(null);
@ -187,9 +195,9 @@ public class BrowserComp extends SimpleComp {
return; return;
} }
var toSelect = map.entrySet() var toSelect = map.entrySet().stream()
.stream() .filter(openFileSystemModelTabEntry ->
.filter(openFileSystemModelTabEntry -> openFileSystemModelTabEntry.getKey().equals(newValue)) openFileSystemModelTabEntry.getKey().equals(newValue))
.findAny() .findAny()
.map(Map.Entry::getValue) .map(Map.Entry::getValue)
.orElse(null); .orElse(null);
@ -228,9 +236,9 @@ public class BrowserComp extends SimpleComp {
tabs.getTabs().addListener((ListChangeListener<? super Tab>) c -> { tabs.getTabs().addListener((ListChangeListener<? super Tab>) c -> {
while (c.next()) { while (c.next()) {
for (var r : c.getRemoved()) { for (var r : c.getRemoved()) {
var source = map.entrySet() var source = map.entrySet().stream()
.stream() .filter(openFileSystemModelTabEntry ->
.filter(openFileSystemModelTabEntry -> openFileSystemModelTabEntry.getValue().equals(r)) openFileSystemModelTabEntry.getValue().equals(r))
.findAny() .findAny()
.orElse(null); .orElse(null);
@ -253,14 +261,22 @@ public class BrowserComp extends SimpleComp {
ring.setMinSize(16, 16); ring.setMinSize(16, 16);
ring.setPrefSize(16, 16); ring.setPrefSize(16, 16);
ring.setMaxSize(16, 16); ring.setMaxSize(16, 16);
ring.progressProperty().bind(Bindings.createDoubleBinding(() -> model.getBusy().get() ? -1d : 0, PlatformThread.sync(model.getBusy()))); ring.progressProperty()
.bind(Bindings.createDoubleBinding(
() -> model.getBusy().get() ? -1d : 0, PlatformThread.sync(model.getBusy())));
var image = model.getEntry().get().getProvider().getDisplayIconFileName(model.getEntry().getStore()); var image = model.getEntry()
.get()
.getProvider()
.getDisplayIconFileName(model.getEntry().getStore());
var logo = PrettyImageHelper.ofFixedSquare(image, 16).createRegion(); var logo = PrettyImageHelper.ofFixedSquare(image, 16).createRegion();
tab.graphicProperty().bind(Bindings.createObjectBinding(() -> { tab.graphicProperty()
.bind(Bindings.createObjectBinding(
() -> {
return model.getBusy().get() ? ring : logo; return model.getBusy().get() ? ring : logo;
}, PlatformThread.sync(model.getBusy()))); },
PlatformThread.sync(model.getBusy())));
tab.setText(model.getName()); tab.setText(model.getName());
tab.setContent(new OpenFileSystemComp(model).createSimple()); tab.setContent(new OpenFileSystemComp(model).createSimple());
@ -281,12 +297,17 @@ public class BrowserComp extends SimpleComp {
StackPane c = (StackPane) tabs.lookup("#" + id + " .tab-container"); StackPane c = (StackPane) tabs.lookup("#" + id + " .tab-container");
c.getStyleClass().add("color-box"); c.getStyleClass().add("color-box");
var color = DataStorage.get().getRootForEntry(model.getEntry().get()).getColor(); var color = DataStorage.get()
.getRootForEntry(model.getEntry().get())
.getColor();
if (color != null) { if (color != null) {
c.getStyleClass().add(color.getId()); c.getStyleClass().add(color.getId());
} }
new FancyTooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(c); new FancyTooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(c);
c.addEventHandler(DragEvent.DRAG_ENTERED, mouseEvent -> Platform.runLater(() -> tabs.getSelectionModel().select(tab))); c.addEventHandler(
DragEvent.DRAG_ENTERED,
mouseEvent -> Platform.runLater(
() -> tabs.getSelectionModel().select(tab)));
}); });
} }
}); });

View file

@ -24,6 +24,17 @@ final class BrowserContextMenu extends ContextMenu {
createMenu(); createMenu();
} }
private static List<BrowserEntry> resolveIfNeeded(BrowserAction action, List<BrowserEntry> selected) {
return action.automaticallyResolveLinks()
? selected.stream()
.map(browserEntry -> new BrowserEntry(
browserEntry.getRawFileEntry().resolved(),
browserEntry.getModel(),
browserEntry.isSynthetic()))
.toList()
: selected;
}
private void createMenu() { private void createMenu() {
AppFont.normal(this.getStyleableNode()); AppFont.normal(this.getStyleableNode());
@ -81,7 +92,10 @@ final class BrowserContextMenu extends ContextMenu {
} }
m.setDisable(!a.isActive(model, used)); m.setDisable(!a.isActive(model, used));
if (la.getProFeatureId() != null && !LicenseProvider.get().getFeature(la.getProFeatureId()).isSupported()) { if (la.getProFeatureId() != null
&& !LicenseProvider.get()
.getFeature(la.getProFeatureId())
.isSupported()) {
m.setDisable(true); m.setDisable(true);
m.setGraphic(new FontIcon("mdi2p-professional-hexagon")); m.setGraphic(new FontIcon("mdi2p-professional-hexagon"));
} }
@ -91,15 +105,4 @@ final class BrowserContextMenu extends ContextMenu {
} }
} }
} }
private static List<BrowserEntry> resolveIfNeeded(BrowserAction action, List<BrowserEntry> selected) {
return action.automaticallyResolveLinks()
? selected.stream()
.map(browserEntry -> new BrowserEntry(
browserEntry.getRawFileEntry().resolved(),
browserEntry.getModel(),
browserEntry.isSynthetic()))
.toList()
: selected;
}
} }

View file

@ -2,8 +2,8 @@ package io.xpipe.app.browser;
import io.xpipe.app.browser.icon.DirectoryType; import io.xpipe.app.browser.icon.DirectoryType;
import io.xpipe.app.browser.icon.FileType; import io.xpipe.app.browser.icon.FileType;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileSystem;
import lombok.Getter; import lombok.Getter;

View file

@ -12,9 +12,9 @@ import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.HumanReadableFormat; import io.xpipe.app.util.HumanReadableFormat;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.process.OsType; 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.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;
@ -81,18 +81,18 @@ final class BrowserFileListComp extends SimpleComp {
filenameCol.setCellFactory(col -> new FilenameCell(fileList.getEditing())); filenameCol.setCellFactory(col -> new FilenameCell(fileList.getEditing()));
var sizeCol = new TableColumn<BrowserEntry, Number>("Size"); var sizeCol = new TableColumn<BrowserEntry, Number>("Size");
sizeCol.setCellValueFactory(param -> sizeCol.setCellValueFactory(param -> new SimpleLongProperty(
new SimpleLongProperty(param.getValue().getRawFileEntry().resolved().getSize())); param.getValue().getRawFileEntry().resolved().getSize()));
sizeCol.setCellFactory(col -> new FileSizeCell()); sizeCol.setCellFactory(col -> new FileSizeCell());
var mtimeCol = new TableColumn<BrowserEntry, Instant>("Modified"); var mtimeCol = new TableColumn<BrowserEntry, Instant>("Modified");
mtimeCol.setCellValueFactory(param -> mtimeCol.setCellValueFactory(param -> new SimpleObjectProperty<>(
new SimpleObjectProperty<>(param.getValue().getRawFileEntry().resolved().getDate())); param.getValue().getRawFileEntry().resolved().getDate()));
mtimeCol.setCellFactory(col -> new FileTimeCell()); mtimeCol.setCellFactory(col -> new FileTimeCell());
var modeCol = new TableColumn<BrowserEntry, String>("Attributes"); var modeCol = new TableColumn<BrowserEntry, String>("Attributes");
modeCol.setCellValueFactory(param -> modeCol.setCellValueFactory(param -> new SimpleObjectProperty<>(
new SimpleObjectProperty<>(param.getValue().getRawFileEntry().resolved().getMode())); param.getValue().getRawFileEntry().resolved().getMode()));
modeCol.setCellFactory(col -> new FileModeCell()); modeCol.setCellFactory(col -> new FileModeCell());
modeCol.setSortable(false); modeCol.setSortable(false);
@ -171,7 +171,7 @@ final class BrowserFileListComp extends SimpleComp {
.mapToInt(entry -> table.getItems().indexOf(entry)) .mapToInt(entry -> table.getItems().indexOf(entry))
.toArray(); .toArray();
table.getSelectionModel() table.getSelectionModel()
.selectIndices(table.getItems().indexOf(c.getList().get(0)), indices); .selectIndices(table.getItems().indexOf(c.getList().getFirst()), indices);
}); });
}); });
} }
@ -247,12 +247,20 @@ final class BrowserFileListComp extends SimpleComp {
} }
if (row.getItem() != null if (row.getItem() != null
&& row.getItem().getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) { && row.getItem()
.getRawFileEntry()
.resolved()
.getKind()
== FileKind.DIRECTORY) {
return event.getButton() == MouseButton.SECONDARY; return event.getButton() == MouseButton.SECONDARY;
} }
if (row.getItem() != null if (row.getItem() != null
&& row.getItem().getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY) { && row.getItem()
.getRawFileEntry()
.resolved()
.getKind()
!= FileKind.DIRECTORY) {
return event.getButton() == MouseButton.SECONDARY return event.getButton() == MouseButton.SECONDARY
|| event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2; || event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2;
} }
@ -409,9 +417,10 @@ final class BrowserFileListComp extends SimpleComp {
} }
double proximity = 100; double proximity = 100;
Bounds tableBounds = tableView.localToScene(tableView.getBoundsInParent()); Bounds tableBounds = tableView.localToScene(tableView.getBoundsInLocal());
double dragY = event.getSceneY(); double dragY = event.getSceneY();
double topYProximity = tableBounds.getMinY() + proximity; // Include table header as well in calculations
double topYProximity = tableBounds.getMinY() + proximity + 20;
double bottomYProximity = tableBounds.getMaxY() - proximity; double bottomYProximity = tableBounds.getMaxY() - proximity;
// clamp new values between 0 and 1 to prevent scrollbar flicking around at the edges // clamp new values between 0 and 1 to prevent scrollbar flicking around at the edges
@ -424,15 +433,60 @@ final class BrowserFileListComp extends SimpleComp {
} }
} }
private static class FileSizeCell extends TableCell<BrowserEntry, Number> {
@Override
protected void updateItem(Number fileSize, boolean empty) {
super.updateItem(fileSize, empty);
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
setText(null);
} else {
var path = getTableRow().getItem();
if (path.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
setText("");
} else {
setText(byteCount(fileSize.longValue()));
}
}
}
}
private static class FileModeCell extends TableCell<BrowserEntry, String> {
@Override
protected void updateItem(String mode, boolean empty) {
super.updateItem(mode, empty);
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
setText(null);
} else {
setText(mode);
}
}
}
private static class FileTimeCell extends TableCell<BrowserEntry, Instant> {
@Override
protected void updateItem(Instant fileTime, boolean empty) {
super.updateItem(fileTime, empty);
if (empty) {
setText(null);
} else {
setText(
fileTime != null
? HumanReadableFormat.date(
fileTime.atZone(ZoneId.systemDefault()).toLocalDateTime())
: "");
}
}
}
private class FilenameCell extends TableCell<BrowserEntry, String> { private class FilenameCell extends TableCell<BrowserEntry, String> {
private final StringProperty img = new SimpleStringProperty(); private final StringProperty img = new SimpleStringProperty();
private final StringProperty text = new SimpleStringProperty(); private final StringProperty text = new SimpleStringProperty();
private final Node imageView = new PrettySvgComp(img, 24, 24)
.createRegion();
private final StackPane textField = private final StackPane textField =
new LazyTextFieldComp(text).createStructure().get(); new LazyTextFieldComp(text).createStructure().get();
private final HBox graphic;
private final BooleanProperty updating = new SimpleBooleanProperty(); private final BooleanProperty updating = new SimpleBooleanProperty();
@ -463,7 +517,8 @@ final class BrowserFileListComp extends SimpleComp {
}; };
text.addListener(listener); text.addListener(listener);
graphic = new HBox(imageView, textField); Node imageView = new PrettySvgComp(img, 24, 24).createRegion();
HBox graphic = new HBox(imageView, textField);
graphic.setSpacing(10); graphic.setSpacing(10);
graphic.setAlignment(Pos.CENTER_LEFT); graphic.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(textField, Priority.ALWAYS); HBox.setHgrow(textField, Priority.ALWAYS);
@ -520,52 +575,4 @@ final class BrowserFileListComp extends SimpleComp {
} }
} }
} }
private static class FileSizeCell extends TableCell<BrowserEntry, Number> {
@Override
protected void updateItem(Number fileSize, boolean empty) {
super.updateItem(fileSize, empty);
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
setText(null);
} else {
var path = getTableRow().getItem();
if (path.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
setText("");
} else {
setText(byteCount(fileSize.longValue()));
}
}
}
}
private static class FileModeCell extends TableCell<BrowserEntry, String> {
@Override
protected void updateItem(String mode, boolean empty) {
super.updateItem(mode, empty);
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
setText(null);
} else {
setText(mode);
}
}
}
private static class FileTimeCell extends TableCell<BrowserEntry, Instant> {
@Override
protected void updateItem(Instant fileTime, boolean empty) {
super.updateItem(fileTime, empty);
if (empty) {
setText(null);
} else {
setText(
fileTime != null
? HumanReadableFormat.date(
fileTime.atZone(ZoneId.systemDefault()).toLocalDateTime())
: "");
}
}
}
} }

View file

@ -27,7 +27,8 @@ public class BrowserFileListCompEntry {
private Point2D lastOver = new Point2D(-1, -1); private Point2D lastOver = new Point2D(-1, -1);
private TimerTask activeTask; private TimerTask activeTask;
public BrowserFileListCompEntry(TableView<BrowserEntry> tv, Node row, BrowserEntry item, BrowserFileListModel model) { public BrowserFileListCompEntry(
TableView<BrowserEntry> tv, Node row, BrowserEntry item, BrowserFileListModel model) {
this.tv = tv; this.tv = tv;
this.row = row; this.row = row;
this.item = item; this.item = item;
@ -59,11 +60,15 @@ public class BrowserFileListCompEntry {
var all = tv.getItems(); var all = tv.getItems();
var index = item != null ? all.indexOf(item) : all.size() - 1; var index = item != null ? all.indexOf(item) : all.size() - 1;
var min = Math.min(index, tv.getSelectionModel().getSelectedIndices().stream() var min = Math.min(
index,
tv.getSelectionModel().getSelectedIndices().stream()
.mapToInt(value -> value) .mapToInt(value -> value)
.min() .min()
.orElse(1)); .orElse(1));
var max = Math.max(index, tv.getSelectionModel().getSelectedIndices().stream() var max = Math.max(
index,
tv.getSelectionModel().getSelectedIndices().stream()
.mapToInt(value -> value) .mapToInt(value -> value)
.max() .max()
.orElse(all.indexOf(item))); .orElse(all.indexOf(item)));
@ -98,13 +103,15 @@ public class BrowserFileListCompEntry {
return false; return false;
} }
if (!Objects.equals(model.getFileSystemModel().getFileSystem(), cb.getEntries().get(0).getFileSystem())) { if (!Objects.equals(
model.getFileSystemModel().getFileSystem(),
cb.getEntries().getFirst().getFileSystem())) {
return true; return true;
} }
// Prevent drag and drops of files into the current directory // Prevent drag and drops of files into the current directory
if (cb.getBaseDirectory() != null && cb if (cb.getBaseDirectory() != null
.getBaseDirectory() && cb.getBaseDirectory()
.getPath() .getPath()
.equals(model.getFileSystemModel().getCurrentDirectory().getPath()) .equals(model.getFileSystemModel().getCurrentDirectory().getPath())
&& (item == null || item.getRawFileEntry().getKind() != FileKind.DIRECTORY)) { && (item == null || item.getRawFileEntry().getKind() != FileKind.DIRECTORY)) {

View file

@ -2,8 +2,8 @@ package io.xpipe.app.browser;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileSystem;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
@ -99,8 +99,9 @@ public final class BrowserFileListModel {
path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY); path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
var comp = comparatorProperty.getValue(); var comp = comparatorProperty.getValue();
Comparator<? super BrowserEntry> us = Comparator<? super BrowserEntry> us = comp != null
comp != null ? syntheticFirst.thenComparing(dirsFirst).thenComparing(comp) : syntheticFirst.thenComparing(dirsFirst); ? syntheticFirst.thenComparing(dirsFirst).thenComparing(comp)
: syntheticFirst.thenComparing(dirsFirst);
l.sort(us); l.sort(us);
} }
@ -110,14 +111,17 @@ public final class BrowserFileListModel {
boolean exists; boolean exists;
try { try {
exists = fileSystemModel.getFileSystem().fileExists(newFullPath) || fileSystemModel.getFileSystem().directoryExists(newFullPath); exists = fileSystemModel.getFileSystem().fileExists(newFullPath)
|| fileSystemModel.getFileSystem().directoryExists(newFullPath);
} catch (Exception e) { } catch (Exception e) {
ErrorEvent.fromThrowable(e).handle(); ErrorEvent.fromThrowable(e).handle();
return false; return false;
} }
if (exists) { if (exists) {
ErrorEvent.fromMessage("Target " + newFullPath + " does already exist").expected().handle(); ErrorEvent.fromMessage("Target " + newFullPath + " does already exist")
.expected()
.handle();
fileSystemModel.refresh(); fileSystemModel.refresh();
return false; return false;
} }

View file

@ -16,6 +16,14 @@ import org.kordamp.ikonli.javafx.FontIcon;
public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> { public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> {
private final OpenFileSystemModel model;
private final Property<String> filterString;
public BrowserFilterComp(OpenFileSystemModel model, Property<String> filterString) {
this.model = model;
this.filterString = filterString;
}
@Override @Override
public Structure createBase() { public Structure createBase() {
var expanded = new SimpleBooleanProperty(); var expanded = new SimpleBooleanProperty();
@ -98,12 +106,4 @@ public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> {
return box; return box;
} }
} }
private final OpenFileSystemModel model;
private final Property<String> filterString;
public BrowserFilterComp(OpenFileSystemModel model, Property<String> filterString) {
this.model = model;
this.filterString = filterString;
}
} }

View file

@ -1,6 +1,8 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import atlantafx.base.theme.Styles;
import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
@ -11,6 +13,16 @@ public class BrowserGreetingComp extends SimpleComp {
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var r = new Label(getText());
AppLayoutModel.get().getSelected().addListener((observableValue, entry, t1) -> {
r.setText(getText());
});
AppFont.setSize(r, 7);
r.getStyleClass().add(Styles.TEXT_BOLD);
return r;
}
private String getText() {
var ldt = LocalDateTime.now(); var ldt = LocalDateTime.now();
var hour = ldt.getHour(); var hour = ldt.getHour();
String text; String text;
@ -21,8 +33,6 @@ public class BrowserGreetingComp extends SimpleComp {
} else { } else {
text = "Good afternoon"; text = "Good afternoon";
} }
var r = new Label(text); return text;
AppFont.setSize(r, 7);
return r;
} }
} }

View file

@ -33,6 +33,7 @@ public class BrowserModel {
private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this); private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this);
private final ObservableList<BrowserEntry> selection = FXCollections.observableArrayList(); private final ObservableList<BrowserEntry> selection = FXCollections.observableArrayList();
private final BrowserSavedState savedState; private final BrowserSavedState savedState;
@Setter @Setter
private Consumer<List<FileReference>> onFinish; private Consumer<List<FileReference>> onFinish;
@ -70,12 +71,21 @@ public class BrowserModel {
public void reset() { public void reset() {
synchronized (BrowserModel.this) { synchronized (BrowserModel.this) {
for (OpenFileSystemModel o : new ArrayList<>(openFileSystems)) { for (OpenFileSystemModel o : new ArrayList<>(openFileSystems)) {
// Don't close busy connections gracefully
// as we otherwise might lock up
if (o.isBusy()) {
continue;
}
closeFileSystemSync(o); closeFileSystemSync(o);
} }
if (savedState != null) { if (savedState != null) {
savedState.save(); savedState.save();
} }
} }
// Delete all files
localTransfersStage.clear();
} }
public void finishChooser() { public void finishChooser() {
@ -95,8 +105,10 @@ public class BrowserModel {
return; return;
} }
var stores = chosen.stream().map( var stores = chosen.stream()
entry -> new FileReference(selected.getValue().getEntry(), entry.getRawFileEntry().getPath())).toList(); .map(entry -> new FileReference(
selected.getValue().getEntry(), entry.getRawFileEntry().getPath()))
.toList();
onFinish.accept(stores); onFinish.accept(stores);
} }
@ -107,8 +119,11 @@ public class BrowserModel {
} }
private void closeFileSystemSync(OpenFileSystemModel open) { private void closeFileSystemSync(OpenFileSystemModel open) {
if (DataStorage.get().getStoreEntries().contains(open.getEntry().get()) && savedState != null && open.getCurrentPath().get() != null) { if (DataStorage.get().getStoreEntries().contains(open.getEntry().get())
savedState.add(new BrowserSavedState.Entry(open.getEntry().get().getUuid(), open.getCurrentPath().get())); && savedState != null
&& open.getCurrentPath().get() != null) {
savedState.add(new BrowserSavedState.Entry(
open.getEntry().get().getUuid(), open.getCurrentPath().get()));
} }
open.closeSync(); open.closeSync();
synchronized (BrowserModel.this) { synchronized (BrowserModel.this) {
@ -116,7 +131,10 @@ public class BrowserModel {
} }
} }
public void openFileSystemAsync(DataStoreEntryRef<? extends FileSystemStore> store, FailableFunction<OpenFileSystemModel, String, Exception> path, BooleanProperty externalBusy) { public void openFileSystemAsync(
DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<OpenFileSystemModel, String, Exception> path,
BooleanProperty externalBusy) {
if (store == null) { if (store == null) {
return; return;
} }

View file

@ -106,7 +106,6 @@ public class BrowserNavBar extends SimpleComp {
}) })
.augment(new SimpleCompStructure<>(homeButton)); .augment(new SimpleCompStructure<>(homeButton));
var historyButton = new Button(null, new FontIcon("mdi2h-history")); var historyButton = new Button(null, new FontIcon("mdi2h-history"));
historyButton.setAccessibleText("History"); historyButton.setAccessibleText("History");
historyButton.getStyleClass().add(Styles.RIGHT_PILL); historyButton.getStyleClass().add(Styles.RIGHT_PILL);
@ -146,7 +145,6 @@ public class BrowserNavBar extends SimpleComp {
.maxHeightProperty() .maxHeightProperty()
.bind(((Region) struc.get().getChildren().get(1)).heightProperty()); .bind(((Region) struc.get().getChildren().get(1)).heightProperty());
((Region) struc.get().getChildren().get(2)) ((Region) struc.get().getChildren().get(2))
.minHeightProperty() .minHeightProperty()
.bind(((Region) struc.get().getChildren().get(1)).heightProperty()); .bind(((Region) struc.get().getChildren().get(1)).heightProperty());
@ -197,7 +195,8 @@ public class BrowserNavBar extends SimpleComp {
cm.getItems().add(current); cm.getItems().add(current);
} }
var b = model.getHistory().getBackwardHistory(Integer.MAX_VALUE).stream().toList(); var b = model.getHistory().getBackwardHistory(Integer.MAX_VALUE).stream()
.toList();
if (!b.isEmpty()) { if (!b.isEmpty()) {
cm.getItems().add(new SeparatorMenuItem()); cm.getItems().add(new SeparatorMenuItem());
} }

View file

@ -9,7 +9,7 @@ import java.util.UUID;
public interface BrowserSavedState { public interface BrowserSavedState {
public void add(Entry entry); void add(Entry entry);
void save(); void save();
@ -18,7 +18,7 @@ public interface BrowserSavedState {
@Value @Value
@Jacksonized @Jacksonized
@Builder @Builder
public static class Entry { class Entry {
UUID uuid; UUID uuid;
String path; String path;

View file

@ -20,12 +20,6 @@ import java.util.List;
@JsonDeserialize(using = BrowserSavedStateImpl.Deserializer.class) @JsonDeserialize(using = BrowserSavedStateImpl.Deserializer.class)
public class BrowserSavedStateImpl implements BrowserSavedState { public class BrowserSavedStateImpl implements BrowserSavedState {
static BrowserSavedStateImpl load() {
return AppCache.get("browser-state", BrowserSavedStateImpl.class, () -> {
return new BrowserSavedStateImpl(FXCollections.observableArrayList());
});
}
@JsonSerialize(as = List.class) @JsonSerialize(as = List.class)
ObservableList<Entry> lastSystems; ObservableList<Entry> lastSystems;
@ -33,25 +27,10 @@ public class BrowserSavedStateImpl implements BrowserSavedState {
this.lastSystems = FXCollections.observableArrayList(lastSystems); this.lastSystems = FXCollections.observableArrayList(lastSystems);
} }
public static class Deserializer extends StdDeserializer<BrowserSavedStateImpl> { static BrowserSavedStateImpl load() {
return AppCache.get("browser-state", BrowserSavedStateImpl.class, () -> {
protected Deserializer() { return new BrowserSavedStateImpl(FXCollections.observableArrayList());
super(BrowserSavedStateImpl.class); });
}
@Override
@SneakyThrows
public BrowserSavedStateImpl deserialize(JsonParser p, DeserializationContext ctxt) {
var tree = (ObjectNode) JacksonMapper.getDefault().readTree(p);
JavaType javaType = JacksonMapper.getDefault()
.getTypeFactory()
.constructCollectionLikeType(List.class, Entry.class);
List<Entry> ls = JacksonMapper.getDefault().treeToValue(tree.remove("lastSystems"), javaType);
if (ls == null) {
ls = List.of();
}
return new BrowserSavedStateImpl(ls);
}
} }
@Override @Override
@ -72,4 +51,24 @@ public class BrowserSavedStateImpl implements BrowserSavedState {
public ObservableList<Entry> getEntries() { public ObservableList<Entry> getEntries() {
return lastSystems; return lastSystems;
} }
public static class Deserializer extends StdDeserializer<BrowserSavedStateImpl> {
protected Deserializer() {
super(BrowserSavedStateImpl.class);
}
@Override
@SneakyThrows
public BrowserSavedStateImpl deserialize(JsonParser p, DeserializationContext ctxt) {
var tree = (ObjectNode) JacksonMapper.getDefault().readTree(p);
JavaType javaType =
JacksonMapper.getDefault().getTypeFactory().constructCollectionLikeType(List.class, Entry.class);
List<Entry> ls = JacksonMapper.getDefault().treeToValue(tree.remove("lastSystems"), javaType);
if (ls == null) {
ls = List.of();
}
return new BrowserSavedStateImpl(ls);
}
}
} }

View file

@ -31,6 +31,13 @@ import java.util.function.Function;
@AllArgsConstructor @AllArgsConstructor
public class BrowserSelectionListComp extends SimpleComp { public class BrowserSelectionListComp extends SimpleComp {
ObservableList<FileSystem.FileEntry> list;
Function<FileSystem.FileEntry, ObservableValue<String>> nameTransformation;
public BrowserSelectionListComp(ObservableList<FileSystem.FileEntry> list) {
this(list, entry -> new SimpleStringProperty(FileNames.getFileName(entry.getPath())));
}
public static Image snapshot(ObservableList<FileSystem.FileEntry> list) { public static Image snapshot(ObservableList<FileSystem.FileEntry> 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);
@ -41,13 +48,6 @@ public class BrowserSelectionListComp extends SimpleComp {
return r.snapshot(parameters, null); return r.snapshot(parameters, null);
} }
ObservableList<FileSystem.FileEntry> list;
Function<FileSystem.FileEntry, ObservableValue<String>> nameTransformation;
public BrowserSelectionListComp(ObservableList<FileSystem.FileEntry> list) {
this(list, entry -> new SimpleStringProperty(FileNames.getFileName(entry.getPath())));
}
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var c = new ListBoxViewComp<>(list, list, entry -> { var c = new ListBoxViewComp<>(list, list, entry -> {

View file

@ -2,11 +2,13 @@ package io.xpipe.app.browser;
import atlantafx.base.controls.Spacer; import atlantafx.base.controls.Spacer;
import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment; 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.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
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.layout.Region; import javafx.scene.layout.Region;
@ -21,6 +23,57 @@ public class BrowserStatusBarComp extends SimpleComp {
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var bar = new ToolBar();
bar.getItems()
.setAll(
createClipboardStatus().createRegion(),
createProgressStatus().createRegion(),
new Spacer(),
createSelectionStatus().createRegion());
bar.getStyleClass().add("status-bar");
bar.setOnDragDetected(event -> {
event.consume();
bar.startFullDrag();
});
AppFont.small(bar);
simulateEmptyCell(bar);
return bar;
}
private Comp<?> createProgressStatus() {
var transferredCount = PlatformThread.sync(Bindings.createStringBinding(
() -> {
return HumanReadableFormat.byteCount(
model.getProgress().getValue().getTransferred());
},
model.getProgress()));
var allCount = PlatformThread.sync(Bindings.createStringBinding(
() -> {
return HumanReadableFormat.byteCount(
model.getProgress().getValue().getTotal());
},
model.getProgress()));
var progressComp = new LabelComp(Bindings.createStringBinding(
() -> {
if (model.getProgress().getValue() == null
|| model.getProgress().getValue().done()) {
return null;
} else {
var name = (model.getProgress().getValue().getName() != null
? " @ " + model.getProgress().getValue().getName() + " "
: "");
return transferredCount.getValue() + " / " + allCount.getValue() + name;
}
},
transferredCount,
allCount,
model.getProgress()));
return progressComp;
}
private Comp<?> createClipboardStatus() {
var cc = PlatformThread.sync(BrowserClipboard.currentCopyClipboard); var cc = PlatformThread.sync(BrowserClipboard.currentCopyClipboard);
var ccCount = Bindings.createStringBinding( var ccCount = Bindings.createStringBinding(
() -> { () -> {
@ -32,7 +85,10 @@ public class BrowserStatusBarComp extends SimpleComp {
} }
}, },
cc); cc);
return new LabelComp(ccCount);
}
private Comp<?> createSelectionStatus() {
var selectedCount = PlatformThread.sync(Bindings.createIntegerBinding( var selectedCount = PlatformThread.sync(Bindings.createIntegerBinding(
() -> { () -> {
return model.getFileList().getSelection().size(); return model.getFileList().getSelection().size();
@ -46,7 +102,6 @@ public class BrowserStatusBarComp extends SimpleComp {
.count(); .count();
}, },
model.getFileList().getAll())); model.getFileList().getAll()));
var selectedComp = new LabelComp(Bindings.createStringBinding( var selectedComp = new LabelComp(Bindings.createStringBinding(
() -> { () -> {
if (selectedCount.getValue().intValue() == 0) { if (selectedCount.getValue().intValue() == 0) {
@ -57,19 +112,7 @@ public class BrowserStatusBarComp extends SimpleComp {
}, },
selectedCount, selectedCount,
allCount)); allCount));
return selectedComp;
var bar = new ToolBar();
bar.getItems().setAll(new LabelComp(ccCount).createRegion(), new Spacer(), selectedComp.createRegion());
bar.getStyleClass().add("status-bar");
bar.setOnDragDetected(event -> {
event.consume();
bar.startFullDrag();
});
AppFont.small(bar);
simulateEmptyCell(bar);
return bar;
} }
private void simulateEmptyCell(Region r) { private void simulateEmptyCell(Region r) {

View file

@ -9,12 +9,12 @@ import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileNames; 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;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.Dragboard; 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;
@ -29,48 +29,61 @@ import java.util.Optional;
public class BrowserTransferComp extends SimpleComp { public class BrowserTransferComp extends SimpleComp {
private final BrowserTransferModel stage; private final BrowserTransferModel model;
public BrowserTransferComp(BrowserTransferModel stage) { public BrowserTransferComp(BrowserTransferModel model) {
this.stage = stage; this.model = model;
} }
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var background = new LabelComp(AppI18n.observable("transferDescription")) var background = new LabelComp(AppI18n.observable("transferDescription"))
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline"))) .apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline")))
.visible(BindingsHelper.persist(Bindings.isEmpty(stage.getItems()))); .visible(BindingsHelper.persist(Bindings.isEmpty(model.getItems())));
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 = BindingsHelper.mappedContentBinding(stage.getItems(), item -> item.getFileEntry()); var binding = BindingsHelper.mappedContentBinding(model.getItems(), item -> item.getFileEntry());
var list = new BrowserSelectionListComp(binding, entry -> Bindings.createStringBinding(() -> { var list = new BrowserSelectionListComp(
var sourceItem = stage.getItems().stream().filter(item -> item.getFileEntry() == entry).findAny(); binding,
entry -> Bindings.createStringBinding(
() -> {
var sourceItem = model.getItems().stream()
.filter(item -> item.getFileEntry() == entry)
.findAny();
if (sourceItem.isEmpty()) { if (sourceItem.isEmpty()) {
return "?"; return "?";
} }
var name = sourceItem.get().getFinishedDownload().get() ? "Local" : DataStorage.get().getStoreDisplayName(entry.getFileSystem().getStore()).orElse("?"); var name =
sourceItem.get().downloadFinished().get()
? "Local"
: DataStorage.get()
.getStoreDisplayName(entry.getFileSystem()
.getStore())
.orElse("?");
return FileNames.getFileName(entry.getPath()) + " (" + name + ")"; return FileNames.getFileName(entry.getPath()) + " (" + name + ")";
}, stage.getAllDownloaded())) },
model.getAllDownloaded()))
.apply(struc -> struc.get().setMinHeight(150)) .apply(struc -> struc.get().setMinHeight(150))
.grow(false, true); .grow(false, true);
var dragNotice = new LabelComp(stage.getAllDownloaded().flatMap(aBoolean -> aBoolean ? AppI18n.observable("dragLocalFiles") : AppI18n.observable("dragFiles"))) var dragNotice = new LabelComp(model.getAllDownloaded()
.flatMap(aBoolean ->
aBoolean ? AppI18n.observable("dragLocalFiles") : AppI18n.observable("dragFiles")))
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2e-export"))) .apply(struc -> struc.get().setGraphic(new FontIcon("mdi2e-export")))
.hide(PlatformThread.sync( .hide(PlatformThread.sync(BindingsHelper.persist(Bindings.isEmpty(model.getItems()))))
BindingsHelper.persist(Bindings.isEmpty(stage.getItems()))))
.grow(true, false) .grow(true, false)
.apply(struc -> struc.get().setPadding(new Insets(8))); .apply(struc -> struc.get().setPadding(new Insets(8)));
var downloadButton = new IconButtonComp("mdi2d-download", () -> { var downloadButton = new IconButtonComp("mdi2d-download", () -> {
stage.download(); model.download();
}) })
.hide(BindingsHelper.persist(Bindings.isEmpty(stage.getItems()))) .hide(BindingsHelper.persist(Bindings.isEmpty(model.getItems())))
.disable(PlatformThread.sync(stage.getAllDownloaded())) .disable(PlatformThread.sync(model.getAllDownloaded()))
.apply(new FancyTooltipAugment<>("downloadStageDescription")); .apply(new FancyTooltipAugment<>("downloadStageDescription"));
var clearButton = new IconButtonComp("mdi2c-close", () -> { var clearButton = new IconButtonComp("mdi2c-close", () -> {
stage.clear(); model.clear();
}) })
.hide(BindingsHelper.persist(Bindings.isEmpty(stage.getItems()))); .hide(BindingsHelper.persist(Bindings.isEmpty(model.getItems())));
var clearPane = Comp.derive( var clearPane = Comp.derive(
new HorizontalComp(List.of(downloadButton, clearButton)) new HorizontalComp(List.of(downloadButton, clearButton))
.apply(struc -> struc.get().setSpacing(10)), .apply(struc -> struc.get().setSpacing(10)),
@ -93,37 +106,56 @@ public class BrowserTransferComp extends SimpleComp {
event.acceptTransferModes(TransferMode.ANY); event.acceptTransferModes(TransferMode.ANY);
event.consume(); event.consume();
} }
// Accept drops from outside the app window
if (event.getGestureSource() == null
&& !event.getDragboard().getFiles().isEmpty()) {
event.acceptTransferModes(TransferMode.ANY);
event.consume();
}
}); });
struc.get().setOnDragDropped(event -> { struc.get().setOnDragDropped(event -> {
// Accept drops from inside the app window
if (event.getGestureSource() != null) { if (event.getGestureSource() != null) {
var files = BrowserClipboard.retrieveDrag(event.getDragboard()) var drag = BrowserClipboard.retrieveDrag(event.getDragboard());
.getEntries(); if (drag == null) {
stage.drop(files); return;
}
var files = drag.getEntries();
model.drop(
model.getBrowserModel()
.getSelected()
.getValue(),
files);
event.setDropCompleted(true);
event.consume();
}
// Accept drops from outside the app window
if (event.getGestureSource() == null) {
model.dropLocal(event.getDragboard().getFiles());
event.setDropCompleted(true); event.setDropCompleted(true);
event.consume(); event.consume();
} }
}); });
struc.get().setOnDragDetected(event -> { struc.get().setOnDragDetected(event -> {
if (stage.getDownloading().get()) { if (model.getDownloading().get()) {
return; return;
} }
// Drag within browser var selected = model.getItems().stream()
if (!stage.getAllDownloaded().get()) { .map(BrowserTransferModel.Item::getFileEntry)
var selected = stage.getItems().stream().map(item -> item.getFileEntry()).toList(); .toList();
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY); Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
db.setContent(BrowserClipboard.startDrag(null, selected));
Image image = BrowserSelectionListComp.snapshot(FXCollections.observableList(selected)); var cc = BrowserClipboard.startDrag(null, selected);
db.setDragView(image, -20, 15); if (cc == null) {
event.setDragDetect(true);
event.consume();
return; return;
} }
// Drag outside browser var files = model.getItems().stream()
var files = stage.getItems().stream() .filter(item -> item.downloadFinished().get())
.map(item -> { .map(item -> {
try { try {
var file = item.getLocalFile(); var file = item.getLocalFile();
@ -131,40 +163,35 @@ public class BrowserTransferComp extends SimpleComp {
return Optional.<File>empty(); return Optional.<File>empty();
} }
return Optional.of(file return Optional.of(
.toRealPath() file.toRealPath().toFile());
.toFile());
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
}) })
.flatMap(Optional::stream) .flatMap(Optional::stream)
.toList(); .toList();
Dragboard db = struc.get().startDragAndDrop(TransferMode.MOVE);
var cc = new ClipboardContent();
cc.putFiles(files); cc.putFiles(files);
db.setContent(cc); db.setContent(cc);
var image = BrowserSelectionListComp.snapshot( Image image = BrowserSelectionListComp.snapshot(FXCollections.observableList(selected));
FXCollections.observableList(stage.getItems().stream()
.map(item -> item.getFileEntry())
.toList()));
db.setDragView(image, -20, 15); db.setDragView(image, -20, 15);
event.setDragDetect(true); event.setDragDetect(true);
event.consume(); event.consume();
}); });
struc.get().setOnDragDone(event -> { struc.get().setOnDragDone(event -> {
// macOS does always report false here // macOS does always report false here, which is unfortunate
if (!event.isAccepted()) { if (!event.isAccepted() && !OsType.getLocal().equals(OsType.MACOS)) {
return; return;
} }
stage.getItems().clear(); // Don't clear, it might be more convenient to keep the contents
// model.clear();
event.consume(); event.consume();
}); });
}), }),
PlatformThread.sync(stage.getDownloading())); PlatformThread.sync(model.getDownloading()));
return stack.styleClass("transfer").createRegion(); return stack.styleClass("transfer").createRegion();
} }
} }

View file

@ -2,16 +2,23 @@ package io.xpipe.app.browser;
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.core.store.FileNames; import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileSystem;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
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;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -21,8 +28,7 @@ import java.util.concurrent.Executors;
@Value @Value
public class BrowserTransferModel { public class BrowserTransferModel {
private static final Path TEMP = private static final Path TEMP = ShellTemp.getLocalTempDataDirectory("download");
FileUtils.getTempDirectory().toPath().resolve("xpipe").resolve("download");
ExecutorService executor = Executors.newSingleThreadExecutor(r -> { ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = Executors.defaultThreadFactory().newThread(r); Thread t = Executors.defaultThreadFactory().newThread(r);
@ -30,30 +36,32 @@ public class BrowserTransferModel {
t.setName("file downloader"); t.setName("file downloader");
return t; return t;
}); });
@Value
public static class Item {
String name;
FileSystem.FileEntry fileEntry;
Path localFile;
BooleanProperty finishedDownload = new SimpleBooleanProperty();
}
BrowserModel browserModel; BrowserModel browserModel;
ObservableList<Item> items = FXCollections.observableArrayList(); ObservableList<Item> items = FXCollections.observableArrayList();
BooleanProperty downloading = new SimpleBooleanProperty(); BooleanProperty downloading = new SimpleBooleanProperty();
BooleanProperty allDownloaded = new SimpleBooleanProperty(); BooleanProperty allDownloaded = new SimpleBooleanProperty();
public void clear() { private void cleanDirectory() {
try { if (!Files.isDirectory(TEMP)) {
FileUtils.deleteDirectory(TEMP.toFile()); return;
}
try (var ls = Files.list(TEMP)) {
var list = ls.toList();
for (Path path : list) {
FileUtils.forceDelete(path.toFile());
}
} catch (IOException e) { } catch (IOException e) {
ErrorEvent.fromThrowable(e).handle(); ErrorEvent.fromThrowable(e).handle();
} }
}
public void clear() {
cleanDirectory();
items.clear(); items.clear();
} }
public void drop(List<FileSystem.FileEntry> entries) { public void drop(OpenFileSystemModel model, List<FileSystem.FileEntry> entries) {
entries.forEach(entry -> { entries.forEach(entry -> {
var name = FileNames.getFileName(entry.getPath()); var name = FileNames.getFileName(entry.getPath());
if (items.stream().anyMatch(item -> item.getName().equals(name))) { if (items.stream().anyMatch(item -> item.getName().equals(name))) {
@ -61,12 +69,39 @@ public class BrowserTransferModel {
} }
Path file = TEMP.resolve(name); Path file = TEMP.resolve(name);
var item = new Item(name, entry, file); var item = new Item(model, name, entry, file);
items.add(item); items.add(item);
allDownloaded.set(false); allDownloaded.set(false);
}); });
} }
public void dropLocal(List<File> entries) {
if (entries.isEmpty()) {
return;
}
var empty = items.isEmpty();
try {
var paths = entries.stream().map(File::toPath).filter(Files::exists).toList();
for (Path path : paths) {
var entry = FileSystemHelper.getLocal(path);
var name = entry.getName();
if (items.stream().anyMatch(item -> item.getName().equals(name))) {
return;
}
var item = new Item(null, name, entry, path);
item.progress.setValue(BrowserTransferProgress.finished(entry.getName(), entry.getSize()));
items.add(item);
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
if (empty) {
allDownloaded.set(true);
}
}
public void download() { public void download() {
executor.submit(() -> { executor.submit(() -> {
try { try {
@ -77,18 +112,23 @@ public class BrowserTransferModel {
} }
for (Item item : new ArrayList<>(items)) { for (Item item : new ArrayList<>(items)) {
if (item.getFinishedDownload().get()) { if (item.downloadFinished().get()) {
continue;
}
if (item.getOpenFileSystemModel() != null
&& item.getOpenFileSystemModel().isClosed()) {
continue; continue;
} }
try { try {
try (var b = new BooleanScope(downloading).start()) { try (var b = new BooleanScope(downloading).start()) {
FileSystemHelper.dropFilesInto( FileSystemHelper.dropFilesInto(
FileSystemHelper.getLocal(TEMP), FileSystemHelper.getLocal(TEMP), List.of(item.getFileEntry()), true, false, progress -> {
List.of(item.getFileEntry()), item.getProgress().setValue(progress);
true); item.getOpenFileSystemModel().getProgress().setValue(progress);
});
} }
item.finishedDownload.set(true);
} catch (Throwable t) { } catch (Throwable t) {
ErrorEvent.fromThrowable(t).handle(); ErrorEvent.fromThrowable(t).handle();
items.remove(item); items.remove(item);
@ -97,4 +137,31 @@ public class BrowserTransferModel {
allDownloaded.set(true); allDownloaded.set(true);
}); });
} }
@Value
public static class Item {
OpenFileSystemModel openFileSystemModel;
String name;
FileSystem.FileEntry fileEntry;
Path localFile;
Property<BrowserTransferProgress> progress;
public Item(
OpenFileSystemModel openFileSystemModel, String name, FileSystem.FileEntry fileEntry, Path localFile) {
this.openFileSystemModel = openFileSystemModel;
this.name = name;
this.fileEntry = fileEntry;
this.localFile = localFile;
this.progress =
new SimpleObjectProperty<>(BrowserTransferProgress.empty(fileEntry.getName(), fileEntry.getSize()));
}
public ObservableBooleanValue downloadFinished() {
return Bindings.createBooleanBinding(
() -> {
return progress.getValue().done();
},
progress);
}
}
} }

View file

@ -0,0 +1,27 @@
package io.xpipe.app.browser;
import lombok.Value;
@Value
public class BrowserTransferProgress {
String name;
long transferred;
long total;
static BrowserTransferProgress empty() {
return new BrowserTransferProgress(null, 0, 0);
}
static BrowserTransferProgress empty(String name, long size) {
return new BrowserTransferProgress(name, 0, size);
}
static BrowserTransferProgress finished(String name, long size) {
return new BrowserTransferProgress(name, size, size);
}
public boolean done() {
return transferred >= total;
}
}

View file

@ -1,10 +1,10 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import atlantafx.base.controls.Spacer; import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.ListBoxViewComp; import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.base.TileButtonComp; import io.xpipe.app.comp.base.TileButtonComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.LabelComp; import io.xpipe.app.fxcomps.impl.LabelComp;
@ -42,7 +42,9 @@ public class BrowserWelcomeComp extends SimpleComp {
var vbox = new VBox(welcome, new Spacer(4, Orientation.VERTICAL)); var vbox = new VBox(welcome, new Spacer(4, Orientation.VERTICAL));
vbox.setAlignment(Pos.CENTER_LEFT); vbox.setAlignment(Pos.CENTER_LEFT);
var img = PrettyImageHelper.ofSvg(new SimpleStringProperty("Hips.svg"), 50, 75).padding(new Insets(5, 0, 0, 0)).createRegion(); var img = PrettyImageHelper.ofSvg(new SimpleStringProperty("Hips.svg"), 50, 75)
.padding(new Insets(5, 0, 0, 0))
.createRegion();
var hbox = new HBox(img, vbox); var hbox = new HBox(img, vbox);
hbox.setAlignment(Pos.CENTER_LEFT); hbox.setAlignment(Pos.CENTER_LEFT);
hbox.setSpacing(15); hbox.setSpacing(15);
@ -68,11 +70,15 @@ public class BrowserWelcomeComp extends SimpleComp {
}); });
var empty = Bindings.createBooleanBinding(() -> list.isEmpty(), list); var empty = Bindings.createBooleanBinding(() -> list.isEmpty(), list);
var header = new LabelComp(Bindings.createStringBinding(() -> { var header = new LabelComp(Bindings.createStringBinding(
return !empty.get() ? "You were recently connected to the following systems:" : () -> {
"Here you will be able to see where you left off last time."; return !empty.get()
}, empty)).createRegion(); ? "You were recently connected to the following systems:"
header.getStyleClass().add(Styles.TEXT_MUTED); : "Here you will be able to see where you left off last time.";
},
empty))
.createRegion();
AppFont.setSize(header, 1);
vbox.getChildren().add(header); vbox.getChildren().add(header);
var storeList = new VBox(); var storeList = new VBox();
@ -80,21 +86,31 @@ public class BrowserWelcomeComp extends SimpleComp {
var listBox = new ListBoxViewComp<>(list, list, e -> { var listBox = new ListBoxViewComp<>(list, list, e -> {
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid()); var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
var graphic = entry.get().getProvider().getDisplayIconFileName(entry.get().getStore()); var graphic = entry.get()
.getProvider()
.getDisplayIconFileName(entry.get().getStore());
var view = PrettyImageHelper.ofFixedSize(graphic, 50, 40); var view = PrettyImageHelper.ofFixedSize(graphic, 50, 40);
view.padding(new Insets(2, 8, 2, 8)); view.padding(new Insets(2, 8, 2, 8));
var content = var content = JfxHelper.createNamedEntry(
JfxHelper.createNamedEntry(DataStorage.get().getStoreDisplayName(entry.get()), e.getPath(), graphic); DataStorage.get().getStoreDisplayName(entry.get()), e.getPath(), graphic);
var disable = new SimpleBooleanProperty(); var disable = new SimpleBooleanProperty();
return new ButtonComp(null, content, () -> { return new ButtonComp(null, content, () -> {
ThreadHelper.runAsync(() -> { ThreadHelper.runAsync(() -> {
model.restoreStateAsync(e, disable); model.restoreStateAsync(e, disable);
}); });
}).accessibleText(DataStorage.get().getStoreDisplayName(entry.get())).disable(disable).styleClass("color-listBox").apply(struc -> struc.get().setMaxWidth(2000)).grow(true, false); })
}).apply(struc -> { .accessibleText(DataStorage.get().getStoreDisplayName(entry.get()))
.disable(disable)
.styleClass("color-listBox")
.apply(struc -> struc.get().setMaxWidth(2000))
.grow(true, false);
})
.apply(struc -> {
VBox vBox = (VBox) struc.get().getContent(); VBox vBox = (VBox) struc.get().getContent();
vBox.setSpacing(10); vBox.setSpacing(10);
}).hide(empty).createRegion(); })
.hide(empty)
.createRegion();
var layout = new VBox(); var layout = new VBox();
layout.getStyleClass().add("welcome"); layout.getStyleClass().add("welcome");
@ -109,7 +125,10 @@ public class BrowserWelcomeComp extends SimpleComp {
var tile = new TileButtonComp("restore", "restoreAllSessions", "mdmz-restore", actionEvent -> { var tile = new TileButtonComp("restore", "restoreAllSessions", "mdmz-restore", actionEvent -> {
model.restoreState(state); model.restoreState(state);
actionEvent.consume(); actionEvent.consume();
}).grow(true, false).hide(empty).accessibleTextKey("restoreAllSessions"); })
.grow(true, false)
.hide(empty)
.accessibleTextKey("restoreAllSessions");
layout.getChildren().add(tile.createRegion()); layout.getChildren().add(tile.createRegion());
return layout; return layout;

View file

@ -7,13 +7,22 @@ import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileSystem;
import io.xpipe.core.store.LocalStore; import io.xpipe.core.store.LocalStore;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.LinkedHashMap; 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 = 16384;
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;
@ -114,7 +123,8 @@ public class FileSystemHelper {
} }
if (!model.getFileSystem().directoryExists(path)) { if (!model.getFileSystem().directoryExists(path)) {
throw ErrorEvent.unreportable(new IllegalArgumentException(String.format("Directory %s does not exist", path))); throw ErrorEvent.unreportable(
new IllegalArgumentException(String.format("Directory %s does not exist", path)));
} }
try { try {
@ -125,8 +135,6 @@ public class FileSystemHelper {
} }
} }
private static FileSystem localFileSystem;
public static FileSystem.FileEntry getLocal(Path file) throws Exception { public static FileSystem.FileEntry getLocal(Path file) throws Exception {
if (localFileSystem == null) { if (localFileSystem == null) {
localFileSystem = new LocalStore().createFileSystem(); localFileSystem = new LocalStore().createFileSystem();
@ -144,8 +152,8 @@ public class FileSystemHelper {
Files.isDirectory(file) ? FileKind.DIRECTORY : FileKind.FILE); Files.isDirectory(file) ? FileKind.DIRECTORY : FileKind.FILE);
} }
public static void dropLocalFilesInto(FileSystem.FileEntry entry, List<Path> files) { public static void dropLocalFilesInto(
try { FileSystem.FileEntry entry, List<Path> files, Consumer<BrowserTransferProgress> progress, boolean checkConflicts) throws Exception {
var entries = files.stream() var entries = files.stream()
.map(path -> { .map(path -> {
try { try {
@ -155,14 +163,11 @@ public class FileSystemHelper {
} }
}) })
.toList(); .toList();
dropFilesInto(entry, entries, false); dropFilesInto(entry, entries, false, checkConflicts, progress);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
} }
public static void delete(List<FileSystem.FileEntry> files) { public static void delete(List<FileSystem.FileEntry> files) {
if (files.size() == 0) { if (files.isEmpty()) {
return; return;
} }
@ -176,22 +181,43 @@ public class FileSystemHelper {
} }
public static void dropFilesInto( public static void dropFilesInto(
FileSystem.FileEntry target, List<FileSystem.FileEntry> files, boolean explicitCopy) throws Exception { FileSystem.FileEntry target,
if (files.size() == 0) { List<FileSystem.FileEntry> files,
boolean explicitCopy,
boolean checkConflicts,
Consumer<BrowserTransferProgress> progress)
throws Exception {
if (files.isEmpty()) {
progress.accept(BrowserTransferProgress.empty());
return; 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) { for (var file : files) {
if (file.getFileSystem().equals(target.getFileSystem())) { if (file.getFileSystem().equals(target.getFileSystem())) {
dropFileAcrossSameFileSystem(target, file, explicitCopy); dropFileAcrossSameFileSystem(target, file, explicitCopy, lastConflictChoice, files.size() > 1, checkConflicts);
progress.accept(BrowserTransferProgress.finished(file.getName(), file.getSize()));
} else { } else {
dropFileAcrossFileSystems(target, file); dropFileAcrossFileSystems(target, file, progress, lastConflictChoice, files.size() > 1, checkConflicts);
} }
} }
} }
private static void dropFileAcrossSameFileSystem( private static void dropFileAcrossSameFileSystem(
FileSystem.FileEntry target, FileSystem.FileEntry source, boolean explicitCopy) throws Exception { FileSystem.FileEntry target,
FileSystem.FileEntry source,
boolean explicitCopy,
AtomicReference<BrowserAlerts.FileConflictChoice> lastConflictChoice,
boolean multiple,
boolean checkConflicts)
throws Exception {
// Prevent dropping directory into itself // Prevent dropping directory into itself
if (source.getPath().equals(target.getPath())) { if (source.getPath().equals(target.getPath())) {
return; return;
@ -205,7 +231,12 @@ public class FileSystemHelper {
} }
if (source.getKind() == FileKind.DIRECTORY && target.getFileSystem().directoryExists(targetFile)) { if (source.getKind() == FileKind.DIRECTORY && target.getFileSystem().directoryExists(targetFile)) {
throw ErrorEvent.unreportable(new IllegalArgumentException("Target directory " + targetFile + " does already exist")); throw ErrorEvent.unreportable(
new IllegalArgumentException("Target directory " + targetFile + " does already exist"));
}
if (checkConflicts && !handleChoice(lastConflictChoice, target.getFileSystem(), targetFile, multiple)) {
return;
} }
if (explicitCopy) { if (explicitCopy) {
@ -215,7 +246,13 @@ public class FileSystemHelper {
} }
} }
private static void dropFileAcrossFileSystems(FileSystem.FileEntry target, FileSystem.FileEntry source) private static void dropFileAcrossFileSystems(
FileSystem.FileEntry target,
FileSystem.FileEntry source,
Consumer<BrowserTransferProgress> progress,
AtomicReference<BrowserAlerts.FileConflictChoice> lastConflictChoice,
boolean multiple,
boolean checkConflicts)
throws Exception { throws Exception {
if (target.getKind() != FileKind.DIRECTORY) { if (target.getKind() != FileKind.DIRECTORY) {
throw new IllegalStateException("Target " + target.getPath() + " is not a directory"); throw new IllegalStateException("Target " + target.getPath() + " is not a directory");
@ -229,19 +266,27 @@ public class FileSystemHelper {
return; return;
} }
AtomicLong totalSize = new AtomicLong();
if (source.getKind() == FileKind.DIRECTORY) { if (source.getKind() == FileKind.DIRECTORY) {
var directoryName = FileNames.getFileName(source.getPath()); var directoryName = FileNames.getFileName(source.getPath());
flatFiles.put(source, directoryName); flatFiles.put(source, directoryName);
var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath())); var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath()));
List<FileSystem.FileEntry> list = source.getFileSystem().listFilesRecursively(source.getPath()); List<FileSystem.FileEntry> list = source.getFileSystem().listFilesRecursively(source.getPath());
list.forEach(fileEntry -> { for (FileSystem.FileEntry fileEntry : list) {
flatFiles.put(fileEntry, FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath()))); flatFiles.put(fileEntry, FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath())));
}); if (fileEntry.getKind() == FileKind.FILE) {
// This one is up-to-date and does not need to be recalculated
totalSize.addAndGet(fileEntry.getSize());
}
}
} else { } else {
flatFiles.put(source, FileNames.getFileName(source.getPath())); 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()) { for (var e : flatFiles.entrySet()) {
var sourceFile = e.getKey(); var sourceFile = e.getKey();
var targetFile = FileNames.join(target.getPath(), e.getValue()); var targetFile = FileNames.join(target.getPath(), e.getValue());
@ -252,11 +297,127 @@ public class FileSystemHelper {
if (sourceFile.getKind() == FileKind.DIRECTORY) { if (sourceFile.getKind() == FileKind.DIRECTORY) {
target.getFileSystem().mkdirs(targetFile); target.getFileSystem().mkdirs(targetFile);
} else if (sourceFile.getKind() == FileKind.FILE) { } else if (sourceFile.getKind() == FileKind.FILE) {
try (var in = sourceFile.getFileSystem().openInput(sourceFile.getPath()); if (checkConflicts && !handleChoice(
var out = target.getFileSystem().openOutput(targetFile)) { lastConflictChoice, target.getFileSystem(), targetFile, multiple || flatFiles.size() > 1)) {
in.transferTo(out); 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;
}
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

@ -6,9 +6,12 @@ import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.TerminalHelper; import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.*; import io.xpipe.core.process.ProcessControlProvider;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialects;
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;
@ -27,20 +30,21 @@ import java.util.stream.Stream;
public final class OpenFileSystemModel { public final class OpenFileSystemModel {
private final DataStoreEntryRef<? extends FileSystemStore> entry; private final DataStoreEntryRef<? extends FileSystemStore> entry;
private FileSystem fileSystem;
private final Property<String> filter = new SimpleStringProperty(); private final Property<String> filter = new SimpleStringProperty();
private final BrowserFileListModel fileList; private final BrowserFileListModel fileList;
private final ReadOnlyObjectWrapper<String> currentPath = new ReadOnlyObjectWrapper<>(); private final ReadOnlyObjectWrapper<String> currentPath = new ReadOnlyObjectWrapper<>();
private final OpenFileSystemHistory history = new OpenFileSystemHistory(); private final OpenFileSystemHistory history = new OpenFileSystemHistory();
private final BooleanProperty busy = new SimpleBooleanProperty(); private final BooleanProperty busy = new SimpleBooleanProperty();
private final BrowserModel browserModel; private final BrowserModel browserModel;
private OpenFileSystemSavedState savedState;
private OpenFileSystemCache cache;
private final Property<ModalOverlayComp.OverlayContent> overlay = new SimpleObjectProperty<>(); private final Property<ModalOverlayComp.OverlayContent> overlay = new SimpleObjectProperty<>();
private final BooleanProperty inOverview = new SimpleBooleanProperty(); private final BooleanProperty inOverview = new SimpleBooleanProperty();
private final String name; private final String name;
private final String tooltip; private final String tooltip;
private boolean local; private final Property<BrowserTransferProgress> progress =
new SimpleObjectProperty<>(BrowserTransferProgress.empty());
private FileSystem fileSystem;
private OpenFileSystemSavedState savedState;
private OpenFileSystemCache cache;
private int customScriptsStartIndex; private int customScriptsStartIndex;
public OpenFileSystemModel(BrowserModel browserModel, DataStoreEntryRef<? extends FileSystemStore> entry) { public OpenFileSystemModel(BrowserModel browserModel, DataStoreEntryRef<? extends FileSystemStore> entry) {
@ -56,6 +60,24 @@ public final class OpenFileSystemModel {
fileList = new BrowserFileListModel(this); fileList = new BrowserFileListModel(this);
} }
public boolean isBusy() {
return !progress.getValue().done()
|| (fileSystem != null
&& fileSystem.getShell().isPresent()
&& fileSystem.getShell().get().getLock().isLocked());
}
private void startIfNeeded() throws Exception {
if (fileSystem == null) {
return;
}
var s = fileSystem.getShell();
if (s.isPresent()) {
s.get().start();
}
}
public void withShell(FailableConsumer<ShellControl, Exception> c, boolean refresh) { public void withShell(FailableConsumer<ShellControl, Exception> c, boolean refresh) {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
if (fileSystem == null) { if (fileSystem == null) {
@ -131,8 +153,13 @@ public final class OpenFileSystemModel {
return Optional.empty(); return Optional.empty();
} }
try {
// Start shell in case we exited // Start shell in case we exited
getFileSystem().getShell().orElseThrow().start(); startIfNeeded();
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
return Optional.ofNullable(currentPath.get());
}
// Fix common issues with paths // Fix common issues with paths
var adjustedPath = FileSystemHelper.adjustPath(this, path); var adjustedPath = FileSystemHelper.adjustPath(this, path);
@ -158,26 +185,19 @@ public final class OpenFileSystemModel {
var directory = currentPath.get(); var directory = currentPath.get();
var name = adjustedPath + " - " + entry.get().getName(); var name = adjustedPath + " - " + entry.get().getName();
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
if (ShellDialects.getStartableDialects().stream().anyMatch(dialect -> adjustedPath.startsWith(dialect.getOpenCommand()))) { if (ShellDialects.getStartableDialects().stream()
TerminalHelper.open( .anyMatch(dialect -> adjustedPath.startsWith(dialect.getOpenCommand(null)))) {
TerminalLauncher.open(
entry.getEntry(), entry.getEntry(),
name, name,
fileSystem directory,
.getShell() fileSystem.getShell().get().singularSubShell(ShellOpenFunction.of(adjustedPath)));
.get()
.subShell(processControl -> adjustedPath, (sc) -> adjustedPath)
.withInitSnippet(new SimpleScriptSnippet(
fileSystem
.getShell()
.get()
.getShellDialect()
.getCdCommand(currentPath.get()),
ScriptSnippet.ExecutionType.BOTH)));
} else { } else {
TerminalHelper.open( TerminalLauncher.open(
entry.getEntry(), entry.getEntry(),
name, name,
fileSystem.getShell().get().command(adjustedPath).withWorkingDirectory(directory)); directory,
fileSystem.getShell().get().command(adjustedPath));
} }
}); });
return Optional.ofNullable(currentPath.get()); return Optional.ofNullable(currentPath.get());
@ -227,6 +247,7 @@ public final class OpenFileSystemModel {
private boolean loadFilesSync(String dir) { private boolean loadFilesSync(String dir) {
try { try {
if (dir != null) { if (dir != null) {
startIfNeeded();
var stream = getFileSystem().listFiles(dir); var stream = getFileSystem().listFiles(dir);
fileList.setAll(stream); fileList.setAll(stream);
} else { } else {
@ -247,7 +268,8 @@ public final class OpenFileSystemModel {
return; return;
} }
FileSystemHelper.dropLocalFilesInto(entry, files); startIfNeeded();
FileSystemHelper.dropLocalFilesInto(entry, files, progress::setValue, true);
refreshSync(); refreshSync();
}); });
}); });
@ -266,14 +288,10 @@ public final class OpenFileSystemModel {
return; return;
} }
var same = files.get(0).getFileSystem().equals(target.getFileSystem()); startIfNeeded();
if (same && !explicitCopy) { FileSystemHelper.dropFilesInto(target, files, explicitCopy, true, browserTransferProgress -> {
if (!BrowserAlerts.showMoveAlert(files, target)) { progress.setValue(browserTransferProgress);
return; });
}
}
FileSystemHelper.dropFilesInto(target, files, explicitCopy);
refreshSync(); refreshSync();
}); });
}); });
@ -294,9 +312,11 @@ public final class OpenFileSystemModel {
return; return;
} }
startIfNeeded();
var abs = FileNames.join(getCurrentDirectory().getPath(), name); var abs = FileNames.join(getCurrentDirectory().getPath(), name);
if (fileSystem.directoryExists(abs)) { if (fileSystem.directoryExists(abs)) {
throw ErrorEvent.unreportable(new IllegalStateException(String.format("Directory %s already exists", abs))); throw ErrorEvent.unreportable(
new IllegalStateException(String.format("Directory %s already exists", abs)));
} }
fileSystem.mkdirs(abs); fileSystem.mkdirs(abs);
@ -320,6 +340,7 @@ public final class OpenFileSystemModel {
return; return;
} }
startIfNeeded();
var abs = FileNames.join(getCurrentDirectory().getPath(), linkName); var abs = FileNames.join(getCurrentDirectory().getPath(), linkName);
fileSystem.symbolicLink(abs, targetFile); fileSystem.symbolicLink(abs, targetFile);
refreshSync(); refreshSync();
@ -370,14 +391,12 @@ public final class OpenFileSystemModel {
BooleanScope.execute(busy, () -> { BooleanScope.execute(busy, () -> {
var fs = entry.getStore().createFileSystem(); var fs = entry.getStore().createFileSystem();
if (fs.getShell().isPresent()) { if (fs.getShell().isPresent()) {
this.customScriptsStartIndex = fs.getShell().get().getInitCommands().size(); this.customScriptsStartIndex =
fs.getShell().get().getInitCommands().size();
ProcessControlProvider.get().withDefaultScripts(fs.getShell().get()); ProcessControlProvider.get().withDefaultScripts(fs.getShell().get());
} }
fs.open(); fs.open();
this.fileSystem = fs; this.fileSystem = fs;
this.local = fs.getShell()
.map(shellControl -> shellControl.hasLocalSystemAccess())
.orElse(false);
this.cache = new OpenFileSystemCache(this); this.cache = new OpenFileSystemCache(this);
for (BrowserAction b : BrowserAction.ALL) { for (BrowserAction b : BrowserAction.ALL) {
@ -408,21 +427,12 @@ public final class OpenFileSystemModel {
BooleanScope.execute(busy, () -> { BooleanScope.execute(busy, () -> {
if (fileSystem.getShell().isPresent()) { if (fileSystem.getShell().isPresent()) {
var connection = fileSystem.getShell().get(); var connection = fileSystem.getShell().get();
var snippet = directory != null ? new SimpleScriptSnippet(connection.getShellDialect().getCdCommand(directory), var name = (directory != null ? directory + " - " : "")
ScriptSnippet.ExecutionType.BOTH) : null; + entry.get().getName();
if (snippet != null) { TerminalLauncher.open(entry.getEntry(), name, directory, connection);
connection.getInitCommands().add(customScriptsStartIndex,snippet);
}
try {
var name = (directory != null ? directory + " - " : "") + entry.get().getName();
TerminalHelper.open(entry.getEntry(), name, connection);
// Restart connection as we will have to start it anyway, so we speed it up by doing it preemptively // Restart connection as we will have to start it anyway, so we speed it up by doing it preemptively
connection.start(); startIfNeeded();
} finally {
connection.getInitCommands().remove(snippet);
}
} }
}); });
}); });

View file

@ -33,6 +33,86 @@ import java.util.stream.Collectors;
@JsonDeserialize(using = OpenFileSystemSavedState.Deserializer.class) @JsonDeserialize(using = OpenFileSystemSavedState.Deserializer.class)
public class OpenFileSystemSavedState { public class OpenFileSystemSavedState {
private static final Timer TIMEOUT_TIMER = new Timer(true);
private static final int STORED = 10;
@Setter
private OpenFileSystemModel model;
private String lastDirectory;
@NonNull
private ObservableList<RecentEntry> recentDirectories;
public OpenFileSystemSavedState(String lastDirectory, @NonNull ObservableList<RecentEntry> recentDirectories) {
this.lastDirectory = lastDirectory;
this.recentDirectories = recentDirectories;
}
public OpenFileSystemSavedState() {
lastDirectory = null;
recentDirectories = FXCollections.observableList(new ArrayList<>(STORED));
}
static OpenFileSystemSavedState loadForStore(OpenFileSystemModel model) {
var state = AppCache.get("fs-state-" + model.getEntry().get().getUuid(), OpenFileSystemSavedState.class, () -> {
return new OpenFileSystemSavedState();
});
state.setModel(model);
return state;
}
public void save() {
if (model == null) {
return;
}
AppCache.update("fs-state-" + model.getEntry().get().getUuid(), this);
}
public void cd(String dir) {
if (dir == null) {
lastDirectory = null;
return;
}
lastDirectory = dir;
// After 10 seconds
TIMEOUT_TIMER.schedule(
new TimerTask() {
@Override
public void run() {
// Synchronize with platform thread
Platform.runLater(() -> {
if (model.isClosed()) {
return;
}
if (Objects.equals(lastDirectory, dir)) {
updateRecent(dir);
save();
}
});
}
},
10000);
}
private void updateRecent(String dir) {
var without = FileNames.removeTrailingSlash(dir);
var with = FileNames.toDirectory(dir);
recentDirectories.removeIf(recentEntry ->
Objects.equals(recentEntry.directory, without) || Objects.equals(recentEntry.directory, with));
var o = new RecentEntry(with, Instant.now());
if (recentDirectories.size() < STORED) {
recentDirectories.addFirst(o);
} else {
recentDirectories.removeLast();
recentDirectories.addFirst(o);
}
}
public static class Serializer extends StdSerializer<OpenFileSystemSavedState> { public static class Serializer extends StdSerializer<OpenFileSystemSavedState> {
protected Serializer() { protected Serializer() {
@ -79,14 +159,6 @@ public class OpenFileSystemSavedState {
} }
} }
static OpenFileSystemSavedState loadForStore(OpenFileSystemModel model) {
var state = AppCache.get("fs-state-" + model.getEntry().get().getUuid(), OpenFileSystemSavedState.class, () -> {
return new OpenFileSystemSavedState();
});
state.setModel(model);
return state;
}
@Value @Value
@Jacksonized @Jacksonized
@Builder @Builder
@ -95,76 +167,4 @@ public class OpenFileSystemSavedState {
String directory; String directory;
Instant time; Instant time;
} }
@Setter
private OpenFileSystemModel model;
private String lastDirectory;
@NonNull
private ObservableList<RecentEntry> recentDirectories;
public OpenFileSystemSavedState(String lastDirectory, @NonNull ObservableList<RecentEntry> recentDirectories) {
this.lastDirectory = lastDirectory;
this.recentDirectories = recentDirectories;
}
private static final Timer TIMEOUT_TIMER = new Timer(true);
private static final int STORED = 10;
public OpenFileSystemSavedState() {
lastDirectory = null;
recentDirectories = FXCollections.observableList(new ArrayList<>(STORED));
}
public void save() {
if (model == null) {
return;
}
AppCache.update("fs-state-" + model.getEntry().get().getUuid(), this);
}
public void cd(String dir) {
if (dir == null) {
lastDirectory = null;
return;
}
lastDirectory = dir;
// After 10 seconds
TIMEOUT_TIMER.schedule(
new TimerTask() {
@Override
public void run() {
// Synchronize with platform thread
Platform.runLater(() -> {
if (model.isClosed()) {
return;
}
if (Objects.equals(lastDirectory, dir)) {
updateRecent(dir);
save();
}
});
}
},
10000);
}
private void updateRecent(String dir) {
var without = FileNames.removeTrailingSlash(dir);
var with = FileNames.toDirectory(dir);
recentDirectories.removeIf(recentEntry ->
Objects.equals(recentEntry.directory, without) || Objects.equals(recentEntry.directory, with));
var o = new RecentEntry(with, Instant.now());
if (recentDirectories.size() < STORED) {
recentDirectories.add(0, o);
} else {
recentDirectories.remove(recentDirectories.size() - 1);
recentDirectories.add(0, o);
}
}
} }

View file

@ -39,7 +39,8 @@ public class StandaloneFileBrowser {
}); });
} }
public static void openSingleFile(Supplier<DataStoreEntryRef<? extends FileSystemStore>> store, Consumer<FileReference> file) { public static void openSingleFile(
Supplier<DataStoreEntryRef<? extends FileSystemStore>> store, Consumer<FileReference> file) {
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {
var model = new BrowserModel(BrowserModel.Mode.SINGLE_FILE_CHOOSER, null); var model = new BrowserModel(BrowserModel.Mode.SINGLE_FILE_CHOOSER, null);
var comp = new BrowserComp(model) var comp = new BrowserComp(model)
@ -47,7 +48,7 @@ public class StandaloneFileBrowser {
.apply(struc -> AppFont.normal(struc.get())); .apply(struc -> AppFont.normal(struc.get()));
var window = AppWindowHelper.sideWindow(AppI18n.get("openFileTitle"), stage -> comp, false, null); var window = AppWindowHelper.sideWindow(AppI18n.get("openFileTitle"), stage -> comp, false, null);
model.setOnFinish(fileStores -> { model.setOnFinish(fileStores -> {
file.accept(fileStores.size() > 0 ? fileStores.get(0) : null); file.accept(fileStores.size() > 0 ? fileStores.getFirst() : null);
window.close(); window.close();
}); });
window.show(); window.show();
@ -63,7 +64,7 @@ public class StandaloneFileBrowser {
.apply(struc -> AppFont.normal(struc.get())); .apply(struc -> AppFont.normal(struc.get()));
var window = AppWindowHelper.sideWindow(AppI18n.get("saveFileTitle"), stage -> comp, true, null); var window = AppWindowHelper.sideWindow(AppI18n.get("saveFileTitle"), stage -> comp, true, null);
model.setOnFinish(fileStores -> { model.setOnFinish(fileStores -> {
file.setValue(fileStores.size() > 0 ? fileStores.get(0) : null); file.setValue(fileStores.size() > 0 ? fileStores.getFirst() : null);
window.close(); window.close();
}); });
window.show(); window.show();

View file

@ -13,14 +13,6 @@ import java.util.ServiceLoader;
public interface BrowserAction { public interface BrowserAction {
enum Category {
CUSTOM,
OPEN,
NATIVE,
COPY_PASTE,
MUTATION
}
List<BrowserAction> ALL = new ArrayList<>(); List<BrowserAction> ALL = new ArrayList<>();
static List<LeafAction> getFlattened(OpenFileSystemModel model, List<BrowserEntry> entries) { static List<LeafAction> getFlattened(OpenFileSystemModel model, List<BrowserEntry> entries) {
@ -39,7 +31,7 @@ public interface BrowserAction {
.orElseThrow(); .orElseThrow();
} }
default void init(OpenFileSystemModel model) throws Exception {} default void init(OpenFileSystemModel model) {}
default String getProFeatureId() { default String getProFeatureId() {
return null; return null;
@ -75,6 +67,14 @@ public interface BrowserAction {
return true; return true;
} }
enum Category {
CUSTOM,
OPEN,
NATIVE,
COPY_PASTE,
MUTATION
}
class Loader implements ModuleLayerLoader { class Loader implements ModuleLayerLoader {
@Override @Override

View file

@ -7,7 +7,7 @@ import java.util.List;
public class BrowserActionFormatter { public class BrowserActionFormatter {
public static String filesArgument(List<BrowserEntry> entries) { public static String filesArgument(List<BrowserEntry> entries) {
return entries.size() == 1 ? entries.get(0).getOptionallyQuotedFileName() : "(" + entries.size() + ")"; return entries.size() == 1 ? entries.getFirst().getOptionallyQuotedFileName() : "(" + entries.size() + ")";
} }
public static String centerEllipsis(String input, int length) { public static String centerEllipsis(String input, int length) {

View file

@ -52,7 +52,8 @@ public interface LeafAction extends BrowserAction {
b.setDisable(!isActive(model, selected)); b.setDisable(!isActive(model, selected));
}); });
if (getProFeatureId() != null && !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) { if (getProFeatureId() != null
&& !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) {
b.setDisable(true); b.setDisable(true);
b.setGraphic(new FontIcon("mdi2p-professional-hexagon")); b.setGraphic(new FontIcon("mdi2p-professional-hexagon"));
} }
@ -83,7 +84,8 @@ public interface LeafAction extends BrowserAction {
mi.setMnemonicParsing(false); mi.setMnemonicParsing(false);
mi.setDisable(!isActive(model, selected)); mi.setDisable(!isActive(model, selected));
if (getProFeatureId() != null && !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) { if (getProFeatureId() != null
&& !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) {
mi.setDisable(true); mi.setDisable(true);
mi.setText(mi.getText() + " (Pro)"); mi.setText(mi.getText() + " (Pro)");
} }

View file

@ -4,7 +4,7 @@ import io.xpipe.app.browser.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.ApplicationHelper; import io.xpipe.app.util.ApplicationHelper;
import io.xpipe.app.util.TerminalHelper; import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;
import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FilenameUtils;
@ -24,25 +24,30 @@ public abstract class MultiExecuteAction implements BranchAction {
model.withShell( model.withShell(
pc -> { pc -> {
for (BrowserEntry entry : entries) { for (BrowserEntry entry : entries) {
TerminalHelper.open(model.getEntry().getEntry(), FilenameUtils.getBaseName( TerminalLauncher.open(
entry.getRawFileEntry().getPath()), pc.command(createCommand(pc, model, entry)) model.getEntry().getEntry(),
.withWorkingDirectory(model.getCurrentDirectory() FilenameUtils.getBaseName(
.getPath())); entry.getRawFileEntry().getPath()),
model.getCurrentDirectory() != null
? model.getCurrentDirectory()
.getPath()
: null,
pc.command(createCommand(pc, model, entry)));
} }
}, },
false); false);
} }
@Override
public boolean isApplicable(OpenFileSystemModel model, List<BrowserEntry> entries) {
return AppPrefs.get().terminalType().getValue() != null;
}
@Override @Override
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) { public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
var t = AppPrefs.get().terminalType().getValue(); var t = AppPrefs.get().terminalType().getValue();
return "in " + (t != null ? t.toTranslatedString() : "?"); return "in " + (t != null ? t.toTranslatedString() : "?");
} }
@Override
public boolean isApplicable(OpenFileSystemModel model, List<BrowserEntry> entries) {
return AppPrefs.get().terminalType().getValue() != null;
}
}, },
new LeafAction() { new LeafAction() {
@ -51,7 +56,8 @@ public abstract class MultiExecuteAction implements BranchAction {
model.withShell( model.withShell(
pc -> { pc -> {
for (BrowserEntry entry : entries) { for (BrowserEntry entry : entries) {
var cmd = ApplicationHelper.createDetachCommand(pc, createCommand(pc, model, entry)); var cmd = ApplicationHelper.createDetachCommand(
pc, createCommand(pc, model, entry));
pc.command(cmd) pc.command(cmd)
.withWorkingDirectory(model.getCurrentDirectory() .withWorkingDirectory(model.getCurrentDirectory()
.getPath()) .getPath())

View file

@ -9,7 +9,10 @@ import java.io.BufferedReader;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.*; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public interface DirectoryType { public interface DirectoryType {
@ -71,6 +74,12 @@ public interface DirectoryType {
}); });
} }
String getId();
boolean matches(FileSystem.FileEntry entry);
String getIcon(FileSystem.FileEntry entry, boolean open);
class Simple implements DirectoryType { class Simple implements DirectoryType {
@Getter @Getter
@ -101,10 +110,4 @@ public interface DirectoryType {
return open ? this.open.getIcon() : this.closed.getIcon(); return open ? this.open.getIcon() : this.closed.getIcon();
} }
} }
String getId();
boolean matches(FileSystem.FileEntry entry);
String getIcon(FileSystem.FileEntry entry, boolean open);
} }

View file

@ -53,6 +53,12 @@ public interface FileType {
}); });
} }
String getId();
boolean matches(FileSystem.FileEntry entry);
String getIcon();
@Getter @Getter
class Simple implements FileType { class Simple implements FileType {
@ -72,7 +78,9 @@ public interface FileType {
return false; return false;
} }
return (entry.getExtension() != null && endings.contains("." + entry.getExtension().toLowerCase(Locale.ROOT))) || endings.contains(entry.getName()); return (entry.getExtension() != null
&& endings.contains("." + entry.getExtension().toLowerCase(Locale.ROOT)))
|| endings.contains(entry.getName());
} }
@Override @Override
@ -80,10 +88,4 @@ public interface FileType {
return icon.getIcon(); return icon.getIcon();
} }
} }
String getId();
boolean matches(FileSystem.FileEntry entry);
String getIcon();
} }

View file

@ -9,6 +9,7 @@ import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane; import javafx.scene.layout.Pane;
@ -31,13 +32,14 @@ public class AppLayoutComp extends Comp<CompStructure<Pane>> {
model.getSelected()))))); model.getSelected())))));
var pane = new BorderPane(); var pane = new BorderPane();
var sidebar = new SideMenuBarComp(model.getSelected(), model.getEntries()); var sidebar = new SideMenuBarComp(model.getSelectedInternal(), model.getEntries());
pane.setCenter(multi.createRegion()); pane.setCenter(multi.createRegion());
pane.setRight(sidebar.createRegion()); pane.setRight(sidebar.createRegion());
pane.getStyleClass().add("background"); pane.getStyleClass().add("background");
model.getSelected().addListener((c, o, n) -> { model.getSelected().addListener((c, o, n) -> {
if (o != null && o.equals(model.getEntries().get(2))) { if (o != null && o.equals(model.getEntries().get(2))) {
AppPrefs.get().save(); AppPrefs.get().save();
DataStorage.get().saveAsync();
} }
}); });
AppFont.normal(pane); AppFont.normal(pane);

View file

@ -0,0 +1,103 @@
package io.xpipe.app.comp.base;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import javafx.application.Platform;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.List;
import java.util.function.Function;
public abstract class DialogComp extends Comp<CompStructure<Region>> {
public static void showWindow(String titleKey, Function<Stage, DialogComp> f) {
var loading = new SimpleBooleanProperty();
Platform.runLater(() -> {
var stage = AppWindowHelper.sideWindow(
AppI18n.get(titleKey),
window -> {
var c = f.apply(window);
loading.bind(c.busy());
return c;
},
false,
loading);
stage.show();
});
}
protected Region createStepNavigation() {
HBox buttons = new HBox();
buttons.setFillHeight(true);
var customButton = bottom();
if (customButton != null) {
buttons.getChildren().add(customButton.createRegion());
}
buttons.getChildren().add(new Spacer());
buttons.getStyleClass().add("buttons");
buttons.setSpacing(5);
buttons.setAlignment(Pos.CENTER_RIGHT);
buttons.getChildren()
.addAll(customButtons().stream()
.map(buttonComp -> buttonComp.createRegion())
.toList());
var nextButton = new ButtonComp(AppI18n.observable("finishStep"), null, this::finish)
.apply(struc -> struc.get().setDefaultButton(true))
.styleClass(Styles.ACCENT)
.styleClass("next");
buttons.getChildren().add(nextButton.createRegion());
return buttons;
}
protected List<Comp<?>> customButtons() {
return List.of();
}
@Override
public CompStructure<Region> createBase() {
var sp = scrollPane(content()).createRegion();
VBox vbox = new VBox();
vbox.getChildren().addAll(sp, createStepNavigation());
vbox.getStyleClass().add("dialog-comp");
vbox.setFillWidth(true);
VBox.setVgrow(sp, Priority.ALWAYS);
return new SimpleCompStructure<>(vbox);
}
protected ObservableValue<Boolean> busy() {
return new SimpleBooleanProperty(false);
}
protected abstract void finish();
public abstract Comp<?> content();
protected Comp<?> scrollPane(Comp<?> content) {
var entry = content.styleClass("dialog-content");
return Comp.of(() -> {
var entryR = entry.createRegion();
var sp = new ScrollPane(entryR);
sp.setFitToWidth(true);
entryR.minHeightProperty().bind(sp.heightProperty());
return sp;
});
}
public Comp<?> bottom() {
return null;
}
}

View file

@ -45,7 +45,7 @@ public class IntegratedTextAreaComp extends SimpleComp {
c.getChildren().addAll(textArea, pane); c.getChildren().addAll(textArea, pane);
return c; return c;
}), }),
paths -> value.setValue(Files.readString(paths.get(0)))); paths -> value.setValue(Files.readString(paths.getFirst())));
return fileDrop.createRegion(); return fileDrop.createRegion();
} }

View file

@ -1,6 +1,5 @@
package io.xpipe.app.comp.base; package io.xpipe.app.comp.base;
import com.jfoenix.controls.JFXTextField;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
@ -28,7 +27,7 @@ public class LazyTextFieldComp extends Comp<LazyTextFieldComp.Structure> {
@Override @Override
public LazyTextFieldComp.Structure createBase() { public LazyTextFieldComp.Structure createBase() {
var sp = new StackPane(); var sp = new StackPane();
var r = new JFXTextField(); var r = new TextField();
r.setOnKeyPressed(ke -> { r.setOnKeyPressed(ke -> {
if (ke.getCode().equals(KeyCode.ESCAPE)) { if (ke.getCode().equals(KeyCode.ESCAPE)) {
@ -69,8 +68,7 @@ public class LazyTextFieldComp extends Comp<LazyTextFieldComp.Structure> {
SimpleChangeListener.apply(currentValue, n -> { SimpleChangeListener.apply(currentValue, n -> {
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {
// Check if control value is the same. Then don't set it as that might cause bugs // Check if control value is the same. Then don't set it as that might cause bugs
if (Objects.equals(r.getText(), n) if (Objects.equals(r.getText(), n) || (n == null && r.getText().isEmpty())) {
|| (n == null && r.getText().isEmpty())) {
return; return;
} }

View file

@ -66,7 +66,8 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
return new SimpleCompStructure<>(scroll); return new SimpleCompStructure<>(scroll);
} }
private void refresh(VBox listView, List<? extends T> shown, List<? extends T> all, Map<T, Region> cache, boolean asynchronous) { private void refresh(
VBox listView, List<? extends T> shown, List<? extends T> all, Map<T, Region> cache, boolean asynchronous) {
Runnable update = () -> { Runnable update = () -> {
// Clear cache of unused values // Clear cache of unused values
cache.keySet().removeIf(t -> !all.contains(t)); cache.keySet().removeIf(t -> !all.contains(t));

View file

@ -65,7 +65,8 @@ public class ListSelectorComp<T> extends SimpleComp {
if (showAllSelector) { if (showAllSelector) {
var allSelector = new CheckBox(null); var allSelector = new CheckBox(null);
allSelector.setSelected(values.stream().filter(t -> !disable.test(t)).count() == selected.size()); allSelector.setSelected(
values.stream().filter(t -> !disable.test(t)).count() == selected.size());
allSelector.selectedProperty().addListener((observable, oldValue, newValue) -> { allSelector.selectedProperty().addListener((observable, oldValue, newValue) -> {
cbs.forEach(checkBox -> { cbs.forEach(checkBox -> {
if (checkBox.isDisabled()) { if (checkBox.isDisabled()) {

View file

@ -16,10 +16,8 @@ import javafx.scene.layout.StackPane;
public class LoadingOverlayComp extends Comp<CompStructure<StackPane>> { public class LoadingOverlayComp extends Comp<CompStructure<StackPane>> {
public static LoadingOverlayComp noProgress(Comp<?> comp, ObservableValue<Boolean> loading) { private static final double FPS = 30.0;
return new LoadingOverlayComp(comp, loading, new SimpleDoubleProperty(-1)); private static final double cycleDurationSeconds = 4.0;
}
private final Comp<?> comp; private final Comp<?> comp;
private final ObservableValue<Boolean> showLoading; private final ObservableValue<Boolean> showLoading;
private final ObservableValue<Number> progress; private final ObservableValue<Number> progress;
@ -30,6 +28,10 @@ public class LoadingOverlayComp extends Comp<CompStructure<StackPane>> {
this.progress = PlatformThread.sync(progress); this.progress = PlatformThread.sync(progress);
} }
public static LoadingOverlayComp noProgress(Comp<?> comp, ObservableValue<Boolean> loading) {
return new LoadingOverlayComp(comp, loading, new SimpleDoubleProperty(-1));
}
@Override @Override
public CompStructure<StackPane> createBase() { public CompStructure<StackPane> createBase() {
var compStruc = comp.createStructure(); var compStruc = comp.createStructure();
@ -39,6 +41,11 @@ public class LoadingOverlayComp extends Comp<CompStructure<StackPane>> {
loading.progressProperty().bind(progress); loading.progressProperty().bind(progress);
loading.visibleProperty().bind(Bindings.not(AppPrefs.get().performanceMode())); loading.visibleProperty().bind(Bindings.not(AppPrefs.get().performanceMode()));
// var pane = new StackPane();
// Parent node = new Indicator((int) (FPS * cycleDurationSeconds), 2.0).getNode();
// pane.getChildren().add(node);
// pane.setAlignment(Pos.CENTER);
var loadingOverlay = new StackPane(loading); var loadingOverlay = new StackPane(loading);
loadingOverlay.getStyleClass().add("loading-comp"); loadingOverlay.getStyleClass().add("loading-comp");
loadingOverlay.setVisible(showLoading.getValue()); loadingOverlay.setVisible(showLoading.getValue());

View file

@ -46,11 +46,14 @@ public class MarkdownComp extends Comp<CompStructure<StackPane>> {
@SneakyThrows @SneakyThrows
private WebView createWebView() { private WebView createWebView() {
var wv = new WebView(); var wv = new WebView();
wv.getEngine().setUserDataDirectory(AppProperties.get().getDataDir().resolve("webview").toFile()); wv.getEngine()
.setUserDataDirectory(
AppProperties.get().getDataDir().resolve("webview").toFile());
wv.setPageFill(Color.TRANSPARENT); wv.setPageFill(Color.TRANSPARENT);
var theme = AppPrefs.get() != null && AppPrefs.get().theme.getValue().isDark() ? "web/github-markdown-dark.css" : "web/github-markdown-light.css"; var theme = AppPrefs.get() != null && AppPrefs.get().theme.getValue().isDark()
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, theme) ? "web/github-markdown-dark.css"
.orElseThrow(); : "web/github-markdown-light.css";
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, theme).orElseThrow();
wv.getEngine().setUserStyleSheetLocation(url.toString()); wv.getEngine().setUserStyleSheetLocation(url.toString());
SimpleChangeListener.apply(PlatformThread.sync(markdown), val -> { SimpleChangeListener.apply(PlatformThread.sync(markdown), val -> {

View file

@ -7,15 +7,12 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.Shortcuts;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar; import javafx.scene.control.ButtonBar;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
@ -23,23 +20,14 @@ import lombok.Value;
public class ModalOverlayComp extends SimpleComp { public class ModalOverlayComp extends SimpleComp {
private final Comp<?> background;
private final Property<OverlayContent> overlayContent;
public ModalOverlayComp(Comp<?> background, Property<OverlayContent> overlayContent) { public ModalOverlayComp(Comp<?> background, Property<OverlayContent> overlayContent) {
this.background = background; this.background = background;
this.overlayContent = overlayContent; this.overlayContent = overlayContent;
} }
@Value
public static class OverlayContent {
String titleKey;
Comp<?> content;
String finishKey;
Runnable onFinish;
}
private final Comp<?> background;
private final Property<OverlayContent> overlayContent;
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var bgRegion = background.createRegion(); var bgRegion = background.createRegion();
@ -62,7 +50,7 @@ public class ModalOverlayComp extends SimpleComp {
if (newValue.finishKey != null) { if (newValue.finishKey != null) {
var finishButton = new Button(AppI18n.get(newValue.finishKey)); var finishButton = new Button(AppI18n.get(newValue.finishKey));
Shortcuts.addShortcut(finishButton, new KeyCodeCombination(KeyCode.ENTER)); finishButton.setDefaultButton(true);
Styles.toggleStyleClass(finishButton, Styles.FLAT); Styles.toggleStyleClass(finishButton, Styles.FLAT);
finishButton.setOnAction(event -> { finishButton.setOnAction(event -> {
newValue.onFinish.run(); newValue.onFinish.run();
@ -96,4 +84,13 @@ public class ModalOverlayComp extends SimpleComp {
}); });
return pane; return pane;
} }
@Value
public static class OverlayContent {
String titleKey;
Comp<?> content;
String finishKey;
Runnable onFinish;
}
} }

View file

@ -20,6 +20,9 @@ import java.util.Map;
public class OsLogoComp extends SimpleComp { public class OsLogoComp extends SimpleComp {
private static final Map<String, String> ICONS = new HashMap<>();
private static final String LINUX_DEFAULT = "linux-24.png";
private static final String LINUX_DEFAULT_SVG = "linux.svg";
private final StoreEntryWrapper wrapper; private final StoreEntryWrapper wrapper;
private final ObservableValue<SystemStateComp.State> state; private final ObservableValue<SystemStateComp.State> state;
@ -47,7 +50,8 @@ public class OsLogoComp extends SimpleComp {
return getImage(ons.getOsName()); return getImage(ons.getOsName());
}, },
wrapper.getPersistentState(), state)); wrapper.getPersistentState(),
state));
var hide = BindingsHelper.map(img, s -> s != null); var hide = BindingsHelper.map(img, s -> s != null);
return new StackComp(List.of( return new StackComp(List.of(
new SystemStateComp(state).hide(hide), new SystemStateComp(state).hide(hide),
@ -55,9 +59,6 @@ public class OsLogoComp extends SimpleComp {
.createRegion(); .createRegion();
} }
private static final Map<String, String> ICONS = new HashMap<>();
private static final String LINUX_DEFAULT = "linux-24.png";
private String getImage(String name) { private String getImage(String name) {
if (name == null) { if (name == null) {
return null; return null;
@ -66,8 +67,10 @@ public class OsLogoComp extends SimpleComp {
if (ICONS.isEmpty()) { if (ICONS.isEmpty()) {
AppResources.with(AppResources.XPIPE_MODULE, "img/os", file -> { AppResources.with(AppResources.XPIPE_MODULE, "img/os", file -> {
try (var list = Files.list(file)) { try (var list = Files.list(file)) {
list.filter(path -> path.toString().endsWith(".svg") && !path.toString().endsWith(LINUX_DEFAULT)) list.filter(path -> path.toString().endsWith(".svg")
.map(path -> FileNames.getFileName(path.toString())).forEach(path -> { && !path.toString().endsWith(LINUX_DEFAULT_SVG))
.map(path -> FileNames.getFileName(path.toString()))
.forEach(path -> {
var base = FileNames.getBaseName(path).replace("-dark", "") + "-24.png"; var base = FileNames.getBaseName(path).replace("-dark", "") + "-24.png";
ICONS.put(FileNames.getBaseName(base).split("-")[0], "os/" + base); ICONS.put(FileNames.getBaseName(base).split("-")[0], "os/" + base);
}); });
@ -75,6 +78,10 @@ public class OsLogoComp extends SimpleComp {
}); });
} }
return ICONS.entrySet().stream().filter(e->name.toLowerCase().contains(e.getKey())).findAny().map(e->e.getValue()).orElse("os/" + LINUX_DEFAULT); return ICONS.entrySet().stream()
.filter(e -> name.toLowerCase().contains(e.getKey()))
.findAny()
.map(e -> e.getValue())
.orElse("os/" + LINUX_DEFAULT);
} }
} }

View file

@ -6,6 +6,7 @@ import io.xpipe.app.core.AppLogs;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.Augment;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
@ -14,13 +15,13 @@ import io.xpipe.app.issue.UserReportComp;
import io.xpipe.app.update.UpdateAvailableAlert; import io.xpipe.app.update.UpdateAvailableAlert;
import io.xpipe.app.update.XPipeDistributionType; import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.Hyperlinks; import io.xpipe.app.util.Hyperlinks;
import javafx.application.Platform;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.layout.Priority; import javafx.scene.layout.*;
import javafx.scene.layout.Region; import javafx.scene.paint.Color;
import javafx.scene.layout.VBox;
import java.util.List; import java.util.List;
@ -39,6 +40,32 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
var vbox = new VBox(); var vbox = new VBox();
vbox.setFillWidth(true); vbox.setFillWidth(true);
var selectedBorder = Bindings.createObjectBinding(
() -> {
var c = Platform.getPreferences().getAccentColor();
return new Border(new BorderStroke(
c, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(0, 3, 0, 0)));
},
Platform.getPreferences().accentColorProperty());
var hoverBorder = Bindings.createObjectBinding(
() -> {
var c = Platform.getPreferences().getAccentColor().darker();
return new Border(new BorderStroke(
c, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(0, 3, 0, 0)));
},
Platform.getPreferences().accentColorProperty());
var noneBorder = Bindings.createObjectBinding(
() -> {
return new Border(new BorderStroke(
Color.TRANSPARENT,
BorderStrokeStyle.SOLID,
CornerRadii.EMPTY,
new BorderWidths(0, 3, 0, 0)));
},
Platform.getPreferences().accentColorProperty());
var selected = PseudoClass.getPseudoClass("selected"); var selected = PseudoClass.getPseudoClass("selected");
entries.forEach(e -> { entries.forEach(e -> {
var b = new IconButtonComp(e.icon(), () -> value.setValue(e)).apply(new FancyTooltipAugment<>(e.name())); var b = new IconButtonComp(e.icon(), () -> value.setValue(e)).apply(new FancyTooltipAugment<>(e.name()));
@ -50,22 +77,59 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
struc.get().pseudoClassStateChanged(selected, n.equals(e)); struc.get().pseudoClassStateChanged(selected, n.equals(e));
}); });
}); });
struc.get()
.borderProperty()
.bind(Bindings.createObjectBinding(
() -> {
if (value.getValue().equals(e)) {
return selectedBorder.get();
}
if (struc.get().isHover()) {
return hoverBorder.get();
}
return noneBorder.get();
},
struc.get().hoverProperty(),
value,
hoverBorder,
selectedBorder,
noneBorder));
}); });
b.accessibleText(e.name()); b.accessibleText(e.name());
vbox.getChildren().add(b.createRegion()); vbox.getChildren().add(b.createRegion());
}); });
{ Augment<CompStructure<Button>> simpleBorders = struc -> {
var b = new IconButtonComp( struc.get()
"mdal-bug_report", .borderProperty()
.bind(Bindings.createObjectBinding(
() -> { () -> {
if (struc.get().isHover()) {
return hoverBorder.get();
}
return noneBorder.get();
},
struc.get().hoverProperty(),
value,
hoverBorder,
selectedBorder,
noneBorder));
};
{
var b = new IconButtonComp("mdal-bug_report", () -> {
var event = ErrorEvent.fromMessage("User Report"); var event = ErrorEvent.fromMessage("User Report");
if (AppLogs.get().isWriteToFile()) { if (AppLogs.get().isWriteToFile()) {
event.attachment(AppLogs.get().getSessionLogsDirectory()); event.attachment(AppLogs.get().getSessionLogsDirectory());
} }
UserReportComp.show(event.build()); UserReportComp.show(event.build());
}) })
.apply(new FancyTooltipAugment<>("reportIssue")).accessibleTextKey("reportIssue"); .apply(new FancyTooltipAugment<>("reportIssue"))
.apply(simpleBorders)
.accessibleTextKey("reportIssue");
b.apply(struc -> { b.apply(struc -> {
AppFont.setSize(struc.get(), 2); AppFont.setSize(struc.get(), 2);
}); });
@ -74,26 +138,20 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
{ {
var b = new IconButtonComp("mdi2g-github", () -> Hyperlinks.open(Hyperlinks.GITHUB)) var b = new IconButtonComp("mdi2g-github", () -> Hyperlinks.open(Hyperlinks.GITHUB))
.apply(new FancyTooltipAugment<>("visitGithubRepository")).accessibleTextKey("visitGithubRepository"); .apply(new FancyTooltipAugment<>("visitGithubRepository"))
.apply(simpleBorders)
.accessibleTextKey("visitGithubRepository");
b.apply(struc -> { b.apply(struc -> {
AppFont.setSize(struc.get(), 2); AppFont.setSize(struc.get(), 2);
}); });
vbox.getChildren().add(b.createRegion()); vbox.getChildren().add(b.createRegion());
} }
// {
// var b = new IconButtonComp("mdi2c-comment-processing-outline", () -> Hyperlinks.open(Hyperlinks.ROADMAP))
// .apply(new FancyTooltipAugment<>("roadmap"));
// b.apply(struc -> {
// AppFont.setSize(struc.get(), 2);
// });
// vbox.getChildren().add(b.createRegion());
// }
{ {
var b = new IconButtonComp("mdi2d-discord", () -> Hyperlinks.open(Hyperlinks.DISCORD)) var b = new IconButtonComp("mdi2d-discord", () -> Hyperlinks.open(Hyperlinks.DISCORD))
.apply(new FancyTooltipAugment<>("discord")).accessibleTextKey("discord"); .apply(new FancyTooltipAugment<>("discord"))
.apply(simpleBorders)
.accessibleTextKey("discord");
b.apply(struc -> { b.apply(struc -> {
AppFont.setSize(struc.get(), 2); AppFont.setSize(struc.get(), 2);
}); });
@ -102,7 +160,8 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
{ {
var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableAlert.showIfNeeded()) var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableAlert.showIfNeeded())
.apply(new FancyTooltipAugment<>("updateAvailableTooltip")).accessibleTextKey("updateAvailableTooltip"); .apply(new FancyTooltipAugment<>("updateAvailableTooltip"))
.accessibleTextKey("updateAvailableTooltip");
b.apply(struc -> { b.apply(struc -> {
AppFont.setSize(struc.get(), 2); AppFont.setSize(struc.get(), 2);
}); });
@ -123,7 +182,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
filler.setMaxHeight(3000); filler.setMaxHeight(3000);
vbox.getChildren().add(filler); vbox.getChildren().add(filler);
VBox.setVgrow(filler, Priority.ALWAYS); VBox.setVgrow(filler, Priority.ALWAYS);
filler.prefWidthProperty().bind(((Region) vbox.getChildren().get(0)).widthProperty()); filler.prefWidthProperty().bind(((Region) vbox.getChildren().getFirst()).widthProperty());
vbox.getStyleClass().add("sidebar-comp"); vbox.getStyleClass().add("sidebar-comp");
return new SimpleCompStructure<>(vbox); return new SimpleCompStructure<>(vbox);

View file

@ -15,6 +15,7 @@ public class SideSplitPaneComp extends Comp<SideSplitPaneComp.Structure> {
private final Comp<?> center; private final Comp<?> center;
private Double initialWidth; private Double initialWidth;
private Consumer<Double> onDividerChange; private Consumer<Double> onDividerChange;
public SideSplitPaneComp(Comp<?> left, Comp<?> center) { public SideSplitPaneComp(Comp<?> left, Comp<?> center) {
this.left = left; this.left = left;
this.center = center; this.center = center;
@ -36,13 +37,13 @@ public class SideSplitPaneComp extends Comp<SideSplitPaneComp.Structure> {
} }
if (!setInitial.get() && initialWidth != null) { if (!setInitial.get() && initialWidth != null) {
r.getDividers().get(0).setPosition(initialWidth / newValue.doubleValue()); r.getDividers().getFirst().setPosition(initialWidth / newValue.doubleValue());
setInitial.set(true); setInitial.set(true);
} }
}); });
SplitPane.setResizableWithParent(sidebar, false); SplitPane.setResizableWithParent(sidebar, false);
r.getDividers().get(0).positionProperty().addListener((observable, oldValue, newValue) -> { r.getDividers().getFirst().positionProperty().addListener((observable, oldValue, newValue) -> {
if (r.getWidth() <= 0) { if (r.getWidth() <= 0) {
return; return;
} }
@ -52,7 +53,7 @@ public class SideSplitPaneComp extends Comp<SideSplitPaneComp.Structure> {
} }
}); });
r.getStyleClass().add("side-split-pane-comp"); r.getStyleClass().add("side-split-pane-comp");
return new Structure(sidebar, c, r, r.getDividers().get(0)); return new Structure(sidebar, c, r, r.getDividers().getFirst());
} }
public SideSplitPaneComp withInitialWidth(double val) { public SideSplitPaneComp withInitialWidth(double val) {

View file

@ -39,7 +39,7 @@ public class StoreToggleComp extends SimpleComp {
}, },
section.getWrapper().getValidity(), section.getWrapper().getValidity(),
section.getShowDetails())); section.getShowDetails()));
var t = new NamedToggleComp(value, AppI18n.observable(nameKey)) var t = new ToggleSwitchComp(value, AppI18n.observable(nameKey))
.visible(visible) .visible(visible)
.disable(disable); .disable(disable);
value.addListener((observable, oldValue, newValue) -> { value.addListener((observable, oldValue, newValue) -> {

View file

@ -18,29 +18,12 @@ import org.kordamp.ikonli.javafx.StackedFontIcon;
@Getter @Getter
public class SystemStateComp extends SimpleComp { public class SystemStateComp extends SimpleComp {
private final ObservableValue<State> state;
public SystemStateComp(ObservableValue<State> state) { public SystemStateComp(ObservableValue<State> state) {
this.state = state; this.state = state;
} }
public enum State {
FAILURE,
SUCCESS,
OTHER;
public static ObservableValue<State> shellState(StoreEntryWrapper w) {
return BindingsHelper.map(w.getPersistentState(),o -> {
if (o instanceof ShellStoreState shellStoreState) {
return shellStoreState.getRunning() != null ? shellStoreState.getRunning() ? SUCCESS : FAILURE : OTHER;
}
return OTHER;
});
}
}
private final ObservableValue<State> state;
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var icon = PlatformThread.sync(Bindings.createStringBinding( var icon = PlatformThread.sync(Bindings.createStringBinding(
@ -58,15 +41,19 @@ public class SystemStateComp extends SimpleComp {
border.getStyleClass().add("outer-icon"); border.getStyleClass().add("outer-icon");
border.setOpacity(0.5); border.setOpacity(0.5);
var success = Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-success-emphasis; }"); var success = Styles.toDataURI(
var failure = Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-danger-emphasis; }"); ".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-success-emphasis; }");
var other = Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-accent-emphasis; }"); var failure =
Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-danger-emphasis; }");
var other =
Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-accent-emphasis; }");
var pane = new StackedFontIcon(); var pane = new StackedFontIcon();
pane.getChildren().addAll(fi, border); pane.getChildren().addAll(fi, border);
pane.setAlignment(Pos.CENTER); pane.setAlignment(Pos.CENTER);
var dataClass1 = """ var dataClass1 =
"""
.stacked-ikonli-font-icon > .outer-icon { .stacked-ikonli-font-icon > .outer-icon {
-fx-icon-size: 22px; -fx-icon-size: 22px;
} }
@ -78,9 +65,31 @@ public class SystemStateComp extends SimpleComp {
SimpleChangeListener.apply(PlatformThread.sync(state), val -> { SimpleChangeListener.apply(PlatformThread.sync(state), val -> {
pane.getStylesheets().removeAll(success, failure, other); pane.getStylesheets().removeAll(success, failure, other);
pane.getStylesheets().add(val == State.SUCCESS ? success : val == State.FAILURE ? failure: other); pane.getStylesheets().add(val == State.SUCCESS ? success : val == State.FAILURE ? failure : other);
}); });
return pane; return pane;
} }
public enum State {
FAILURE,
SUCCESS,
OTHER;
public static ObservableValue<State> shellState(StoreEntryWrapper w) {
return BindingsHelper.map(w.getPersistentState(), o -> {
if (o instanceof ShellStoreState s) {
if (s.getShellDialect() != null && !s.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) {
return SUCCESS;
}
return s.getRunning() != null
? s.getRunning() ? SUCCESS : FAILURE
: OTHER;
}
return OTHER;
});
}
}
} }

View file

@ -28,6 +28,18 @@ import java.util.function.Consumer;
@Getter @Getter
public class TileButtonComp extends Comp<TileButtonComp.Structure> { public class TileButtonComp extends Comp<TileButtonComp.Structure> {
private final ObservableValue<String> name;
private final ObservableValue<String> description;
private final ObservableValue<String> icon;
private final Consumer<ActionEvent> action;
public TileButtonComp(String nameKey, String descriptionKey, String icon, Consumer<ActionEvent> action) {
this.name = AppI18n.observable(nameKey);
this.description = AppI18n.observable(descriptionKey);
this.icon = new SimpleStringProperty(icon);
this.action = action;
}
@Override @Override
public Structure createBase() { public Structure createBase() {
var bt = new Button(); var bt = new Button();
@ -68,7 +80,13 @@ public class TileButtonComp extends Comp<TileButtonComp.Structure> {
fi.setIconSize((int) (size * 0.55)); fi.setIconSize((int) (size * 0.55));
}); });
bt.setGraphic(hbox); bt.setGraphic(hbox);
return Structure.builder().graphic(fi).button(bt).content(hbox).name(header).description(desc).build(); return Structure.builder()
.graphic(fi)
.button(bt)
.content(hbox)
.name(header)
.description(desc)
.build();
} }
@Value @Value
@ -85,16 +103,4 @@ public class TileButtonComp extends Comp<TileButtonComp.Structure> {
return button; return button;
} }
} }
private final ObservableValue<String> name;
private final ObservableValue<String> description;
private final ObservableValue<String> icon;
private final Consumer<ActionEvent> action;
public TileButtonComp(String nameKey, String descriptionKey, String icon, Consumer<ActionEvent> action) {
this.name = AppI18n.observable(nameKey);
this.description = AppI18n.observable(descriptionKey);
this.icon = new SimpleStringProperty(icon);
this.action = action;
}
} }

View file

@ -0,0 +1,37 @@
package io.xpipe.app.comp.base;
import atlantafx.base.controls.ToggleSwitch;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.beans.property.Property;
import javafx.beans.value.ObservableValue;
import javafx.scene.layout.Region;
public class ToggleSwitchComp extends SimpleComp {
private final Property<Boolean> selected;
private final ObservableValue<String> name;
public ToggleSwitchComp(Property<Boolean> selected, ObservableValue<String> name) {
this.selected = selected;
this.name = name;
}
@Override
protected Region createSimple() {
var s = new ToggleSwitch();
s.setSelected(selected.getValue());
s.selectedProperty().addListener((observable, oldValue, newValue) -> {
selected.setValue(newValue);
});
selected.addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
s.setSelected(newValue);
});
});
if (name != null) {
s.textProperty().bind(PlatformThread.sync(name));
}
return s;
}
}

View file

@ -4,7 +4,6 @@ import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.geometry.HPos; import javafx.geometry.HPos;
import javafx.geometry.Insets; import javafx.geometry.Insets;
@ -32,16 +31,26 @@ public class DenseStoreEntryComp extends StoreEntryComp {
: Comp.empty(); : Comp.empty();
information.setGraphic(state.createRegion()); information.setGraphic(state.createRegion());
var summary = wrapper.getSummary();
var info = wrapper.getEntry().getProvider().informationString(wrapper); var info = wrapper.getEntry().getProvider().informationString(wrapper);
SimpleChangeListener.apply(grid.hoverProperty(), val -> { var summary = wrapper.getSummary();
if (val && summary.getValue() != null && wrapper.getEntry().getProvider().alwaysShowSummary()) { if (wrapper.getEntry().getProvider() != null) {
information.textProperty().bind(PlatformThread.sync(summary)); information
.textProperty()
.bind(PlatformThread.sync(Bindings.createStringBinding(
() -> {
var val = summary.getValue();
if (val != null
&& grid.isHover()
&& wrapper.getEntry().getProvider().alwaysShowSummary()) {
return val;
} else { } else {
information.textProperty().bind(PlatformThread.sync(info)); return info.getValue();
}
},
grid.hoverProperty(),
info,
summary)));
} }
});
return information; return information;
} }
@ -51,9 +60,12 @@ public class DenseStoreEntryComp extends StoreEntryComp {
grid.setHgap(8); grid.setHgap(8);
var name = createName().createRegion(); var name = createName().createRegion();
name.maxWidthProperty().bind(Bindings.createDoubleBinding(() -> { name.maxWidthProperty()
.bind(Bindings.createDoubleBinding(
() -> {
return grid.getWidth() / 2.5; return grid.getWidth() / 2.5;
}, grid.widthProperty())); },
grid.widthProperty()));
if (showIcon) { if (showIcon) {
var storeIcon = createIcon(30, 24); var storeIcon = createIcon(30, 24);

View file

@ -12,7 +12,6 @@ public class StandardStoreEntryComp extends StoreEntryComp {
super(entry, content); super(entry, content);
} }
protected Region createContent() { protected Region createContent() {
var name = createName().createRegion(); var name = createName().createRegion();

View file

@ -1,9 +1,9 @@
package io.xpipe.app.comp.store; package io.xpipe.app.comp.store;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory; import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.storage.DataStoreEntry;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
@ -60,12 +60,12 @@ public class StoreCategoryWrapper {
return StoreViewState.get().getCategories().stream() return StoreViewState.get().getCategories().stream()
.filter(storeCategoryWrapper -> .filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(category.getParentCategory())) storeCategoryWrapper.getCategory().getUuid().equals(category.getParentCategory()))
.findAny().orElse(null); .findAny()
.orElse(null);
} }
public boolean contains(DataStoreEntry entry) { public boolean contains(StoreEntryWrapper entry) {
return entry.getCategoryUuid().equals(category.getUuid()) return entry.getEntry().getCategoryUuid().equals(category.getUuid()) || containedEntries.contains(entry);
|| children.stream().anyMatch(storeCategoryWrapper -> storeCategoryWrapper.contains(entry));
} }
public void select() { public void select() {
@ -87,6 +87,10 @@ public class StoreCategoryWrapper {
update(); update();
})); }));
AppPrefs.get().showChildCategoriesInParentCategory().addListener((observable, oldValue, newValue) -> {
update();
});
sortMode.addListener((observable, oldValue, newValue) -> { sortMode.addListener((observable, oldValue, newValue) -> {
category.setSortMode(newValue); category.setSortMode(newValue);
}); });
@ -117,15 +121,21 @@ public class StoreCategoryWrapper {
share.setValue(category.isShare()); share.setValue(category.isShare());
containedEntries.setAll(StoreViewState.get().getAllEntries().stream() containedEntries.setAll(StoreViewState.get().getAllEntries().stream()
.filter(entry -> contains(entry.getEntry())) .filter(entry -> {
return entry.getEntry().getCategoryUuid().equals(category.getUuid())
|| (AppPrefs.get()
.showChildCategoriesInParentCategory()
.get()
&& children.stream()
.anyMatch(storeCategoryWrapper -> storeCategoryWrapper.contains(entry)));
})
.toList()); .toList());
children.setAll(StoreViewState.get().getCategories().stream() children.setAll(StoreViewState.get().getCategories().stream()
.filter(storeCategoryWrapper -> getCategory() .filter(storeCategoryWrapper -> getCategory()
.getUuid() .getUuid()
.equals(storeCategoryWrapper.getCategory().getParentCategory())) .equals(storeCategoryWrapper.getCategory().getParentCategory()))
.toList()); .toList());
Optional.ofNullable(getParent()) Optional.ofNullable(getParent()).ifPresent(storeCategoryWrapper -> {
.ifPresent(storeCategoryWrapper -> {
storeCategoryWrapper.update(); storeCategoryWrapper.update();
}); });
} }

View file

@ -1,13 +1,15 @@
package io.xpipe.app.comp.store; package io.xpipe.app.comp.store;
import atlantafx.base.controls.Spacer;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.DialogComp;
import io.xpipe.app.comp.base.ErrorOverlayComp; import io.xpipe.app.comp.base.ErrorOverlayComp;
import io.xpipe.app.comp.base.MultiStepComp;
import io.xpipe.app.comp.base.PopupMenuButtonComp; import io.xpipe.app.comp.base.PopupMenuButtonComp;
import io.xpipe.app.core.*; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.ext.DataStoreProvider; import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.fxcomps.util.SimpleChangeListener;
@ -25,12 +27,12 @@ import javafx.beans.binding.Bindings;
import javafx.beans.property.*; import javafx.beans.property.*;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.control.Alert; import javafx.geometry.Orientation;
import javafx.scene.control.ScrollPane; import javafx.scene.control.*;
import javafx.scene.control.Separator;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
@ -41,9 +43,10 @@ import java.util.function.Consumer;
import java.util.function.Predicate; import java.util.function.Predicate;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class StoreCreationComp extends MultiStepComp.Step<CompStructure<?>> { public class StoreCreationComp extends DialogComp {
MultiStepComp parent; Stage window;
Consumer<DataStoreEntry> consumer;
Property<DataStoreProvider> provider; Property<DataStoreProvider> provider;
Property<DataStore> store; Property<DataStore> store;
Predicate<DataStoreProvider> filter; Predicate<DataStoreProvider> filter;
@ -53,19 +56,22 @@ public class StoreCreationComp extends MultiStepComp.Step<CompStructure<?>> {
BooleanProperty finished = new SimpleBooleanProperty(); BooleanProperty finished = new SimpleBooleanProperty();
ObservableValue<DataStoreEntry> entry; ObservableValue<DataStoreEntry> entry;
BooleanProperty changedSinceError = new SimpleBooleanProperty(); BooleanProperty changedSinceError = new SimpleBooleanProperty();
BooleanProperty skippable = new SimpleBooleanProperty();
StringProperty name; StringProperty name;
DataStoreEntry existingEntry; DataStoreEntry existingEntry;
boolean staticDisplay; boolean staticDisplay;
public StoreCreationComp( public StoreCreationComp(
MultiStepComp parent, Stage window,
Consumer<DataStoreEntry> consumer,
Property<DataStoreProvider> provider, Property<DataStoreProvider> provider,
Property<DataStore> store, Property<DataStore> store,
Predicate<DataStoreProvider> filter, Predicate<DataStoreProvider> filter,
String initialName, String initialName,
DataStoreEntry existingEntry, DataStoreEntry existingEntry,
boolean staticDisplay) { boolean staticDisplay) {
this.parent = parent; this.window = window;
this.consumer = consumer;
this.provider = provider; this.provider = provider;
this.store = store; this.store = store;
this.filter = filter; this.filter = filter;
@ -97,7 +103,8 @@ public class StoreCreationComp extends MultiStepComp.Step<CompStructure<?>> {
newValue.validate(); newValue.validate();
}); });
}); });
this.entry = Bindings.createObjectBinding(() -> { this.entry = Bindings.createObjectBinding(
() -> {
if (name.getValue() == null || store.getValue() == null) { if (name.getValue() == null || store.getValue() == null) {
return null; return null;
} }
@ -111,22 +118,27 @@ public class StoreCreationComp extends MultiStepComp.Step<CompStructure<?>> {
var targetCategory = p != null var targetCategory = p != null
? p.getCategoryUuid() ? p.getCategoryUuid()
: DataStorage.get() : DataStorage.get().getSelectedCategory().getUuid();
.getSelectedCategory() var rootCategory = DataStorage.get()
.getUuid(); .getRootCategory(DataStorage.get()
var rootCategory = DataStorage.get().getRootCategory(DataStorage.get().getStoreCategoryIfPresent(targetCategory).orElseThrow()); .getStoreCategoryIfPresent(targetCategory)
.orElseThrow());
// Don't put connections in the scripts category ever // Don't put connections in the scripts category ever
if ((provider.getValue().getCreationCategory() == null || !provider.getValue().getCreationCategory().equals(DataStoreProvider.CreationCategory.SCRIPT)) && if ((provider.getValue().getCreationCategory() == null
rootCategory.equals(DataStorage.get().getAllScriptsCategory())) { || !provider.getValue()
targetCategory = DataStorage.get().getDefaultCategory().getUuid(); .getCreationCategory()
.equals(DataStoreProvider.CreationCategory.SCRIPT))
&& rootCategory.equals(DataStorage.get().getAllScriptsCategory())) {
targetCategory = DataStorage.get()
.getDefaultConnectionsCategory()
.getUuid();
} }
return DataStoreEntry.createNew( return DataStoreEntry.createNew(
UUID.randomUUID(), UUID.randomUUID(), targetCategory, name.getValue(), store.getValue());
targetCategory, },
name.getValue(), name,
store.getValue()); store);
}, name, store);
} }
public static void showEdit(DataStoreEntry e) { public static void showEdit(DataStoreEntry e) {
@ -148,16 +160,23 @@ public class StoreCreationComp extends MultiStepComp.Step<CompStructure<?>> {
e); e);
} }
public static void showCreation(DataStoreProvider selected, Predicate<DataStoreProvider> filter) { public static void showCreation(DataStoreProvider selected, DataStoreProvider.CreationCategory category) {
showCreation(selected != null ? selected.defaultStore() : null, category);
}
public static void showCreation(DataStore base, DataStoreProvider.CreationCategory category) {
show( show(
null, null,
selected, base != null ? DataStoreProviders.byStore(base) : null,
selected != null ? selected.defaultStore() : null, base,
filter, dataStoreProvider -> category.equals(dataStoreProvider.getCreationCategory()),
e -> { e -> {
try { try {
DataStorage.get().addStoreEntryIfNotPresent(e); DataStorage.get().addStoreEntryIfNotPresent(e);
if (e.getProvider().shouldHaveChildren()) { if (e.getProvider().shouldHaveChildren()
&& AppPrefs.get()
.openConnectionSearchWindowOnConnectionCreation()
.get()) {
ScanAlert.showAsync(e); ScanAlert.showAsync(e);
} }
} catch (Exception ex) { } catch (Exception ex) {
@ -178,38 +197,117 @@ public class StoreCreationComp extends MultiStepComp.Step<CompStructure<?>> {
DataStoreEntry existingEntry) { DataStoreEntry existingEntry) {
var prop = new SimpleObjectProperty<>(provider); var prop = new SimpleObjectProperty<>(provider);
var store = new SimpleObjectProperty<>(s); var store = new SimpleObjectProperty<>(s);
var loading = new SimpleBooleanProperty(); DialogComp.showWindow(
var name = "addConnection"; "addConnection",
Platform.runLater(() -> { stage -> new StoreCreationComp(
var stage = AppWindowHelper.sideWindow( stage, con, prop, store, filter, initialName, existingEntry, staticDisplay));
AppI18n.get(name), }
window -> {
return new MultiStepComp() {
private final StoreCreationComp creator = new StoreCreationComp( private static boolean showInvalidConfirmAlert() {
this, prop, store, filter, initialName, existingEntry, staticDisplay); return AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("confirmInvalidStoreTitle"));
alert.setHeaderText(AppI18n.get("confirmInvalidStoreHeader"));
alert.getDialogPane()
.setContent(AppWindowHelper.alertContentText(AppI18n.get("confirmInvalidStoreContent")));
alert.setAlertType(Alert.AlertType.CONFIRMATION);
alert.getButtonTypes().clear();
alert.getButtonTypes().add(new ButtonType("Retry", ButtonBar.ButtonData.CANCEL_CLOSE));
alert.getButtonTypes().add(new ButtonType("Skip", ButtonBar.ButtonData.OK_DONE));
})
.map(b -> b.getButtonData().isDefaultButton())
.orElse(false);
}
@Override @Override
protected List<Entry> setup() { protected List<Comp<?>> customButtons() {
loading.bind(creator.busy); return List.of(new ButtonComp(AppI18n.observable("skip"), null, () -> {
return List.of(new Entry(AppI18n.observable("a"), creator)); if (showInvalidConfirmAlert()) {
commit();
} else {
finish();
}
})
.visible(skippable));
}
@Override
protected ObservableValue<Boolean> busy() {
return busy;
} }
@Override @Override
protected void finish() { protected void finish() {
window.close(); if (finished.get()) {
if (creator.entry.getValue() != null) { return;
con.accept(creator.entry.getValue());
} }
if (store.getValue() == null) {
return;
}
// We didn't change anything
if (existingEntry != null && existingEntry.getStore().equals(store.getValue())) {
commit();
return;
}
if (!validator.getValue().validate()) {
var msg = validator
.getValue()
.getValidationResult()
.getMessages()
.getFirst()
.getText();
TrackEvent.info(msg);
var newMessage = msg;
// Temporary fix for equal error message not showing up again
if (Objects.equals(newMessage, messageProp.getValue())) {
newMessage = newMessage + " ";
}
messageProp.setValue(newMessage);
changedSinceError.setValue(false);
return;
}
ThreadHelper.runAsync(() -> {
try (var b = new BooleanScope(busy).start()) {
DataStorage.get().addStoreEntryInProgress(entry.getValue());
entry.getValue().validateOrThrow();
commit();
} catch (Throwable ex) {
if (ex instanceof ValidationException) {
ErrorEvent.unreportable(ex);
skippable.set(false);
} else {
skippable.set(true);
}
var newMessage = ExceptionConverter.convertMessage(ex);
// Temporary fix for equal error message not showing up again
if (Objects.equals(newMessage, messageProp.getValue())) {
newMessage = newMessage + " ";
}
messageProp.setValue(newMessage);
changedSinceError.setValue(false);
ErrorEvent.fromThrowable(ex).omit().handle();
} finally {
DataStorage.get().removeStoreEntryInProgress(entry.getValue());
} }
};
},
false,
loading);
stage.show();
}); });
} }
@Override
public Comp<?> content() {
return Comp.of(this::createLayout);
}
@Override
protected Comp<?> scrollPane(Comp<?> content) {
var back = super.scrollPane(content);
return new ErrorOverlayComp(back, messageProp);
}
@Override @Override
public Comp<?> bottom() { public Comp<?> bottom() {
var disable = Bindings.createBooleanBinding( var disable = Bindings.createBooleanBinding(
@ -219,7 +317,9 @@ public class StoreCreationComp extends MultiStepComp.Step<CompStructure<?>> {
|| !store.getValue().isComplete() || !store.getValue().isComplete()
// When switching providers, both observables change one after another. // When switching providers, both observables change one after another.
// So temporarily there might be a store class mismatch // So temporarily there might be a store class mismatch
|| provider.getValue().getStoreClasses().stream().noneMatch(aClass -> aClass.isAssignableFrom(store.getValue().getClass())) || provider.getValue().getStoreClasses().stream()
.noneMatch(aClass -> aClass.isAssignableFrom(
store.getValue().getClass()))
|| provider.getValue().createInsightsMarkdown(store.getValue()) == null; || provider.getValue().createInsightsMarkdown(store.getValue()) == null;
}, },
provider, provider,
@ -238,17 +338,6 @@ public class StoreCreationComp extends MultiStepComp.Step<CompStructure<?>> {
.styleClass("button-comp"); .styleClass("button-comp");
} }
private static boolean showInvalidConfirmAlert() {
return AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("confirmInvalidStoreTitle"));
alert.setHeaderText(AppI18n.get("confirmInvalidStoreHeader"));
alert.setContentText(AppI18n.get("confirmInvalidStoreContent"));
alert.setAlertType(Alert.AlertType.CONFIRMATION);
})
.map(b -> b.getButtonData().isDefaultButton())
.orElse(false);
}
private Region createStoreProperties(Comp<?> comp, Validator propVal) { private Region createStoreProperties(Comp<?> comp, Validator propVal) {
return new OptionsBuilder() return new OptionsBuilder()
.addComp(comp, store) .addComp(comp, store)
@ -259,18 +348,26 @@ public class StoreCreationComp extends MultiStepComp.Step<CompStructure<?>> {
.build(); .build();
} }
@Override private void commit() {
public CompStructure<? extends Region> createBase() { if (finished.get()) {
var back = Comp.of(this::createLayout); return;
var message = new ErrorOverlayComp(back, messageProp); }
return message.createStructure(); finished.setValue(true);
if (entry.getValue() != null) {
consumer.accept(entry.getValue());
}
PlatformThread.runLaterIfNeeded(() -> {
window.close();
});
} }
private Region createLayout() { private Region createLayout() {
var layout = new BorderPane(); var layout = new BorderPane();
layout.getStyleClass().add("store-creator"); layout.getStyleClass().add("store-creator");
layout.setPadding(new Insets(20)); layout.setPadding(new Insets(20));
var providerChoice = new DataStoreProviderChoiceComp(filter, provider, staticDisplay); var providerChoice = new StoreProviderChoiceComp(filter, provider, staticDisplay);
if (staticDisplay) { if (staticDisplay) {
providerChoice.apply(struc -> struc.get().setDisable(true)); providerChoice.apply(struc -> struc.get().setDisable(true));
} }
@ -297,93 +394,9 @@ public class StoreCreationComp extends MultiStepComp.Step<CompStructure<?>> {
var sep = new Separator(); var sep = new Separator();
sep.getStyleClass().add("spacer"); sep.getStyleClass().add("spacer");
var top = new VBox(providerChoice.createRegion(), sep); var top = new VBox(providerChoice.createRegion(), new Spacer(7, Orientation.VERTICAL), sep);
top.getStyleClass().add("top"); top.getStyleClass().add("top");
layout.setTop(top); layout.setTop(top);
return layout; return layout;
} }
@Override
public boolean canContinue() {
if (provider.getValue() != null) {
var install = provider.getValue().getRequiredAdditionalInstallation();
if (install != null && !AppExtensionManager.getInstance().isInstalled(install)) {
ThreadHelper.runAsync(() -> {
try (var ignored = new BooleanScope(busy).start()) {
AppExtensionManager.getInstance().installIfNeeded(install);
/*
TODO: Use reload
*/
finished.setValue(true);
OperationMode.shutdown(false, false);
PlatformThread.runLaterIfNeeded(parent::next);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
});
return false;
}
}
if (finished.get()) {
return true;
}
if (store.getValue() == null) {
return false;
}
// We didn't change anything
if (existingEntry != null && existingEntry.getStore().equals(store.getValue())) {
return true;
}
if (messageProp.getValue() != null && !changedSinceError.get()) {
if (AppPrefs.get().developerMode().getValue() && showInvalidConfirmAlert()) {
return true;
}
}
if (!validator.getValue().validate()) {
var msg = validator
.getValue()
.getValidationResult()
.getMessages()
.getFirst()
.getText();
TrackEvent.info(msg);
var newMessage = msg;
// Temporary fix for equal error message not showing up again
if (Objects.equals(newMessage, messageProp.getValue())) {
newMessage = newMessage + " ";
}
messageProp.setValue(newMessage);
changedSinceError.setValue(false);
return false;
}
ThreadHelper.runAsync(() -> {
try (var b = new BooleanScope(busy).start()) {
DataStorage.get().addStoreEntryInProgress(entry.getValue());
entry.getValue().validateOrThrow();
finished.setValue(true);
PlatformThread.runLaterIfNeeded(parent::next);
} catch (Throwable ex) {
var newMessage = ExceptionConverter.convertMessage(ex);
// Temporary fix for equal error message not showing up again
if (Objects.equals(newMessage, messageProp.getValue())) {
newMessage = newMessage + " ";
}
messageProp.setValue(newMessage);
changedSinceError.setValue(false);
if (ex instanceof ValidationException) {
ErrorEvent.unreportable(ex);
}
ErrorEvent.fromThrowable(ex).omit().handle();
} finally {
DataStorage.get().removeStoreEntryInProgress(entry.getValue());
}
});
return false;
}
} }

View file

@ -24,37 +24,42 @@ public class StoreCreationMenu {
menu.getItems().add(automatically); menu.getItems().add(automatically);
menu.getItems().add(new SeparatorMenuItem()); menu.getItems().add(new SeparatorMenuItem());
menu.getItems().add(category("addHost", "mdi2h-home-plus", menu.getItems().add(category("addHost", "mdi2h-home-plus", DataStoreProvider.CreationCategory.HOST, "ssh"));
DataStoreProvider.CreationCategory.HOST, "ssh"));
menu.getItems().add(category("addShell", "mdi2t-text-box-multiple", menu.getItems()
DataStoreProvider.CreationCategory.SHELL, null)); .add(category("addShell", "mdi2t-text-box-multiple", DataStoreProvider.CreationCategory.SHELL, null));
menu.getItems().add(category("addScript", "mdi2s-script-text-outline", menu.getItems()
DataStoreProvider.CreationCategory.SCRIPT, "script")); .add(category(
"addScript", "mdi2s-script-text-outline", DataStoreProvider.CreationCategory.SCRIPT, "script"));
menu.getItems().add(category("addCommand", "mdi2c-code-greater-than", menu.getItems()
DataStoreProvider.CreationCategory.COMMAND, "cmd")); .add(category(
"addCommand", "mdi2c-code-greater-than", DataStoreProvider.CreationCategory.COMMAND, "cmd"));
menu.getItems().add(category("addTunnel", "mdi2v-vector-polyline-plus", menu.getItems()
DataStoreProvider.CreationCategory.TUNNEL, null)); .add(category(
"addTunnel", "mdi2v-vector-polyline-plus", DataStoreProvider.CreationCategory.TUNNEL, null));
menu.getItems().add(category("addCluster", "mdi2d-domain-plus", menu.getItems()
DataStoreProvider.CreationCategory.CLUSTER, null)); .add(category("addDatabase", "mdi2d-database-plus", DataStoreProvider.CreationCategory.DATABASE, null));
menu.getItems().add(category("addDatabase", "mdi2d-database-plus",
DataStoreProvider.CreationCategory.DATABASE, null));
} }
private static MenuItem category(String name, String graphic, DataStoreProvider.CreationCategory category, String defaultProvider) { private static MenuItem category(
var sub = DataStoreProviders.getAll().stream().filter(dataStoreProvider -> category.equals(dataStoreProvider.getCreationCategory())).toList(); String name, String graphic, DataStoreProvider.CreationCategory category, String defaultProvider) {
var sub = DataStoreProviders.getAll().stream()
.filter(dataStoreProvider -> category.equals(dataStoreProvider.getCreationCategory()))
.toList();
if (sub.size() < 2) { if (sub.size() < 2) {
var item = new MenuItem(); var item = new MenuItem();
item.setGraphic(new FontIcon(graphic)); item.setGraphic(new FontIcon(graphic));
item.textProperty().bind(AppI18n.observable(name)); item.textProperty().bind(AppI18n.observable(name));
item.setOnAction(event -> { item.setOnAction(event -> {
StoreCreationComp.showCreation(defaultProvider != null ? DataStoreProviders.byName(defaultProvider).orElseThrow() : null, StoreCreationComp.showCreation(
v -> category.equals(v.getCreationCategory())); defaultProvider != null
? DataStoreProviders.byName(defaultProvider).orElseThrow()
: null,
category);
event.consume(); event.consume();
}); });
return item; return item;
@ -68,16 +73,19 @@ public class StoreCreationMenu {
return; return;
} }
StoreCreationComp.showCreation(defaultProvider != null ? DataStoreProviders.byName(defaultProvider).orElseThrow() : null, StoreCreationComp.showCreation(
v -> category.equals(v.getCreationCategory())); defaultProvider != null
? DataStoreProviders.byName(defaultProvider).orElseThrow()
: null,
category);
event.consume(); event.consume();
}); });
sub.forEach(dataStoreProvider -> { sub.forEach(dataStoreProvider -> {
var item = new MenuItem(dataStoreProvider.getDisplayName()); var item = new MenuItem(dataStoreProvider.getDisplayName());
item.setGraphic(PrettyImageHelper.ofFixedSmallSquare(dataStoreProvider.getDisplayIconFileName(null)).createRegion()); item.setGraphic(PrettyImageHelper.ofFixedSmallSquare(dataStoreProvider.getDisplayIconFileName(null))
.createRegion());
item.setOnAction(event -> { item.setOnAction(event -> {
StoreCreationComp.showCreation(dataStoreProvider, StoreCreationComp.showCreation(dataStoreProvider, category);
v -> category.equals(v.getCreationCategory()));
event.consume(); event.consume();
}); });
menu.getItems().add(item); menu.getItems().add(item);

View file

@ -40,24 +40,6 @@ import java.util.Arrays;
public abstract class StoreEntryComp extends SimpleComp { public abstract class StoreEntryComp extends SimpleComp {
public static StoreEntryComp create(
StoreEntryWrapper entry, Comp<?> content, boolean preferLarge) {
if (!preferLarge) {
return new DenseStoreEntryComp(entry, true, content);
} else {
return new StandardStoreEntryComp(entry, content);
}
}
public static Comp<?> customSection(StoreSection e, boolean topLevel) {
var prov = e.getWrapper().getEntry().getProvider();
if (prov != null) {
return prov.customEntryComp(e, topLevel);
} else {
return new StandardStoreEntryComp(e.getWrapper(), null);
}
}
public static final PseudoClass FAILED = PseudoClass.getPseudoClass("failed"); public static final PseudoClass FAILED = PseudoClass.getPseudoClass("failed");
public static final PseudoClass INCOMPLETE = PseudoClass.getPseudoClass("incomplete"); public static final PseudoClass INCOMPLETE = PseudoClass.getPseudoClass("incomplete");
public static final ObservableDoubleValue INFO_NO_CONTENT_WIDTH = public static final ObservableDoubleValue INFO_NO_CONTENT_WIDTH =
@ -72,6 +54,29 @@ public abstract class StoreEntryComp extends SimpleComp {
this.content = content; this.content = content;
} }
public static StoreEntryComp create(StoreEntryWrapper entry, Comp<?> content, boolean preferLarge) {
var forceCondensed = AppPrefs.get() != null
&& AppPrefs.get().condenseConnectionDisplay().get();
if (!preferLarge || forceCondensed) {
return new DenseStoreEntryComp(entry, true, content);
} else {
return new StandardStoreEntryComp(entry, content);
}
}
public static Comp<?> customSection(StoreSection e, boolean topLevel) {
var prov = e.getWrapper().getEntry().getProvider();
if (prov != null) {
return prov.customEntryComp(e, topLevel);
} else {
var forceCondensed = AppPrefs.get() != null
&& AppPrefs.get().condenseConnectionDisplay().get();
return forceCondensed
? new DenseStoreEntryComp(e.getWrapper(), true, null)
: new StandardStoreEntryComp(e.getWrapper(), null);
}
}
@Override @Override
protected final Region createSimple() { protected final Region createSimple() {
var r = createContent(); var r = createContent();
@ -83,8 +88,7 @@ public abstract class StoreEntryComp extends SimpleComp {
button.setPadding(Insets.EMPTY); button.setPadding(Insets.EMPTY);
button.setMaxWidth(5000); button.setMaxWidth(5000);
button.setFocusTraversable(true); button.setFocusTraversable(true);
button.accessibleTextProperty() button.accessibleTextProperty().bind(wrapper.nameProperty());
.bind(wrapper.nameProperty());
button.setOnAction(event -> { button.setOnAction(event -> {
event.consume(); event.consume();
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
@ -105,8 +109,13 @@ public abstract class StoreEntryComp extends SimpleComp {
protected Label createInformation() { protected Label createInformation() {
var information = new Label(); var information = new Label();
information.setGraphicTextGap(7); information.setGraphicTextGap(7);
information.textProperty().bind(wrapper.getEntry().getProvider() != null ? information
PlatformThread.sync(wrapper.getEntry().getProvider().informationString(wrapper)) : new SimpleStringProperty()); .textProperty()
.bind(
wrapper.getEntry().getProvider() != null
? PlatformThread.sync(
wrapper.getEntry().getProvider().informationString(wrapper))
: new SimpleStringProperty());
information.getStyleClass().add("information"); information.getStyleClass().add("information");
AppFont.header(information); AppFont.header(information);
@ -191,15 +200,16 @@ public abstract class StoreEntryComp extends SimpleComp {
continue; continue;
} }
var button = new IconButtonComp( var button =
actionProvider.getIcon(wrapper.getEntry().ref()), () -> { new IconButtonComp(actionProvider.getIcon(wrapper.getEntry().ref()), () -> {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
var action = actionProvider.createAction( var action = actionProvider.createAction(
wrapper.getEntry().ref()); wrapper.getEntry().ref());
action.execute(); action.execute();
}); });
}); });
button.accessibleText(actionProvider.getName(wrapper.getEntry().ref()).getValue()); button.accessibleText(
actionProvider.getName(wrapper.getEntry().ref()).getValue());
button.apply(new FancyTooltipAugment<>( button.apply(new FancyTooltipAugment<>(
actionProvider.getName(wrapper.getEntry().ref()))); actionProvider.getName(wrapper.getEntry().ref())));
if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ONLY_SHOW_IF_ENABLED) { if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ONLY_SHOW_IF_ENABLED) {
@ -213,11 +223,11 @@ public abstract class StoreEntryComp extends SimpleComp {
var settingsButton = createSettingsButton(); var settingsButton = createSettingsButton();
list.add(settingsButton); list.add(settingsButton);
if (list.size() > 1) { if (list.size() > 1) {
list.get(0).styleClass(Styles.LEFT_PILL); list.getFirst().styleClass(Styles.LEFT_PILL);
for (int i = 1; i < list.size() - 1; i++) { for (int i = 1; i < list.size() - 1; i++) {
list.get(i).styleClass(Styles.CENTER_PILL); list.get(i).styleClass(Styles.CENTER_PILL);
} }
list.get(list.size() - 1).styleClass(Styles.RIGHT_PILL); list.getLast().styleClass(Styles.RIGHT_PILL);
} }
list.forEach(comp -> { list.forEach(comp -> {
comp.apply(struc -> struc.get().getStyleClass().remove(Styles.FLAT)); comp.apply(struc -> struc.get().getStyleClass().remove(Styles.FLAT));
@ -264,11 +274,13 @@ public abstract class StoreEntryComp extends SimpleComp {
? new Menu(null, new FontIcon(icon)) ? new Menu(null, new FontIcon(icon))
: new MenuItem(null, new FontIcon(icon)); : new MenuItem(null, new FontIcon(icon));
var proRequired = p.getKey().getProFeatureId() != null && var proRequired = p.getKey().getProFeatureId() != null
!LicenseProvider.get().getFeature(p.getKey().getProFeatureId()).isSupported(); && !LicenseProvider.get()
.getFeature(p.getKey().getProFeatureId())
.isSupported();
if (proRequired) { if (proRequired) {
item.setDisable(true); item.setDisable(true);
item.textProperty().bind(Bindings.createStringBinding(() -> name.getValue() + " (Pro)",name)); item.textProperty().bind(Bindings.createStringBinding(() -> name.getValue() + " (Pro)", name));
} else { } else {
item.textProperty().bind(name); item.textProperty().bind(name);
} }
@ -285,8 +297,7 @@ public abstract class StoreEntryComp extends SimpleComp {
contextMenu.hide(); contextMenu.hide();
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
var action = actionProvider.createAction( var action = actionProvider.createAction(wrapper.getEntry().ref());
wrapper.getEntry().ref());
action.execute(); action.execute();
}); });
}); });
@ -302,20 +313,27 @@ public abstract class StoreEntryComp extends SimpleComp {
run.textProperty().bind(AppI18n.observable("base.execute")); run.textProperty().bind(AppI18n.observable("base.execute"));
run.setOnAction(event -> { run.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
p.getKey().getDataStoreCallSite().createAction(wrapper.getEntry().ref()).execute(); p.getKey()
.getDataStoreCallSite()
.createAction(wrapper.getEntry().ref())
.execute();
}); });
}); });
menu.getItems().add(run); menu.getItems().add(run);
var sc = new MenuItem(null, new FontIcon("mdi2c-code-greater-than")); var sc = new MenuItem(null, new FontIcon("mdi2c-code-greater-than"));
var url = "xpipe://action/" + p.getKey().getId() + "/" var url = "xpipe://action/" + p.getKey().getId() + "/"
+ wrapper.getEntry().getUuid(); + wrapper.getEntry().getUuid();
sc.textProperty().bind(AppI18n.observable("base.createShortcut")); sc.textProperty().bind(AppI18n.observable("base.createShortcut"));
sc.setOnAction(event -> { sc.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
DesktopShortcuts.create(url, DesktopShortcuts.create(
wrapper.nameProperty().getValue() + " (" + p.getKey().getDataStoreCallSite().getName(wrapper.getEntry().ref()).getValue() + ")"); url,
wrapper.nameProperty().getValue() + " ("
+ p.getKey()
.getDataStoreCallSite()
.getName(wrapper.getEntry().ref())
.getValue() + ")");
}); });
}); });
menu.getItems().add(sc); menu.getItems().add(sc);
@ -345,9 +363,12 @@ public abstract class StoreEntryComp extends SimpleComp {
contextMenu.getItems().add(browse); contextMenu.getItems().add(browse);
} }
if (wrapper.getEntry().getProvider() != null && wrapper.getEntry().getProvider().canMoveCategories()) { if (wrapper.getEntry().getProvider() != null
&& wrapper.getEntry().getProvider().canMoveCategories()) {
var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline")); var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline"));
StoreViewState.get().getSortedCategories(wrapper.getCategory().getValue().getRoot()).forEach(storeCategoryWrapper -> { StoreViewState.get()
.getSortedCategories(wrapper.getCategory().getValue().getRoot())
.forEach(storeCategoryWrapper -> {
MenuItem m = new MenuItem(storeCategoryWrapper.getName()); MenuItem m = new MenuItem(storeCategoryWrapper.getName());
m.setOnAction(event -> { m.setOnAction(event -> {
wrapper.moveTo(storeCategoryWrapper.getCategory()); wrapper.moveTo(storeCategoryWrapper.getCategory());
@ -382,9 +403,16 @@ public abstract class StoreEntryComp extends SimpleComp {
} }
var del = new MenuItem(AppI18n.get("remove"), new FontIcon("mdal-delete_outline")); var del = new MenuItem(AppI18n.get("remove"), new FontIcon("mdal-delete_outline"));
del.disableProperty().bind(Bindings.createBooleanBinding(() -> { del.disableProperty()
return !wrapper.getDeletable().get() && !AppPrefs.get().developerDisableGuiRestrictions().get(); .bind(Bindings.createBooleanBinding(
}, wrapper.getDeletable(), AppPrefs.get().developerDisableGuiRestrictions())); () -> {
return !wrapper.getDeletable().get()
&& !AppPrefs.get()
.developerDisableGuiRestrictions()
.get();
},
wrapper.getDeletable(),
AppPrefs.get().developerDisableGuiRestrictions()));
del.setOnAction(event -> wrapper.delete()); del.setOnAction(event -> wrapper.delete());
contextMenu.getItems().add(del); contextMenu.getItems().add(del);

View file

@ -35,10 +35,18 @@ public class StoreEntryListComp extends SimpleComp {
var showIntro = Bindings.createBooleanBinding( var showIntro = Bindings.createBooleanBinding(
() -> { () -> {
var all = StoreViewState.get().getAllConnectionsCategory(); var all = StoreViewState.get().getAllConnectionsCategory();
var connections = StoreViewState.get().getAllEntries().stream().filter(wrapper -> all.contains(wrapper.getEntry())).toList(); var connections = StoreViewState.get().getAllEntries().stream()
return initialCount == connections.size() && StoreViewState.get().getActiveCategory().getValue().getRoot().equals(StoreViewState.get().getAllConnectionsCategory()); .filter(wrapper -> all.contains(wrapper))
.toList();
return initialCount == connections.size()
&& StoreViewState.get()
.getActiveCategory()
.getValue()
.getRoot()
.equals(StoreViewState.get().getAllConnectionsCategory());
}, },
StoreViewState.get().getAllEntries(), StoreViewState.get().getActiveCategory()); StoreViewState.get().getAllEntries(),
StoreViewState.get().getActiveCategory());
var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>(); var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>();
map.put( map.put(
createList(), createList(),

View file

@ -37,23 +37,43 @@ public class StoreEntryListStatusComp extends SimpleComp {
public StoreEntryListStatusComp() { public StoreEntryListStatusComp() {
this.sortMode = new SimpleObjectProperty<>(); this.sortMode = new SimpleObjectProperty<>();
SimpleChangeListener.apply(StoreViewState.get().getActiveCategory(), val -> { SimpleChangeListener.apply(StoreViewState.get().getActiveCategory(), val -> {
sortMode.unbind(); sortMode.setValue(val.getSortMode().getValue());
sortMode.bindBidirectional(val.getSortMode()); });
sortMode.addListener((observable, oldValue, newValue) -> {
var cat = StoreViewState.get().getActiveCategory().getValue();
if (cat == null) {
return;
}
cat.getSortMode().setValue(newValue);
}); });
} }
private Region createGroupListHeader() { private Region createGroupListHeader() {
var label = new Label(); var label = new Label();
label.textProperty().bind(Bindings.createStringBinding(() -> { label.textProperty()
return StoreViewState.get().getActiveCategory().getValue().getRoot().equals(StoreViewState.get().getAllConnectionsCategory()) ? "Connections" : "Scripts"; .bind(Bindings.createStringBinding(
}, StoreViewState.get().getActiveCategory())); () -> {
return StoreViewState.get()
.getActiveCategory()
.getValue()
.getRoot()
.equals(StoreViewState.get().getAllConnectionsCategory())
? "Connections"
: "Scripts";
},
StoreViewState.get().getActiveCategory()));
label.getStyleClass().add("name"); label.getStyleClass().add("name");
var all = BindingsHelper.filteredContentBinding( var all = BindingsHelper.filteredContentBinding(
StoreViewState.get().getAllEntries(), StoreViewState.get().getAllEntries(),
storeEntryWrapper -> { storeEntryWrapper -> {
var storeRoot = storeEntryWrapper.getCategory().getValue().getRoot(); var storeRoot = storeEntryWrapper.getCategory().getValue().getRoot();
return StoreViewState.get().getActiveCategory().getValue().getRoot().equals(storeRoot); return StoreViewState.get()
.getActiveCategory()
.getValue()
.getRoot()
.equals(storeRoot);
}, },
StoreViewState.get().getActiveCategory()); StoreViewState.get().getActiveCategory());
var shownList = BindingsHelper.filteredContentBinding( var shownList = BindingsHelper.filteredContentBinding(
@ -66,7 +86,13 @@ public class StoreEntryListStatusComp extends SimpleComp {
var count = new CountComp<>(shownList, all); var count = new CountComp<>(shownList, all);
var c = count.createRegion(); var c = count.createRegion();
var topBar = new HBox(label, c, Comp.hspacer().createRegion(), createDateSortButton().createRegion(), Comp.hspacer(2).createRegion(), createAlphabeticalSortButton().createRegion()); var topBar = new HBox(
label,
c,
Comp.hspacer().createRegion(),
createDateSortButton().createRegion(),
Comp.hspacer(2).createRegion(),
createAlphabeticalSortButton().createRegion());
AppFont.setSize(label, 3); AppFont.setSize(label, 3);
AppFont.setSize(c, 3); AppFont.setSize(c, 3);
topBar.setAlignment(Pos.CENTER); topBar.setAlignment(Pos.CENTER);
@ -104,7 +130,6 @@ public class StoreEntryListStatusComp extends SimpleComp {
f.setPadding(new Insets(-3, 0, -3, 0)); f.setPadding(new Insets(-3, 0, -3, 0));
} }
AppFont.medium(hbox); AppFont.medium(hbox);
return hbox; return hbox;
} }

View file

@ -121,7 +121,10 @@ public class StoreEntryWrapper {
deletable.setValue(entry.getConfiguration().isDeletable() deletable.setValue(entry.getConfiguration().isDeletable()
|| AppPrefs.get().developerDisableGuiRestrictions().getValue()); || AppPrefs.get().developerDisableGuiRestrictions().getValue());
category.setValue(StoreViewState.get().getCategoryWrapper(DataStorage.get().getStoreCategoryIfPresent(entry.getCategoryUuid()).orElseThrow())); category.setValue(StoreViewState.get()
.getCategoryWrapper(DataStorage.get()
.getStoreCategoryIfPresent(entry.getCategoryUuid())
.orElseThrow()));
if (!entry.getValidity().isUsable()) { if (!entry.getValidity().isUsable()) {
summary.setValue(null); summary.setValue(null);
@ -155,8 +158,7 @@ public class StoreEntryWrapper {
&& e.getDefaultDataStoreCallSite() && e.getDefaultDataStoreCallSite()
.getApplicableClass() .getApplicableClass()
.isAssignableFrom(entry.getStore().getClass()) .isAssignableFrom(entry.getStore().getClass())
&& e.getDefaultDataStoreCallSite() && e.getDefaultDataStoreCallSite().isApplicable(entry.ref()))
.isApplicable(entry.ref()))
.findFirst() .findFirst()
.map(ActionProvider::getDefaultDataStoreCallSite) .map(ActionProvider::getDefaultDataStoreCallSite)
.orElse(null); .orElse(null);

View file

@ -1,5 +1,6 @@
package io.xpipe.app.comp.store; package io.xpipe.app.comp.store;
import atlantafx.base.theme.Styles;
import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
@ -7,11 +8,9 @@ import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.ScanAlert; import io.xpipe.app.util.ScanAlert;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Orientation;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
@ -23,23 +22,23 @@ public class StoreIntroComp extends SimpleComp {
@Override @Override
public Region createSimple() { public Region createSimple() {
var title = new Label(AppI18n.get("storeIntroTitle")); var title = new Label(AppI18n.get("storeIntroTitle"));
title.getStyleClass().add(Styles.TEXT_BOLD);
AppFont.setSize(title, 7); AppFont.setSize(title, 7);
var introDesc = new Label(AppI18n.get("storeIntroDescription")); var introDesc = new Label(AppI18n.get("storeIntroDescription"));
introDesc.setWrapText(true);
var mfi = new FontIcon("mdi2p-playlist-plus"); introDesc.setMaxWidth(470);
var machine = new Label(AppI18n.get("storeMachineDescription"));
machine.heightProperty().addListener((c, o, n) -> {
mfi.iconSizeProperty().set(n.intValue());
});
var scanButton = new Button(AppI18n.get("detectConnections"), new FontIcon("mdi2m-magnify")); var scanButton = new Button(AppI18n.get("detectConnections"), new FontIcon("mdi2m-magnify"));
scanButton.setOnAction(event -> ScanAlert.showAsync(DataStorage.get().local())); scanButton.setOnAction(event -> ScanAlert.showAsync(DataStorage.get().local()));
scanButton.setDefaultButton(true);
var scanPane = new StackPane(scanButton); var scanPane = new StackPane(scanButton);
scanPane.setAlignment(Pos.CENTER); scanPane.setAlignment(Pos.CENTER);
var img = PrettyImageHelper.ofSvg(new SimpleStringProperty("Wave.svg"), 80, 150).createRegion(); var img = PrettyImageHelper.ofSvg(new SimpleStringProperty("Wave.svg"), 80, 150)
var text = new VBox(title, introDesc, new Separator(Orientation.HORIZONTAL), machine); .createRegion();
var text = new VBox(title, introDesc);
text.setSpacing(5);
text.setAlignment(Pos.CENTER_LEFT); text.setAlignment(Pos.CENTER_LEFT);
var hbox = new HBox(img, text); var hbox = new HBox(img, text);
hbox.setSpacing(35); hbox.setSpacing(35);

View file

@ -19,10 +19,12 @@ public class StoreLayoutComp extends SimpleComp {
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var struc = new SideSplitPaneComp(new StoreSidebarComp(), new StoreEntryListComp()).withInitialWidth( var struc = new SideSplitPaneComp(new StoreSidebarComp(), new StoreEntryListComp())
AppLayoutModel.get().getSavedState().getSidebarWidth()).withOnDividerChange(aDouble -> { .withInitialWidth(AppLayoutModel.get().getSavedState().getSidebarWidth())
.withOnDividerChange(aDouble -> {
AppLayoutModel.get().getSavedState().setSidebarWidth(aDouble); AppLayoutModel.get().getSavedState().setSidebarWidth(aDouble);
}).createStructure(); })
.createStructure();
struc.getLeft().setMinWidth(260); struc.getLeft().setMinWidth(260);
struc.getLeft().setMaxWidth(500); struc.getLeft().setMaxWidth(500);
struc.get().getStyleClass().add("store-layout"); struc.get().getStyleClass().add("store-layout");

View file

@ -0,0 +1,82 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.util.JfxHelper;
import javafx.beans.property.Property;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.Region;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.Supplier;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@AllArgsConstructor
public class StoreProviderChoiceComp extends Comp<CompStructure<ComboBox<DataStoreProvider>>> {
Predicate<DataStoreProvider> filter;
Property<DataStoreProvider> provider;
boolean staticDisplay;
private List<DataStoreProvider> getProviders() {
return DataStoreProviders.getAll().stream()
.filter(val -> filter == null || filter.test(val))
.toList();
}
private Region createGraphic(DataStoreProvider provider) {
if (provider == null) {
return null;
}
var graphic = provider.getDisplayIconFileName(null);
return JfxHelper.createNamedEntry(provider.getDisplayName(), provider.getDisplayDescription(), graphic);
}
@Override
public CompStructure<ComboBox<DataStoreProvider>> createBase() {
Supplier<ListCell<DataStoreProvider>> cellFactory = () -> new ListCell<>() {
@Override
protected void updateItem(DataStoreProvider item, boolean empty) {
super.updateItem(item, empty);
setGraphic(createGraphic(item));
setAccessibleText(item != null ? item.getDisplayName() : null);
setAccessibleHelp(item != null ? item.getDisplayDescription() : null);
}
};
var cb = new ComboBox<DataStoreProvider>();
cb.setCellFactory(param -> {
return cellFactory.get();
});
cb.setButtonCell(cellFactory.get());
var l = getProviders().stream()
.filter(p -> p.getCreationCategory() != null || staticDisplay)
.toList();
l.forEach(dataStoreProvider -> cb.getItems().add(dataStoreProvider));
if (provider.getValue() == null) {
provider.setValue(l.getFirst());
}
cb.setValue(provider.getValue());
provider.bind(cb.valueProperty());
cb.getStyleClass().add("choice-comp");
cb.setAccessibleText("Choose connection type");
cb.setOnKeyPressed(event -> {
if (!event.getCode().equals(KeyCode.ENTER)) {
return;
}
cb.show();
event.consume();
});
return new SimpleCompStructure<>(cb);
}
}

View file

@ -19,15 +19,6 @@ import java.util.function.Predicate;
@Value @Value
public class StoreSection { public class StoreSection {
public static Comp<?> customSection(StoreSection e, boolean topLevel) {
var prov = e.getWrapper().getEntry().getProvider();
if (prov != null) {
return prov.customSectionComp(e, topLevel);
} else {
return new StoreSectionComp(e, topLevel);
}
}
StoreEntryWrapper wrapper; StoreEntryWrapper wrapper;
ObservableList<StoreSection> allChildren; ObservableList<StoreSection> allChildren;
ObservableList<StoreSection> shownChildren; ObservableList<StoreSection> shownChildren;
@ -55,6 +46,15 @@ public class StoreSection {
} }
} }
public static Comp<?> customSection(StoreSection e, boolean topLevel) {
var prov = e.getWrapper().getEntry().getProvider();
if (prov != null) {
return prov.customSectionComp(e, topLevel);
} else {
return new StoreSectionComp(e, topLevel);
}
}
private static ObservableList<StoreSection> sorted( private static ObservableList<StoreSection> sorted(
ObservableList<StoreSection> list, ObservableValue<StoreCategoryWrapper> category) { ObservableList<StoreSection> list, ObservableValue<StoreCategoryWrapper> category) {
if (category == null) { if (category == null) {
@ -63,14 +63,16 @@ public class StoreSection {
var c = Comparator.<StoreSection>comparingInt( var c = Comparator.<StoreSection>comparingInt(
value -> value.getWrapper().getEntry().getValidity().isUsable() ? -1 : 1); value -> value.getWrapper().getEntry().getValidity().isUsable() ? -1 : 1);
var mappedSortMode = BindingsHelper.mappedBinding(category, storeCategoryWrapper -> storeCategoryWrapper != null ? storeCategoryWrapper.getSortMode() : null); var mappedSortMode = BindingsHelper.mappedBinding(
category,
storeCategoryWrapper -> storeCategoryWrapper != null ? storeCategoryWrapper.getSortMode() : null);
return BindingsHelper.orderedContentBinding( return BindingsHelper.orderedContentBinding(
list, list,
(o1, o2) -> { (o1, o2) -> {
var current = mappedSortMode.getValue(); var current = mappedSortMode.getValue();
if (current != null) { if (current != null) {
return c.thenComparing(current.comparator()) return c.thenComparing(current.comparator())
.compare(o1, o2); .compare(current.representative(o1), current.representative(o2));
} else { } else {
return c.compare(o1, o2); return c.compare(o1, o2);
} }
@ -97,7 +99,9 @@ public class StoreSection {
section -> { section -> {
var showFilter = filterString == null || section.shouldShow(filterString.get()); var showFilter = filterString == null || section.shouldShow(filterString.get());
var matchesSelector = section.anyMatches(entryFilter); var matchesSelector = section.anyMatches(entryFilter);
var sameCategory = category == null || category.getValue() == null || category.getValue().contains(section.getWrapper().getEntry()); var sameCategory = category == null
|| category.getValue() == null
|| category.getValue().contains(section.getWrapper());
return showFilter && matchesSelector && sameCategory; return showFilter && matchesSelector && sameCategory;
}, },
category, category,
@ -117,11 +121,11 @@ public class StoreSection {
} }
var allChildren = BindingsHelper.filteredContentBinding(all, other -> { var allChildren = BindingsHelper.filteredContentBinding(all, other -> {
// Legacy implementation that does not use caches. Use for testing // Legacy implementation that does not use children caches. Use for testing
// if (true) return DataStorage.get() // if (true) return DataStorage.get()
// .getDisplayParent(other.getEntry()) // .getDisplayParent(other.getEntry())
// .map(found -> found.equals(e.getEntry())) // .map(found -> found.equals(e.getEntry()))
// .orElse(false); // .orElse(false);
// This check is fast as the children are cached in the storage // This check is fast as the children are cached in the storage
return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry()); return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry());
@ -134,9 +138,13 @@ public class StoreSection {
section -> { section -> {
var showFilter = filterString == null || section.shouldShow(filterString.get()); var showFilter = filterString == null || section.shouldShow(filterString.get());
var matchesSelector = section.anyMatches(entryFilter); var matchesSelector = section.anyMatches(entryFilter);
var sameCategory = category == null || category.getValue() == null || category.getValue().contains(section.getWrapper().getEntry()); var sameCategory = category == null
// If this entry is already shown as root due to a different category than parent, don't show it again here || category.getValue() == null
var notRoot = !DataStorage.get().isRootEntry(section.getWrapper().getEntry()); || category.getValue().contains(section.getWrapper());
// If this entry is already shown as root due to a different category than parent, don't show it
// again here
var notRoot =
!DataStorage.get().isRootEntry(section.getWrapper().getEntry());
return showFilter && matchesSelector && sameCategory && notRoot; return showFilter && matchesSelector && sameCategory && notRoot;
}, },
category, category,

View file

@ -22,12 +22,11 @@ import java.util.List;
public class StoreSectionComp extends Comp<CompStructure<VBox>> { public class StoreSectionComp extends Comp<CompStructure<VBox>> {
public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded");
private static final PseudoClass ROOT = PseudoClass.getPseudoClass("root"); private static final PseudoClass ROOT = PseudoClass.getPseudoClass("root");
private static final PseudoClass SUB = PseudoClass.getPseudoClass("sub"); private static final PseudoClass SUB = PseudoClass.getPseudoClass("sub");
private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd-depth"); private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd-depth");
private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even-depth"); private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even-depth");
public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded");
private final StoreSection section; private final StoreSection section;
private final boolean topLevel; private final boolean topLevel;
@ -38,7 +37,7 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
@Override @Override
public CompStructure<VBox> createBase() { public CompStructure<VBox> createBase() {
var root = StandardStoreEntryComp.customSection(section, topLevel) var root = StoreEntryComp.customSection(section, topLevel)
.apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS)); .apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS));
var button = new IconButtonComp( var button = new IconButtonComp(
Bindings.createStringBinding( Bindings.createStringBinding(
@ -54,9 +53,11 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
.apply(struc -> struc.get().setMinWidth(30)) .apply(struc -> struc.get().setMinWidth(30))
.apply(struc -> struc.get().setPrefWidth(30)) .apply(struc -> struc.get().setPrefWidth(30))
.focusTraversable() .focusTraversable()
.accessibleText(Bindings.createStringBinding(() -> { .accessibleText(Bindings.createStringBinding(
() -> {
return "Expand " + section.getWrapper().getName().getValue(); return "Expand " + section.getWrapper().getName().getValue();
}, section.getWrapper().getName())) },
section.getWrapper().getName()))
.disable(BindingsHelper.persist( .disable(BindingsHelper.persist(
Bindings.size(section.getShownChildren()).isEqualTo(0))) Bindings.size(section.getShownChildren()).isEqualTo(0)))
.grow(false, true) .grow(false, true)

View file

@ -28,19 +28,18 @@ import java.util.function.BiConsumer;
@Builder @Builder
public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> { public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
public static Comp<?> createList(StoreSection top, BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment) { public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded");
return new StoreSectionMiniComp(top, augment);
}
private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd-depth"); private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd-depth");
private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even-depth"); private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even-depth");
public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded");
private final StoreSection section; private final StoreSection section;
@Builder.Default @Builder.Default
private final BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment = (section1, buttonComp) -> {}; private final BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment = (section1, buttonComp) -> {};
public static Comp<?> createList(StoreSection top, BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment) {
return new StoreSectionMiniComp(top, augment);
}
@Override @Override
public CompStructure<VBox> createBase() { public CompStructure<VBox> createBase() {
var list = new ArrayList<Comp<?>>(); var list = new ArrayList<Comp<?>>();
@ -48,14 +47,14 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
if (section.getWrapper() != null) { if (section.getWrapper() != null) {
var root = new ButtonComp(section.getWrapper().nameProperty(), () -> {}) var root = new ButtonComp(section.getWrapper().nameProperty(), () -> {})
.apply(struc -> { .apply(struc -> {
var provider = section.getWrapper() var provider = section.getWrapper().getEntry().getProvider();
.getEntry()
.getProvider();
struc.get() struc.get()
.setGraphic(PrettyImageHelper.ofFixedSmallSquare(provider != null ? provider .setGraphic(PrettyImageHelper.ofFixedSmallSquare(
.getDisplayIconFileName(section.getWrapper() provider != null
? provider.getDisplayIconFileName(section.getWrapper()
.getEntry() .getEntry()
.getStore()) : null) .getStore())
: null)
.createRegion()); .createRegion());
}) })
.apply(struc -> { .apply(struc -> {
@ -79,31 +78,40 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
.apply(struc -> struc.get().setMinWidth(20)) .apply(struc -> struc.get().setMinWidth(20))
.apply(struc -> struc.get().setPrefWidth(20)) .apply(struc -> struc.get().setPrefWidth(20))
.focusTraversable() .focusTraversable()
.accessibleText(Bindings.createStringBinding(() -> { .accessibleText(Bindings.createStringBinding(
return "Expand " + section.getWrapper().getName().getValue(); () -> {
}, section.getWrapper().getName())) return "Expand "
+ section.getWrapper().getName().getValue();
},
section.getWrapper().getName()))
.disable(BindingsHelper.persist( .disable(BindingsHelper.persist(
Bindings.size(section.getAllChildren()).isEqualTo(0))) Bindings.size(section.getAllChildren()).isEqualTo(0)))
.grow(false, true) .grow(false, true)
.styleClass("expand-button"); .styleClass("expand-button");
List<Comp<?>> topEntryList = List.of(button, root); List<Comp<?>> topEntryList = List.of(button, root);
list.add(new HorizontalComp(topEntryList) list.add(new HorizontalComp(topEntryList).apply(struc -> struc.get().setFillHeight(true)));
.apply(struc -> struc.get().setFillHeight(true)));
} else { } else {
expanded = new SimpleBooleanProperty(true); expanded = new SimpleBooleanProperty(true);
} }
// Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the // Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the
// section is actually expanded // section is actually expanded
var listSections = section.getWrapper() != null ? BindingsHelper.filteredContentBinding( var listSections = section.getWrapper() != null
? BindingsHelper.filteredContentBinding(
section.getShownChildren(), section.getShownChildren(),
storeSection -> section.getAllChildren().size() <= 20 storeSection -> section.getAllChildren().size() <= 20 || expanded.get(),
|| expanded.get(),
expanded, expanded,
section.getAllChildren()) : section.getShownChildren(); section.getAllChildren())
: section.getShownChildren();
var content = new ListBoxViewComp<>(listSections, section.getAllChildren(), (StoreSection e) -> { var content = new ListBoxViewComp<>(listSections, section.getAllChildren(), (StoreSection e) -> {
return StoreSectionMiniComp.builder().section(e).augment(this.augment).build(); return StoreSectionMiniComp.builder()
}).withLimit(100).minHeight(0).hgrow(); .section(e)
.augment(this.augment)
.build();
})
.withLimit(100)
.minHeight(0)
.hgrow();
list.add(new HorizontalComp(List.of(content)) list.add(new HorizontalComp(List.of(content))
.styleClass("content") .styleClass("content")
@ -130,8 +138,9 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
return; return;
} }
struc.get().getStyleClass().removeIf( struc.get().getStyleClass().removeIf(s -> Arrays.stream(DataStoreColor.values())
s -> Arrays.stream(DataStoreColor.values()).anyMatch(dataStoreColor -> dataStoreColor.getId().equals(s))); .anyMatch(dataStoreColor ->
dataStoreColor.getId().equals(s)));
struc.get().getStyleClass().remove("none"); struc.get().getStyleClass().remove("none");
struc.get().getStyleClass().add("color-box"); struc.get().getStyleClass().add("color-box");
if (val != null) { if (val != null) {

View file

@ -14,9 +14,18 @@ public class StoreSidebarComp extends SimpleComp {
protected Region createSimple() { protected Region createSimple() {
var sideBar = new VerticalComp(List.of( var sideBar = new VerticalComp(List.of(
new StoreEntryListStatusComp().styleClass("color-box").styleClass("gray"), new StoreEntryListStatusComp().styleClass("color-box").styleClass("gray"),
new StoreCategoryListComp(StoreViewState.get().getAllConnectionsCategory()).styleClass("color-box").styleClass("gray"), new StoreCategoryListComp(StoreViewState.get().getAllConnectionsCategory())
new StoreCategoryListComp(StoreViewState.get().getAllScriptsCategory()).styleClass("color-box").styleClass("gray"), .styleClass("color-box")
Comp.of(() -> new Region()).styleClass("bar").styleClass("color-box").styleClass("gray").styleClass("filler-bar").vgrow())); .styleClass("gray"),
new StoreCategoryListComp(StoreViewState.get().getAllScriptsCategory())
.styleClass("color-box")
.styleClass("gray"),
Comp.of(() -> new Region())
.styleClass("bar")
.styleClass("color-box")
.styleClass("gray")
.styleClass("filler-bar")
.vgrow()));
sideBar.apply(struc -> struc.get().setFillWidth(true)); sideBar.apply(struc -> struc.get().setFillWidth(true));
sideBar.styleClass("sidebar"); sideBar.styleClass("sidebar");
sideBar.prefWidth(240); sideBar.prefWidth(240);

View file

@ -12,6 +12,11 @@ import java.util.stream.Stream;
public interface StoreSortMode { public interface StoreSortMode {
StoreSortMode ALPHABETICAL_DESC = new StoreSortMode() { StoreSortMode ALPHABETICAL_DESC = new StoreSortMode() {
@Override
public StoreSection representative(StoreSection s) {
return s;
}
@Override @Override
public String getId() { public String getId() {
return "alphabetical-desc"; return "alphabetical-desc";
@ -23,8 +28,12 @@ public interface StoreSortMode {
e -> e.getWrapper().nameProperty().getValue().toLowerCase(Locale.ROOT)); e -> e.getWrapper().nameProperty().getValue().toLowerCase(Locale.ROOT));
} }
}; };
StoreSortMode ALPHABETICAL_ASC = new StoreSortMode() { StoreSortMode ALPHABETICAL_ASC = new StoreSortMode() {
@Override
public StoreSection representative(StoreSection s) {
return s;
}
@Override @Override
public String getId() { public String getId() {
return "alphabetical-asc"; return "alphabetical-asc";
@ -37,8 +46,21 @@ public interface StoreSortMode {
.reversed(); .reversed();
} }
}; };
StoreSortMode DATE_DESC = new StoreSortMode() { StoreSortMode DATE_DESC = new StoreSortMode() {
@Override
public StoreSection representative(StoreSection s) {
var c = comparator();
return Stream.of(
s.getShownChildren().stream()
.max((o1, o2) -> {
return c.compare(representative(o1), representative(o2));
})
.orElse(s),
s)
.max(c)
.orElseThrow();
}
@Override @Override
public String getId() { public String getId() {
return "date-desc"; return "date-desc";
@ -54,8 +76,21 @@ public interface StoreSortMode {
}); });
} }
}; };
StoreSortMode DATE_ASC = new StoreSortMode() { StoreSortMode DATE_ASC = new StoreSortMode() {
@Override
public StoreSection representative(StoreSection s) {
var c = comparator();
return Stream.of(
s.getShownChildren().stream()
.min((o1, o2) -> {
return c.compare(representative(o1), representative(o2));
})
.orElse(s),
s)
.min(c)
.orElseThrow();
}
@Override @Override
public String getId() { public String getId() {
return "date-asc"; return "date-asc";
@ -68,9 +103,11 @@ public interface StoreSortMode {
.map(entry -> entry.getLastAccess()) .map(entry -> entry.getLastAccess())
.max(Comparator.naturalOrder()) .max(Comparator.naturalOrder())
.orElseThrow(); .orElseThrow();
}).reversed(); })
.reversed();
} }
}; };
List<StoreSortMode> ALL = List.of(ALPHABETICAL_DESC, ALPHABETICAL_ASC, DATE_DESC, DATE_ASC);
static Stream<DataStoreEntry> flatten(StoreSection section) { static Stream<DataStoreEntry> flatten(StoreSection section) {
return Stream.concat( return Stream.concat(
@ -78,14 +115,14 @@ public interface StoreSortMode {
section.getAllChildren().stream().flatMap(section1 -> flatten(section1))); section.getAllChildren().stream().flatMap(section1 -> flatten(section1)));
} }
List<StoreSortMode> ALL = List.of(ALPHABETICAL_DESC, ALPHABETICAL_ASC, DATE_DESC, DATE_ASC);
static Optional<StoreSortMode> fromId(String id) { static Optional<StoreSortMode> fromId(String id) {
return ALL.stream() return ALL.stream()
.filter(storeSortMode -> storeSortMode.getId().equals(id)) .filter(storeSortMode -> storeSortMode.getId().equals(id))
.findFirst(); .findFirst();
} }
StoreSection representative(StoreSection s);
String getId(); String getId();
Comparator<StoreSection> comparator(); Comparator<StoreSection> comparator();

View file

@ -3,6 +3,7 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.core.AppCache; import io.xpipe.app.core.AppCache;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory; import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
@ -23,6 +24,26 @@ import java.util.stream.Collectors;
public class StoreViewState { public class StoreViewState {
private static StoreViewState INSTANCE; private static StoreViewState INSTANCE;
private final StringProperty filter = new SimpleStringProperty();
@Getter
private final ObservableList<StoreEntryWrapper> allEntries =
FXCollections.observableList(new CopyOnWriteArrayList<>());
@Getter
private final ObservableList<StoreCategoryWrapper> categories =
FXCollections.observableList(new CopyOnWriteArrayList<>());
@Getter
private final Property<StoreCategoryWrapper> activeCategory = new SimpleObjectProperty<>();
@Getter
private StoreSection currentTopLevelSection;
private StoreViewState() {
initContent();
addListeners();
}
public static void init() { public static void init() {
if (INSTANCE != null) { if (INSTANCE != null) {
@ -52,27 +73,6 @@ public class StoreViewState {
return INSTANCE; return INSTANCE;
} }
private final StringProperty filter = new SimpleStringProperty();
@Getter
private final ObservableList<StoreEntryWrapper> allEntries =
FXCollections.observableList(new CopyOnWriteArrayList<>());
@Getter
private final ObservableList<StoreCategoryWrapper> categories =
FXCollections.observableList(new CopyOnWriteArrayList<>());
@Getter
private StoreSection currentTopLevelSection;
@Getter
private final Property<StoreCategoryWrapper> activeCategory = new SimpleObjectProperty<>();
private StoreViewState() {
initContent();
addStorageListeners();
}
private void updateContent() { private void updateContent() {
categories.forEach(c -> c.update()); categories.forEach(c -> c.update());
allEntries.forEach(e -> e.update()); allEntries.forEach(e -> e.update());
@ -112,12 +112,27 @@ public class StoreViewState {
.orElseThrow())); .orElseThrow()));
} }
private void addStorageListeners() { private void addListeners() {
if (AppPrefs.get() != null) {
AppPrefs.get().condenseConnectionDisplay().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> {
synchronized (this) {
var l = new ArrayList<>(allEntries);
allEntries.clear();
allEntries.setAll(l);
}
});
});
}
// Watch out for synchronizing all calls to the entries and categories list! // Watch out for synchronizing all calls to the entries and categories list!
DataStorage.get().addListener(new StorageListener() { DataStorage.get().addListener(new StorageListener() {
@Override @Override
public void onStoreAdd(DataStoreEntry... entry) { public void onStoreAdd(DataStoreEntry... entry) {
var l = Arrays.stream(entry).map(StoreEntryWrapper::new).peek(storeEntryWrapper -> storeEntryWrapper.update()).toList(); var l = Arrays.stream(entry)
.map(StoreEntryWrapper::new)
.peek(storeEntryWrapper -> storeEntryWrapper.update())
.toList();
Platform.runLater(() -> { Platform.runLater(() -> {
// Don't update anything if we have already reset // Don't update anything if we have already reset
if (INSTANCE == null) { if (INSTANCE == null) {

View file

@ -1,6 +1,5 @@
package io.xpipe.app.core; package io.xpipe.app.core;
import io.xpipe.app.Main;
import io.xpipe.app.comp.AppLayoutComp; import io.xpipe.app.comp.AppLayoutComp;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
@ -12,8 +11,8 @@ import javafx.application.Application;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.stage.Stage; import javafx.stage.Stage;
import lombok.Getter; import lombok.Getter;
import lombok.SneakyThrows;
import javax.imageio.ImageIO;
import java.awt.*; import java.awt.*;
@Getter @Getter
@ -27,26 +26,13 @@ public class App extends Application {
} }
@Override @Override
@SneakyThrows
public void start(Stage primaryStage) { public void start(Stage primaryStage) {
TrackEvent.info("Application launched"); TrackEvent.info("Application launched");
APP = this; APP = this;
stage = primaryStage; stage = primaryStage;
stage.opacityProperty().bind(AppPrefs.get().windowOpacity()); stage.opacityProperty().bind(AppPrefs.get().windowOpacity());
// Set dock icon explicitly on mac
// This is necessary in case XPipe was started through a script as it will have no icon otherwise
if (OsType.getLocal().equals(OsType.MACOS) && AppProperties.get().isDeveloperMode() && AppLogs.get().isWriteToSysout()) {
try {
var iconUrl = Main.class.getResourceAsStream("resources/img/logo/logo_macos_128x128.png");
if (iconUrl != null) {
var awtIcon = ImageIO.read(iconUrl);
Taskbar.getTaskbar().setIconImage(awtIcon);
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omitted(true).build().handle();
}
}
if (OsType.getLocal().equals(OsType.MACOS)) { if (OsType.getLocal().equals(OsType.MACOS)) {
Desktop.getDesktop().setPreferencesHandler(e -> { Desktop.getDesktop().setPreferencesHandler(e -> {
AppLayoutModel.get().selectSettings(); AppLayoutModel.get().selectSettings();
@ -56,7 +42,8 @@ public class App extends Application {
if (OsType.getLocal().equals(OsType.LINUX)) { if (OsType.getLocal().equals(OsType.LINUX)) {
try { try {
Toolkit xToolkit = Toolkit.getDefaultToolkit(); Toolkit xToolkit = Toolkit.getDefaultToolkit();
java.lang.reflect.Field awtAppClassNameField = xToolkit.getClass().getDeclaredField("awtAppClassName"); java.lang.reflect.Field awtAppClassNameField =
xToolkit.getClass().getDeclaredField("awtAppClassName");
awtAppClassNameField.setAccessible(true); awtAppClassNameField.setAccessible(true);
awtAppClassNameField.set(xToolkit, "XPipe"); awtAppClassNameField.set(xToolkit, "XPipe");
} catch (Exception e) { } catch (Exception e) {
@ -103,10 +90,7 @@ public class App extends Application {
public void focus() { public void focus() {
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {
stage.setAlwaysOnTop(true);
stage.setAlwaysOnTop(false);
stage.requestFocus(); stage.requestFocus();
}); });
} }
} }

View file

@ -16,7 +16,8 @@ public class AppBundledFonts {
return; return;
} }
System.setProperty("prism.fontdir", XPipeInstallation.getBundledFontsPath().toString()); System.setProperty(
"prism.fontdir", XPipeInstallation.getBundledFontsPath().toString());
System.setProperty("prism.embeddedfonts", "true"); System.setProperty("prism.embeddedfonts", "true");
} }

View file

@ -11,7 +11,8 @@ public class AppDebugModeNotice {
} }
var out = AppLogs.get().getOriginalSysOut(); var out = AppLogs.get().getOriginalSysOut();
var msg = """ var msg =
"""
**************************************** ****************************************
* You are running XPipe in debug mode! * * You are running XPipe in debug mode! *

View file

@ -2,7 +2,6 @@ package io.xpipe.app.core;
import io.xpipe.app.exchange.MessageExchangeImpls; import io.xpipe.app.exchange.MessageExchangeImpls;
import io.xpipe.app.ext.ExtensionException; import io.xpipe.app.ext.ExtensionException;
import io.xpipe.app.ext.ModuleInstall;
import io.xpipe.app.ext.XPipeServiceProviders; import io.xpipe.app.ext.XPipeServiceProviders;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.issue.TrackEvent;
@ -29,6 +28,7 @@ public class AppExtensionManager {
private final List<ModuleLayer> leafModuleLayers = new ArrayList<>(); private final List<ModuleLayer> leafModuleLayers = new ArrayList<>();
private final List<Path> extensionBaseDirectories = new ArrayList<>(); private final List<Path> extensionBaseDirectories = new ArrayList<>();
private ModuleLayer baseLayer = ModuleLayer.boot(); private ModuleLayer baseLayer = ModuleLayer.boot();
@Getter @Getter
private ModuleLayer extendedLayer; private ModuleLayer extendedLayer;
@ -52,11 +52,20 @@ public class AppExtensionManager {
XPipeServiceProviders.load(INSTANCE.extendedLayer); XPipeServiceProviders.load(INSTANCE.extendedLayer);
MessageExchangeImpls.loadAll(); MessageExchangeImpls.loadAll();
} catch (Throwable t) { } catch (Throwable t) {
throw new ExtensionException("Service provider initialization failed. Is the installation data corrupt?", t); throw new ExtensionException(
"Service provider initialization failed. Is the installation data corrupt?", t);
} }
} }
} }
public static void reset() {
INSTANCE = null;
}
public static AppExtensionManager getInstance() {
return INSTANCE;
}
private void loadBaseExtension() { private void loadBaseExtension() {
var baseModule = findAndParseExtension("base", ModuleLayer.boot()); var baseModule = findAndParseExtension("base", ModuleLayer.boot());
if (baseModule.isEmpty()) { if (baseModule.isEmpty()) {
@ -95,14 +104,6 @@ public class AppExtensionManager {
extensionBaseDirectories.add(productionRoot); extensionBaseDirectories.add(productionRoot);
} }
public static void reset() {
INSTANCE = null;
}
public static AppExtensionManager getInstance() {
return INSTANCE;
}
public Set<Module> getContentModules() { public Set<Module> getContentModules() {
return Stream.concat( return Stream.concat(
Stream.of(ModuleLayer.boot().findModule("io.xpipe.app").orElseThrow()), Stream.of(ModuleLayer.boot().findModule("io.xpipe.app").orElseThrow()),
@ -110,87 +111,28 @@ public class AppExtensionManager {
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
public boolean isInstalled(ModuleInstall install) {
var target =
AppExtensionManager.getInstance().getGeneratedModulesDirectory(install.getModule(), install.getId());
return Files.exists(target) && Files.isRegularFile(target.resolve("finished"));
}
public void installIfNeeded(ModuleInstall install) throws Exception {
var target =
AppExtensionManager.getInstance().getGeneratedModulesDirectory(install.getModule(), install.getId());
if (Files.exists(target) && Files.isRegularFile(target.resolve("finished"))) {
return;
}
Files.createDirectories(target);
install.installInternal(target);
Files.createFile(target.resolve("finished"));
}
public Path getGeneratedModulesDirectory(String module, String ext) {
var base = AppProperties.get()
.getDataDir()
.resolve("generated_extensions")
.resolve(AppProperties.get().getVersion())
.resolve(module);
return ext != null ? base.resolve(ext) : base;
}
private void loadAllExtensions() { private void loadAllExtensions() {
for (Path extensionBaseDirectory : extensionBaseDirectories) { for (var ext : List.of("jdbc", "proc", "uacc")) {
loadExtensionRootDirectory(extensionBaseDirectory); var extension = findAndParseExtension(ext, baseLayer)
.orElseThrow(() -> ExtensionException.corrupt("Missing module " + ext));
loadedExtensions.add(extension);
leafModuleLayers.add(extension.getModule().getLayer());
} }
if (leafModuleLayers.size() > 0) {
var scl = ClassLoader.getSystemClassLoader(); var scl = ClassLoader.getSystemClassLoader();
var cfs = leafModuleLayers.stream().map(ModuleLayer::configuration).toList(); var cfs = leafModuleLayers.stream().map(ModuleLayer::configuration).toList();
var finder = ModuleFinder.ofSystem(); var finder = ModuleFinder.ofSystem();
var cf = Configuration.resolve(finder, cfs, finder, List.of()); var cf = Configuration.resolve(finder, cfs, finder, List.of());
extendedLayer = ModuleLayer.defineModulesWithOneLoader(cf, leafModuleLayers, scl) extendedLayer = ModuleLayer.defineModulesWithOneLoader(cf, leafModuleLayers, scl)
.layer(); .layer();
} else {
extendedLayer = baseLayer;
}
}
private void loadExtensionRootDirectory(Path dir) {
if (!Files.exists(dir)) {
return;
}
// Order results as on unix systems the file list order is not deterministic
try (var s = Files.list(dir).sorted(Comparator.comparing(path -> path.toString()))) {
s.forEach(sub -> {
if (Files.isDirectory(sub)) {
// TODO: Better detection for x modules
if (sub.toString().endsWith("x")) {
return;
}
var extension = parseExtensionDirectory(sub, baseLayer);
if (extension.isEmpty()) {
return;
}
loadedExtensions.add(extension.get());
var xModule = findAndParseExtension(
extension.get().getId() + "x",
extension.get().getModule().getLayer());
if (xModule.isPresent()) {
loadedExtensions.add(xModule.get());
leafModuleLayers.add(xModule.get().getModule().getLayer());
} else {
leafModuleLayers.add(extension.get().getModule().getLayer());
}
}
});
} catch (IOException ex) {
ErrorEvent.fromThrowable(ex).handle();
}
} }
private Optional<Extension> findAndParseExtension(String name, ModuleLayer parent) { private Optional<Extension> findAndParseExtension(String name, ModuleLayer parent) {
var inModulePath = ModuleLayer.boot().findModule("io.xpipe.ext." + name);
if (inModulePath.isPresent()) {
return Optional.of(new Extension(null, inModulePath.get().getName(), name, inModulePath.get(), 0));
}
for (Path extensionBaseDirectory : extensionBaseDirectories) { for (Path extensionBaseDirectory : extensionBaseDirectories) {
var found = parseExtensionDirectory(extensionBaseDirectory.resolve(name), parent); var found = parseExtensionDirectory(extensionBaseDirectory.resolve(name), parent);
if (found.isPresent()) { if (found.isPresent()) {
@ -206,7 +148,7 @@ public class AppExtensionManager {
return Optional.empty(); return Optional.empty();
} }
if (loadedExtensions.stream().anyMatch(extension -> extension.dir.equals(dir)) if (loadedExtensions.stream().anyMatch(extension -> dir.equals(extension.dir))
|| loadedExtensions.stream() || loadedExtensions.stream()
.anyMatch(extension -> .anyMatch(extension ->
extension.id.equals(dir.getFileName().toString()))) { extension.id.equals(dir.getFileName().toString()))) {

View file

@ -107,6 +107,7 @@ public class AppFileWatcher {
private class WatchedDirectory { private class WatchedDirectory {
private final BiConsumer<Path, WatchEvent.Kind<Path>> listener; private final BiConsumer<Path, WatchEvent.Kind<Path>> listener;
@Getter @Getter
private final Path baseDir; private final Path baseDir;
@ -114,9 +115,7 @@ public class AppFileWatcher {
this.baseDir = dir; this.baseDir = dir;
this.listener = listener; this.listener = listener;
createRecursiveWatchers(dir); createRecursiveWatchers(dir);
TrackEvent.withTrace("watcher", "Added watched directory") TrackEvent.withTrace("Added watched directory").tag("location", dir).handle();
.tag("location", dir)
.handle();
} }
private void createRecursiveWatchers(Path dir) { private void createRecursiveWatchers(Path dir) {
@ -177,13 +176,12 @@ public class AppFileWatcher {
} }
// Handle event // Handle event
TrackEvent.withTrace("watcher", "Watch event") TrackEvent.withTrace("Watch event")
.tag("baseDir", baseDir) .tag("baseDir", baseDir)
.tag("file", baseDir.relativize(file)) .tag("file", baseDir.relativize(file))
.tag("kind", event.kind().name()) .tag("kind", event.kind().name())
.handle(); .handle();
listener.accept(file, ev.kind()); listener.accept(file, ev.kind());
} }
} }
} }

View file

@ -54,7 +54,8 @@ public class AppFont {
try (var in = Files.newInputStream(file)) { try (var in = Files.newInputStream(file)) {
Font.loadFont(in, OsType.getLocal() == OsType.LINUX ? 11 : 12); Font.loadFont(in, OsType.getLocal() == OsType.LINUX ? 11 : 12);
} catch (Throwable t) { } catch (Throwable t) {
// Font loading can fail in rare cases. This is however not important, so we can just ignore it // Font loading can fail in rare cases. This is however not important, so we can just ignore
// it
} }
return FileVisitResult.CONTINUE; return FileVisitResult.CONTINUE;
} }

View file

@ -1,6 +1,5 @@
package io.xpipe.app.core; package io.xpipe.app.core;
import com.jfoenix.controls.JFXCheckBox;
import io.xpipe.app.comp.base.MarkdownComp; import io.xpipe.app.comp.base.MarkdownComp;
import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
@ -52,7 +51,7 @@ public class AppGreetings {
public static void showIfNeeded() { public static void showIfNeeded() {
boolean set = AppCache.get("legalAccepted", Boolean.class, () -> false); boolean set = AppCache.get("legalAccepted", Boolean.class, () -> false);
if (set || !AppState.get().isInitialLaunch()) { if (set || AppProperties.get().isDevelopmentEnvironment()) {
return; return;
} }
var read = new SimpleBooleanProperty(); var read = new SimpleBooleanProperty();
@ -72,20 +71,21 @@ public class AppGreetings {
}); });
var acceptanceBox = Comp.of(() -> { var acceptanceBox = Comp.of(() -> {
var cb = new JFXCheckBox(); var cb = new CheckBox();
cb.selectedProperty().bindBidirectional(accepted); cb.selectedProperty().bindBidirectional(accepted);
var label = new Label(AppI18n.get("legalAccept")); var label = new Label(AppI18n.get("legalAccept"));
label.setGraphic(cb); label.setGraphic(cb);
AppFont.medium(label); AppFont.medium(label);
label.setPadding(new Insets(40, 0, 10, 0)); label.setPadding(new Insets(20, 0, 10, 0));
label.setOnMouseClicked(event -> accepted.set(!accepted.get())); label.setOnMouseClicked(event -> accepted.set(!accepted.get()));
label.setGraphicTextGap(10);
return label; return label;
}) })
.createRegion(); .createRegion();
var layout = new BorderPane(); var layout = new BorderPane();
layout.getStyleClass().add("window-content"); layout.setPadding(new Insets(20));
layout.setCenter(accordion); layout.setCenter(accordion);
layout.setBottom(acceptanceBox); layout.setBottom(acceptanceBox);
layout.setPrefWidth(700); layout.setPrefWidth(700);

View file

@ -37,10 +37,10 @@ import java.util.regex.Pattern;
public class AppI18n { public class AppI18n {
private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\w+?\\$"); private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\w+?\\$");
private static final AppI18n INSTANCE = new AppI18n();
private Map<String, String> translations; private Map<String, String> translations;
private Map<String, String> markdownDocumentations; private Map<String, String> markdownDocumentations;
private PrettyTime prettyTime; private PrettyTime prettyTime;
private static final AppI18n INSTANCE = new AppI18n();
public static void init() { public static void init() {
var i = INSTANCE; var i = INSTANCE;
@ -51,7 +51,7 @@ public class AppI18n {
i.load(); i.load();
if (AppPrefs.get() != null) { if (AppPrefs.get() != null) {
AppPrefs.get().language.addListener((c, o, n) -> { AppPrefs.get().language().addListener((c, o, n) -> {
i.clear(); i.clear();
i.load(); i.load();
}); });
@ -98,8 +98,11 @@ public class AppI18n {
return "null"; return "null";
} }
return getInstance().prettyTime.formatDuration( return getInstance()
getInstance().prettyTime.approximateDuration(Instant.now().plus(duration.getValue()))); .prettyTime
.formatDuration(getInstance()
.prettyTime
.approximateDuration(Instant.now().plus(duration.getValue())));
}, },
duration); duration);
} }
@ -136,20 +139,6 @@ public class AppI18n {
return s; return s;
} }
private void clear() {
translations.clear();
prettyTime = null;
}
@SuppressWarnings("removal")
public static class CallingClass extends SecurityManager {
public static final CallingClass INSTANCE = new CallingClass();
public Class<?>[] getCallingClasses() {
return getClassContext();
}
}
@SneakyThrows @SneakyThrows
private static String getCallerModuleName() { private static String getCallerModuleName() {
var callers = CallingClass.INSTANCE.getCallingClasses(); var callers = CallingClass.INSTANCE.getCallingClasses();
@ -161,6 +150,7 @@ public class AppI18n {
|| caller.equals(FancyTooltipAugment.class) || caller.equals(FancyTooltipAugment.class)
|| caller.equals(PrefsChoiceValue.class) || caller.equals(PrefsChoiceValue.class)
|| caller.equals(Translatable.class) || caller.equals(Translatable.class)
|| caller.equals(AppWindowHelper.class)
|| caller.equals(OptionsBuilder.class)) { || caller.equals(OptionsBuilder.class)) {
continue; continue;
} }
@ -170,6 +160,11 @@ public class AppI18n {
return ""; return "";
} }
private void clear() {
translations.clear();
prettyTime = null;
}
public String getKey(String s) { public String getKey(String s) {
var key = s; var key = s;
if (!s.contains(".")) { if (!s.contains(".")) {
@ -210,7 +205,7 @@ public class AppI18n {
private boolean matchesLocale(Path f) { private boolean matchesLocale(Path f) {
var l = AppPrefs.get() != null var l = AppPrefs.get() != null
? AppPrefs.get().language.getValue().getLocale() ? AppPrefs.get().language().getValue().getLocale()
: SupportedLocale.ENGLISH.getLocale(); : SupportedLocale.ENGLISH.getLocale();
var name = FilenameUtils.getBaseName(f.getFileName().toString()); var name = FilenameUtils.getBaseName(f.getFileName().toString());
var ending = "_" + l.toLanguageTag(); var ending = "_" + l.toLanguageTag();
@ -219,7 +214,8 @@ public class AppI18n {
public String getMarkdownDocumentation(String name) { public String getMarkdownDocumentation(String name) {
if (!markdownDocumentations.containsKey(name)) { if (!markdownDocumentations.containsKey(name)) {
TrackEvent.withWarn("Markdown documentation for key " + name + " not found").handle(); TrackEvent.withWarn("Markdown documentation for key " + name + " not found")
.handle();
} }
return markdownDocumentations.getOrDefault(name, ""); return markdownDocumentations.getOrDefault(name, "");
@ -311,7 +307,16 @@ public class AppI18n {
this.prettyTime = new PrettyTime( this.prettyTime = new PrettyTime(
AppPrefs.get() != null AppPrefs.get() != null
? AppPrefs.get().language.getValue().getLocale() ? AppPrefs.get().language().getValue().getLocale()
: SupportedLocale.ENGLISH.getLocale()); : SupportedLocale.ENGLISH.getLocale());
} }
@SuppressWarnings("removal")
public static class CallingClass extends SecurityManager {
public static final CallingClass INSTANCE = new CallingClass();
public Class<?>[] getCallingClasses() {
return getClassContext();
}
}
} }

View file

@ -5,7 +5,8 @@ import io.xpipe.app.browser.BrowserModel;
import io.xpipe.app.comp.DeveloperTabComp; import io.xpipe.app.comp.DeveloperTabComp;
import io.xpipe.app.comp.store.StoreLayoutComp; import io.xpipe.app.comp.store.StoreLayoutComp;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.prefs.PrefsComp; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.prefs.AppPrefsComp;
import io.xpipe.app.util.LicenseProvider; import io.xpipe.app.util.LicenseProvider;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
@ -18,20 +19,26 @@ import lombok.extern.jackson.Jacksonized;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@Getter
public class AppLayoutModel { public class AppLayoutModel {
@Data
@Builder
@Jacksonized
public static class SavedState {
double sidebarWidth;
double browserConnectionsWidth;
}
private static AppLayoutModel INSTANCE; private static AppLayoutModel INSTANCE;
@Getter
private final SavedState savedState;
@Getter
private final List<Entry> entries;
private final Property<Entry> selected;
private final ObservableValue<Entry> selectedWrapper;
public AppLayoutModel(SavedState savedState) {
this.savedState = savedState;
this.entries = createEntryList();
this.selected = new SimpleObjectProperty<>(entries.get(1));
this.selectedWrapper = PlatformThread.sync(selected);
}
public static AppLayoutModel get() { public static AppLayoutModel get() {
return INSTANCE; return INSTANCE;
} }
@ -46,19 +53,16 @@ public class AppLayoutModel {
INSTANCE = null; INSTANCE = null;
} }
@Getter public Property<Entry> getSelectedInternal() {
private final SavedState savedState; return selected;
private final List<Entry> entries; }
private final Property<Entry> selected;
public AppLayoutModel(SavedState savedState) { public ObservableValue<Entry> getSelected() {
this.savedState = savedState; return selectedWrapper;
this.entries = createEntryList();
this.selected = new SimpleObjectProperty<>(entries.get(1));
} }
public void selectBrowser() { public void selectBrowser() {
selected.setValue(entries.get(0)); selected.setValue(entries.getFirst());
} }
public void selectSettings() { public void selectSettings() {
@ -75,17 +79,14 @@ public class AppLayoutModel {
private List<Entry> createEntryList() { private List<Entry> createEntryList() {
var l = new ArrayList<>(List.of( var l = new ArrayList<>(List.of(
new Entry( new Entry(AppI18n.observable("browser"), "mdi2f-file-cabinet", new BrowserComp(BrowserModel.DEFAULT)),
AppI18n.observable("browser"), "mdi2f-file-cabinet", new BrowserComp(BrowserModel.DEFAULT)),
new Entry(AppI18n.observable("connections"), "mdi2c-connection", new StoreLayoutComp()), new Entry(AppI18n.observable("connections"), "mdi2c-connection", new StoreLayoutComp()),
new Entry( new Entry(AppI18n.observable("settings"), "mdsmz-miscellaneous_services", new AppPrefsComp())));
AppI18n.observable("settings"), "mdsmz-miscellaneous_services", new PrefsComp(this))));
// new SideMenuBarComp.Entry(AppI18n.observable("help"), "mdi2b-book-open-variant", new // new SideMenuBarComp.Entry(AppI18n.observable("help"), "mdi2b-book-open-variant", new
// StorageLayoutComp()), // StorageLayoutComp()),
// new SideMenuBarComp.Entry(AppI18n.observable("account"), "mdi2a-account", new StorageLayoutComp()) // new SideMenuBarComp.Entry(AppI18n.observable("account"), "mdi2a-account", new StorageLayoutComp())
if (AppProperties.get().isDeveloperMode() && !AppProperties.get().isImage()) { if (AppProperties.get().isDeveloperMode() && !AppProperties.get().isImage()) {
l.add(new Entry( l.add(new Entry(AppI18n.observable("developer"), "mdi2b-book-open-variant", new DeveloperTabComp()));
AppI18n.observable("developer"), "mdi2b-book-open-variant", new DeveloperTabComp()));
} }
l.add(new Entry( l.add(new Entry(
@ -96,5 +97,14 @@ public class AppLayoutModel {
return l; return l;
} }
@Data
@Builder
@Jacksonized
public static class SavedState {
double sidebarWidth;
double browserConnectionsWidth;
}
public record Entry(ObservableValue<String> name, String icon, Comp<?> comp) {} public record Entry(ObservableValue<String> name, String icon, Comp<?> comp) {}
} }

View file

@ -24,7 +24,6 @@ import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoField; import java.time.temporal.ChronoField;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@ -32,23 +31,26 @@ import java.util.concurrent.ConcurrentHashMap;
public class AppLogs { public class AppLogs {
public static final List<String> DEFAULT_LEVELS = List.of("error", "warn", "info", "debug", "trace"); public static final List<String> LOG_LEVELS = List.of("error", "warn", "info", "debug", "trace");
private static final String WRITE_SYSOUT_PROP = "io.xpipe.app.writeSysOut"; private static final String WRITE_SYSOUT_PROP = "io.xpipe.app.writeSysOut";
private static final String WRITE_LOGS_PROP = "io.xpipe.app.writeLogs"; private static final String WRITE_LOGS_PROP = "io.xpipe.app.writeLogs";
private static final String DEBUG_PLATFORM_PROP = "io.xpipe.app.debugPlatform"; private static final String DEBUG_PLATFORM_PROP = "io.xpipe.app.debugPlatform";
private static final String LOG_LEVEL_PROP = "io.xpipe.app.logLevel"; private static final String LOG_LEVEL_PROP = "io.xpipe.app.logLevel";
private static final String DEFAULT_LOG_LEVEL = "info"; private static final String DEFAULT_LOG_LEVEL = "info";
private static final DateTimeFormatter FORMATTER = private static final DateTimeFormatter NAME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss").withZone(ZoneId.systemDefault()); DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss").withZone(ZoneId.systemDefault());
private static final DateTimeFormatter MESSAGE_FORMATTER = private static final DateTimeFormatter MESSAGE_FORMATTER =
DateTimeFormatter.ofPattern("HH:mm:ss:SSS").withZone(ZoneId.systemDefault()); DateTimeFormatter.ofPattern("HH:mm:ss:SSS").withZone(ZoneId.systemDefault());
private static AppLogs INSTANCE; private static AppLogs INSTANCE;
@Getter @Getter
private final PrintStream originalSysOut; private final PrintStream originalSysOut;
@Getter @Getter
private final PrintStream originalSysErr; private final PrintStream originalSysErr;
private final Path logDir; private final Path logDir;
@Getter @Getter
@ -60,16 +62,15 @@ public class AppLogs {
@Getter @Getter
private final String logLevel; private final String logLevel;
private final PrintStream outStream; private final PrintStream outFileStream;
private final Map<String, PrintStream> categoryWriters;
public AppLogs(Path logDir, boolean writeToSysout, boolean writeToFile, String logLevel) { public AppLogs(
Path logDir, boolean writeToSysout, boolean writeToFile, String logLevel, PrintStream outFileStream) {
this.logDir = logDir; this.logDir = logDir;
this.writeToSysout = writeToSysout; this.writeToSysout = writeToSysout;
this.writeToFile = writeToFile; this.writeToFile = writeToFile;
this.logLevel = logLevel; this.logLevel = logLevel;
this.outStream = System.out; this.outFileStream = outFileStream;
this.categoryWriters = new HashMap<>();
this.originalSysOut = System.out; this.originalSysOut = System.out;
this.originalSysErr = System.err; this.originalSysErr = System.err;
@ -96,20 +97,34 @@ public class AppLogs {
} }
public static void init() { public static void init() {
if (INSTANCE != null) {
return;
}
var logDir = AppProperties.get().getDataDir().resolve("logs"); var logDir = AppProperties.get().getDataDir().resolve("logs");
// Regularly clean logs dir
if (XPipeSession.get().isNewBuildSession() && Files.exists(logDir)) { if (XPipeSession.get().isNewBuildSession() && Files.exists(logDir)) {
try { try {
FileUtils.cleanDirectory(logDir.toFile()); List<Path> all;
try (var s = Files.list(logDir)) {
all = s.toList();
}
for (Path path : all) {
// Don't delete installer logs
if (path.getFileName().toString().contains("installer")) {
continue;
}
FileUtils.forceDelete(path.toFile());
}
} catch (Exception ex) { } catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle(); ErrorEvent.fromThrowable(ex).handle();
} }
} }
var shouldLogToFile = shouldWriteLogs();
var now = Instant.now(); var now = Instant.now();
var name = FORMATTER.format(now); var name = NAME_FORMATTER.format(now);
Path usedLogsDir = logDir.resolve(name); Path usedLogsDir = logDir.resolve(name);
// When two instances are being launched within the same second, add milliseconds // When two instances are being launched within the same second, add milliseconds
@ -117,23 +132,34 @@ public class AppLogs {
usedLogsDir = logDir.resolve(name + "_" + now.get(ChronoField.MILLI_OF_SECOND)); usedLogsDir = logDir.resolve(name + "_" + now.get(ChronoField.MILLI_OF_SECOND));
} }
PrintStream outFileStream = null;
var shouldLogToFile = shouldWriteLogs();
if (shouldLogToFile) { if (shouldLogToFile) {
try { try {
Files.createDirectories(usedLogsDir); Files.createDirectories(usedLogsDir);
var file = usedLogsDir.resolve("xpipe.log");
var fos = new FileOutputStream(file.toFile(), true);
var buf = new BufferedOutputStream(fos);
outFileStream = new PrintStream(buf, false);
} catch (Exception ex) { } catch (Exception ex) {
ErrorEvent.fromThrowable(ex).build().handle(); ErrorEvent.fromThrowable(ex).build().handle();
shouldLogToFile = false;
} }
} }
var shouldLogToSysout = shouldWriteSysout(); var shouldLogToSysout = shouldWriteSysout();
if (shouldLogToFile && outFileStream == null) {
TrackEvent.info("Log file initialization failed. Writing to standard out");
shouldLogToSysout = true;
shouldLogToFile = false;
}
if (shouldLogToFile && !shouldLogToSysout) { if (shouldLogToFile && !shouldLogToSysout) {
TrackEvent.info("Writing log output to " + usedLogsDir + " from now on"); TrackEvent.info("Writing log output to " + usedLogsDir + " from now on");
} }
var level = determineLogLevel(); var level = determineLogLevel();
INSTANCE = new AppLogs(usedLogsDir, shouldLogToSysout, shouldLogToFile, level); INSTANCE = new AppLogs(usedLogsDir, shouldLogToSysout, shouldLogToFile, level, outFileStream);
} }
public static void teardown() { public static void teardown() {
@ -149,45 +175,19 @@ public class AppLogs {
return INSTANCE; return INSTANCE;
} }
private static String determineLogLevel() {
if (System.getProperty(LOG_LEVEL_PROP) != null) {
String p = System.getProperty(LOG_LEVEL_PROP);
return LOG_LEVELS.contains(p) ? p : "trace";
}
return DEFAULT_LOG_LEVEL;
}
private void close() { private void close() {
outStream.close(); if (outFileStream != null) {
categoryWriters.forEach((k, s) -> { outFileStream.close();
s.close();
});
} }
private String getCategory(TrackEvent event) {
if (event.getCategory() != null) {
return event.getCategory();
}
return "misc";
}
private synchronized PrintStream getLogStream(TrackEvent e) {
return categoryWriters.computeIfAbsent(getCategory(e), (cat) -> {
var file = logDir.resolve(cat + ".log");
FileOutputStream fos;
try {
fos = new FileOutputStream(file.toFile(), true);
} catch (IOException ex) {
return outStream;
}
return new PrintStream(fos, false);
});
}
public synchronized PrintStream getCatchAllLogStream() {
return categoryWriters.computeIfAbsent("xpipe", (cat) -> {
var file = logDir.resolve(cat + ".log");
FileOutputStream fos;
try {
fos = new FileOutputStream(file.toFile(), true);
} catch (IOException ex) {
return outStream;
}
return new PrintStream(fos, false);
});
} }
private boolean shouldDebugPlatform() { private boolean shouldDebugPlatform() {
@ -210,12 +210,7 @@ public class AppLogs {
return; return;
} }
TrackEvent.builder() TrackEvent.builder().type("info").message(line).build().handle();
.type("info")
.category("sysout")
.message(line)
.build()
.handle();
baos.reset(); baos.reset();
} else { } else {
baos.write(b); baos.write(b);
@ -245,15 +240,6 @@ public class AppLogs {
})); }));
} }
private static String determineLogLevel() {
if (System.getProperty(LOG_LEVEL_PROP) != null) {
String p = System.getProperty(LOG_LEVEL_PROP);
return DEFAULT_LEVELS.contains(p) ? p : "info";
}
return DEFAULT_LOG_LEVEL;
}
public void logException(String description, Throwable e) { public void logException(String description, Throwable e) {
var deob = Deobfuscator.deobfuscateToString(e); var deob = Deobfuscator.deobfuscateToString(e);
var event = TrackEvent.builder() var event = TrackEvent.builder()
@ -264,9 +250,9 @@ public class AppLogs {
} }
public synchronized void logEvent(TrackEvent event) { public synchronized void logEvent(TrackEvent event) {
var li = DEFAULT_LEVELS.indexOf(determineLogLevel()); var li = LOG_LEVELS.indexOf(determineLogLevel());
int i = li == -1 ? 5 : li; int i = li == -1 ? 5 : li;
int current = DEFAULT_LEVELS.indexOf(event.getType()); int current = LOG_LEVELS.indexOf(event.getType());
if (current <= i) { if (current <= i) {
if (writeToSysout) { if (writeToSysout) {
logSysOut(event); logSysOut(event);
@ -281,12 +267,9 @@ public class AppLogs {
var time = MESSAGE_FORMATTER.format(event.getInstant()); var time = MESSAGE_FORMATTER.format(event.getInstant());
var string = var string =
new StringBuilder(time).append(" - ").append(event.getType()).append(": "); new StringBuilder(time).append(" - ").append(event.getType()).append(": ");
if (event.getCategory() != null) {
string.append("[").append(event.getCategory()).append("] ");
}
string.append(event); string.append(event);
var toLog = string.toString(); var toLog = string.toString();
outStream.println(toLog); this.originalSysOut.println(toLog);
} }
private void logToFile(TrackEvent event) { private void logToFile(TrackEvent event) {
@ -295,8 +278,7 @@ public class AppLogs {
new StringBuilder(time).append(" - ").append(event.getType()).append(": "); new StringBuilder(time).append(" - ").append(event.getType()).append(": ");
string.append(event); string.append(event);
var toLog = string.toString(); var toLog = string.toString();
getLogStream(event).println(toLog); outFileStream.println(toLog);
getCatchAllLogStream().println(toLog);
} }
private void setLogLevels() { private void setLogLevels() {
@ -312,10 +294,6 @@ public class AppLogs {
} }
} }
public Path getLogsDirectory() {
return logDir.getParent();
}
public Path getSessionLogsDirectory() { public Path getSessionLogsDirectory() {
return logDir; return logDir;
} }
@ -339,7 +317,7 @@ public class AppLogs {
normalizedName = name; normalizedName = name;
} }
return loggers.computeIfAbsent(normalizedName, Slf4jLogger::new); return loggers.computeIfAbsent(normalizedName, s -> new Slf4jLogger());
} }
}; };
@ -369,12 +347,6 @@ public class AppLogs {
public static final class Slf4jLogger extends AbstractLogger { public static final class Slf4jLogger extends AbstractLogger {
private final String name;
public Slf4jLogger(String name) {
this.name = name;
}
@Override @Override
protected String getFullyQualifiedCallerName() { protected String getFullyQualifiedCallerName() {
return "logger"; return "logger";
@ -390,7 +362,6 @@ public class AppLogs {
} }
} }
TrackEvent.builder() TrackEvent.builder()
.category(name)
.type(level.toString().toLowerCase()) .type(level.toString().toLowerCase())
.message(msg) .message(msg)
.build() .build()
@ -399,62 +370,62 @@ public class AppLogs {
@Override @Override
public boolean isTraceEnabled() { public boolean isTraceEnabled() {
return DEFAULT_LEVELS.indexOf("trace") return LOG_LEVELS.indexOf("trace")
<= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel()); <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());
} }
@Override @Override
public boolean isTraceEnabled(Marker marker) { public boolean isTraceEnabled(Marker marker) {
return DEFAULT_LEVELS.indexOf("trace") return LOG_LEVELS.indexOf("trace")
<= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel()); <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());
} }
@Override @Override
public boolean isDebugEnabled() { public boolean isDebugEnabled() {
return DEFAULT_LEVELS.indexOf("debug") return LOG_LEVELS.indexOf("debug")
<= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel()); <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());
} }
@Override @Override
public boolean isDebugEnabled(Marker marker) { public boolean isDebugEnabled(Marker marker) {
return DEFAULT_LEVELS.indexOf("debug") return LOG_LEVELS.indexOf("debug")
<= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel()); <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());
} }
@Override @Override
public boolean isInfoEnabled() { public boolean isInfoEnabled() {
return DEFAULT_LEVELS.indexOf("info") return LOG_LEVELS.indexOf("info")
<= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel()); <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());
} }
@Override @Override
public boolean isInfoEnabled(Marker marker) { public boolean isInfoEnabled(Marker marker) {
return DEFAULT_LEVELS.indexOf("info") return LOG_LEVELS.indexOf("info")
<= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel()); <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());
} }
@Override @Override
public boolean isWarnEnabled() { public boolean isWarnEnabled() {
return DEFAULT_LEVELS.indexOf("warn") return LOG_LEVELS.indexOf("warn")
<= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel()); <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());
} }
@Override @Override
public boolean isWarnEnabled(Marker marker) { public boolean isWarnEnabled(Marker marker) {
return DEFAULT_LEVELS.indexOf("warn") return LOG_LEVELS.indexOf("warn")
<= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel()); <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());
} }
@Override @Override
public boolean isErrorEnabled() { public boolean isErrorEnabled() {
return DEFAULT_LEVELS.indexOf("error") return LOG_LEVELS.indexOf("error")
<= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel()); <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());
} }
@Override @Override
public boolean isErrorEnabled(Marker marker) { public boolean isErrorEnabled(Marker marker) {
return DEFAULT_LEVELS.indexOf("error") return LOG_LEVELS.indexOf("error")
<= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel()); <= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());
} }
} }
} }

View file

@ -83,7 +83,6 @@ public class AppMainWindow {
private void logChange() { private void logChange() {
TrackEvent.withDebug("Window resize") TrackEvent.withDebug("Window resize")
.windowCategory()
.tag("x", stage.getX()) .tag("x", stage.getX())
.tag("y", stage.getY()) .tag("y", stage.getY())
.tag("width", stage.getWidth()) .tag("width", stage.getWidth())
@ -98,7 +97,6 @@ public class AppMainWindow {
applyState(state); applyState(state);
TrackEvent.withDebug("Window initialized") TrackEvent.withDebug("Window initialized")
.windowCategory()
.tag("x", stage.getX()) .tag("x", stage.getX())
.tag("y", stage.getY()) .tag("y", stage.getY())
.tag("width", stage.getWidth()) .tag("width", stage.getWidth())

View file

@ -19,15 +19,20 @@ public class AppProperties {
private static final String EXTENSION_PATHS_PROP = "io.xpipe.app.extensions"; private static final String EXTENSION_PATHS_PROP = "io.xpipe.app.extensions";
private static AppProperties INSTANCE; private static AppProperties INSTANCE;
boolean fullVersion; boolean fullVersion;
@Getter @Getter
String version; String version;
@Getter @Getter
String build; String build;
UUID buildUuid; UUID buildUuid;
String sentryUrl; String sentryUrl;
String arch; String arch;
@Getter @Getter
boolean image; boolean image;
boolean staging; boolean staging;
boolean useVirtualThreads; boolean useVirtualThreads;
boolean debugThreads; boolean debugThreads;
@ -101,6 +106,10 @@ public class AppProperties {
return INSTANCE; return INSTANCE;
} }
public boolean isDevelopmentEnvironment() {
return !AppProperties.get().isImage() && AppProperties.get().isDeveloperMode();
}
public boolean isDeveloperMode() { public boolean isDeveloperMode() {
if (AppPrefs.get() == null) { if (AppPrefs.get() == null) {
return false; return false;
@ -108,5 +117,4 @@ public class AppProperties {
return AppPrefs.get().developerMode().getValue(); return AppPrefs.get().developerMode().getValue();
} }
} }

View file

@ -57,7 +57,10 @@ public class AppSocketServer {
.handle(); .handle();
} catch (Exception ex) { } catch (Exception ex) {
// Not terminal! // Not terminal!
ErrorEvent.fromThrowable(ex).description("Unable to start local socket server on port " + port).build().handle(); ErrorEvent.fromThrowable(ex)
.description("Unable to start local socket server on port " + port)
.build()
.handle();
} }
} }
@ -112,7 +115,7 @@ public class AppSocketServer {
private boolean performExchange(Socket clientSocket, int id) throws Exception { private boolean performExchange(Socket clientSocket, int id) throws Exception {
if (clientSocket.isClosed()) { if (clientSocket.isClosed()) {
TrackEvent.trace("beacon", "Socket closed"); TrackEvent.trace("Socket closed");
return false; return false;
} }
@ -121,14 +124,14 @@ public class AppSocketServer {
node = JacksonMapper.getDefault().readTree(blockIn); node = JacksonMapper.getDefault().readTree(blockIn);
} }
if (node.isMissingNode()) { if (node.isMissingNode()) {
TrackEvent.trace("beacon", "Received EOF"); TrackEvent.trace("Received EOF");
return false; return false;
} }
TrackEvent.trace("beacon", "Received raw request: \n" + node.toPrettyString()); TrackEvent.trace("Received raw request: \n" + node.toPrettyString());
var req = parseRequest(node); var req = parseRequest(node);
TrackEvent.trace("beacon", "Parsed request: \n" + req.toString()); TrackEvent.trace("Parsed request: \n" + req.toString());
var prov = MessageExchangeImpls.byRequest(req); var prov = MessageExchangeImpls.byRequest(req);
if (prov.isEmpty()) { if (prov.isEmpty()) {
@ -145,19 +148,19 @@ public class AppSocketServer {
@Override @Override
public OutputStream sendBody() throws IOException { public OutputStream sendBody() throws IOException {
TrackEvent.trace("beacon", "Starting writing body for #" + id); TrackEvent.trace("Starting writing body for #" + id);
return AppSocketServer.this.sendBody(clientSocket); return AppSocketServer.this.sendBody(clientSocket);
} }
@Override @Override
public InputStream receiveBody() throws IOException { public InputStream receiveBody() throws IOException {
TrackEvent.trace("beacon", "Starting to read body for #" + id); TrackEvent.trace("Starting to read body for #" + id);
return AppSocketServer.this.receiveBody(clientSocket); return AppSocketServer.this.receiveBody(clientSocket);
} }
}, },
req); req);
TrackEvent.trace("beacon", "Sending response to #" + id + ": \n" + res.toString()); TrackEvent.trace("Sending response to #" + id + ": \n" + res.toString());
AppSocketServer.this.sendResponse(clientSocket, res); AppSocketServer.this.sendResponse(clientSocket, res);
try { try {
@ -170,7 +173,6 @@ public class AppSocketServer {
} }
TrackEvent.builder() TrackEvent.builder()
.category("beacon")
.type("trace") .type("trace")
.message("Socket connection #" + id + " performed exchange " .message("Socket connection #" + id + " performed exchange "
+ req.getClass().getSimpleName()) + req.getClass().getSimpleName())
@ -187,7 +189,7 @@ public class AppSocketServer {
informationNode = JacksonMapper.getDefault().readTree(blockIn); informationNode = JacksonMapper.getDefault().readTree(blockIn);
} }
if (informationNode.isMissingNode()) { if (informationNode.isMissingNode()) {
TrackEvent.trace("beacon", "Received EOF"); TrackEvent.trace("Received EOF");
return; return;
} }
var information = var information =
@ -197,7 +199,6 @@ public class AppSocketServer {
} }
TrackEvent.builder() TrackEvent.builder()
.category("beacon")
.type("trace") .type("trace")
.message("Created new socket connection #" + id) .message("Created new socket connection #" + id)
.tag("client", information != null ? information.toDisplayString() : "Unknown") .tag("client", information != null ? information.toDisplayString() : "Unknown")
@ -211,29 +212,29 @@ public class AppSocketServer {
} }
} }
TrackEvent.builder() TrackEvent.builder()
.category("beacon")
.type("trace") .type("trace")
.message("Socket connection #" + id + " finished successfully") .message("Socket connection #" + id + " finished successfully")
.build() .build()
.handle(); .handle();
} catch (ClientException ce) { } catch (ClientException ce) {
TrackEvent.trace("beacon", "Sending client error to #" + id + ": " + ce.getMessage()); TrackEvent.trace("Sending client error to #" + id + ": " + ce.getMessage());
sendClientErrorResponse(clientSocket, ce.getMessage()); sendClientErrorResponse(clientSocket, ce.getMessage());
} catch (ServerException se) { } catch (ServerException se) {
TrackEvent.trace("beacon", "Sending server error to #" + id + ": " + se.getMessage()); TrackEvent.trace("Sending server error to #" + id + ": " + se.getMessage());
ErrorEvent.fromThrowable(se).build().handle();
Deobfuscator.deobfuscate(se); Deobfuscator.deobfuscate(se);
sendServerErrorResponse(clientSocket, se); sendServerErrorResponse(clientSocket, se);
var toReport = se.getCause() != null ? se.getCause() : se;
ErrorEvent.fromThrowable(toReport).build().handle();
} catch (SocketException ex) { } catch (SocketException ex) {
// Do not send error and omit it, as this might happen often // Do not send error and omit it, as this might happen often
// We do not send the error as the socket connection might be broken // We do not send the error as the socket connection might be broken
ErrorEvent.fromThrowable(ex).omitted(true).build().handle(); ErrorEvent.fromThrowable(ex).omitted(true).build().handle();
} catch (Throwable ex) { } catch (Throwable ex) {
TrackEvent.trace("beacon", "Sending internal server error to #" + id + ": " + ex.getMessage()); TrackEvent.trace("Sending internal server error to #" + id + ": " + ex.getMessage());
ErrorEvent.fromThrowable(ex).build().handle();
Deobfuscator.deobfuscate(ex); Deobfuscator.deobfuscate(ex);
sendServerErrorResponse(clientSocket, ex); sendServerErrorResponse(clientSocket, ex);
ErrorEvent.fromThrowable(ex).build().handle();
} }
} catch (SocketException ex) { } catch (SocketException ex) {
// Omit it, as this might happen often // Omit it, as this might happen often
@ -243,16 +244,13 @@ public class AppSocketServer {
} finally { } finally {
try { try {
clientSocket.close(); clientSocket.close();
TrackEvent.trace("beacon", "Closed socket #" + id); TrackEvent.trace("Closed socket #" + id);
} catch (IOException e) { } catch (IOException e) {
ErrorEvent.fromThrowable(e).build().handle(); ErrorEvent.fromThrowable(e).build().handle();
} }
} }
TrackEvent.builder() TrackEvent.builder().type("trace").message("Socket connection #" + id + " finished unsuccessfully");
.category("beacon")
.type("trace")
.message("Socket connection #" + id + " finished unsuccessfully");
} }
private void performExchangesAsync(Socket clientSocket) { private void performExchangesAsync(Socket clientSocket) {
@ -296,7 +294,7 @@ public class AppSocketServer {
} }
var content = writer.toString(); var content = writer.toString();
TrackEvent.trace("beacon", "Sending raw response:\n" + content); TrackEvent.trace("Sending raw response:\n" + content);
try (OutputStream blockOut = BeaconFormat.writeBlocks(outSocket.getOutputStream())) { try (OutputStream blockOut = BeaconFormat.writeBlocks(outSocket.getOutputStream())) {
blockOut.write(content.getBytes(StandardCharsets.UTF_8)); blockOut.write(content.getBytes(StandardCharsets.UTF_8));
} }
@ -336,7 +334,7 @@ public class AppSocketServer {
private <T extends RequestMessage> T parseRequest(JsonNode header) throws Exception { private <T extends RequestMessage> T parseRequest(JsonNode header) throws Exception {
ObjectNode content = (ObjectNode) header.required("xPipeMessage"); ObjectNode content = (ObjectNode) header.required("xPipeMessage");
TrackEvent.trace("beacon", "Parsed raw request:\n" + content.toPrettyString()); TrackEvent.trace("Parsed raw request:\n" + content.toPrettyString());
var type = content.required("messageType").textValue(); var type = content.required("messageType").textValue();
var phase = content.required("messagePhase").textValue(); var phase = content.required("messagePhase").textValue();

View file

@ -17,6 +17,7 @@ public class AppState {
@NonFinal @NonFinal
@Setter @Setter
String userName; String userName;
@NonFinal @NonFinal
@Setter @Setter
String userEmail; String userEmail;

View file

@ -28,7 +28,7 @@ public class AppStyle {
loadStylesheets(); loadStylesheets();
if (AppPrefs.get() != null) { if (AppPrefs.get() != null) {
AppPrefs.get().useSystemFont.addListener((c, o, n) -> { AppPrefs.get().useSystemFont().addListener((c, o, n) -> {
changeFontUsage(n); changeFontUsage(n);
}); });
} }
@ -48,17 +48,19 @@ public class AppStyle {
return; return;
} }
TrackEvent.trace("core", "Loading styles for module " + module.getName()); TrackEvent.trace("Loading styles for module " + module.getName());
Files.walkFileTree(path, new SimpleFileVisitor<>() { Files.walkFileTree(path, new SimpleFileVisitor<>() {
@Override @Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
try { try {
var bytes = Files.readAllBytes(file); var bytes = Files.readAllBytes(file);
if (file.getFileName().toString().endsWith(".bss")) { if (file.getFileName().toString().endsWith(".bss")) {
var s = "data:application/octet-stream;base64," + Base64.getEncoder().encodeToString(bytes); var s = "data:application/octet-stream;base64,"
+ Base64.getEncoder().encodeToString(bytes);
STYLESHEET_CONTENTS.put(file, s); STYLESHEET_CONTENTS.put(file, s);
} else if (file.getFileName().toString().endsWith(".css")) { } else if (file.getFileName().toString().endsWith(".css")) {
var s = "data:text/css;base64," + Base64.getEncoder().encodeToString(bytes); var s = "data:text/css;base64,"
+ Base64.getEncoder().encodeToString(bytes);
STYLESHEET_CONTENTS.put(file, s); STYLESHEET_CONTENTS.put(file, s);
} }
} catch (IOException ex) { } catch (IOException ex) {
@ -93,7 +95,7 @@ public class AppStyle {
} }
public static void addStylesheets(Scene scene) { public static void addStylesheets(Scene scene) {
if (AppPrefs.get() != null && !AppPrefs.get().useSystemFont.get()) { if (AppPrefs.get() != null && !AppPrefs.get().useSystemFont().getValue()) {
scene.getStylesheets().add(FONT_CONTENTS); scene.getStylesheets().add(FONT_CONTENTS);
} }

View file

@ -1,7 +1,6 @@
package io.xpipe.app.core; package io.xpipe.app.core;
import atlantafx.base.theme.*; import atlantafx.base.theme.*;
import com.jthemedetecor.OsThemeDetector;
import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.fxcomps.util.SimpleChangeListener;
@ -14,7 +13,10 @@ import javafx.animation.KeyFrame;
import javafx.animation.KeyValue; import javafx.animation.KeyValue;
import javafx.animation.Timeline; import javafx.animation.Timeline;
import javafx.application.Application; import javafx.application.Application;
import javafx.application.ColorScheme;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
@ -71,30 +73,23 @@ public class AppTheme {
} }
try { try {
OsThemeDetector detector = OsThemeDetector.getDetector();
if (AppPrefs.get().theme.getValue() == null) { if (AppPrefs.get().theme.getValue() == null) {
try { setDefault(Platform.getPreferences().getColorScheme());
setDefault(detector.isDark());
} catch (Throwable ex) {
ErrorEvent.fromThrowable(ex).omit().handle();
setDefault(false);
}
} }
// The gnome detector sometimes runs into issues, also it's not that important Platform.getPreferences().colorSchemeProperty().addListener((observableValue, colorScheme, t1) -> {
if (!OsType.getLocal().equals(OsType.LINUX)) { Platform.runLater(() -> {
detector.registerListener(dark -> { if (t1 == ColorScheme.DARK
PlatformThread.runLaterIfNeeded(() -> { && !AppPrefs.get().theme.getValue().isDark()) {
if (dark && !AppPrefs.get().theme.getValue().isDark()) {
AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme()); AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());
} }
if (!dark && AppPrefs.get().theme.getValue().isDark()) { if (t1 != ColorScheme.DARK
&& AppPrefs.get().theme.getValue().isDark()) {
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme()); AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
} }
}); });
}); });
}
} catch (Throwable t) { } catch (Throwable t) {
ErrorEvent.fromThrowable(t).omit().handle(); ErrorEvent.fromThrowable(t).omit().handle();
} }
@ -110,8 +105,8 @@ public class AppTheme {
init = true; init = true;
} }
private static void setDefault(boolean dark) { private static void setDefault(ColorScheme colorScheme) {
if (dark) { if (colorScheme == ColorScheme.DARK) {
AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme()); AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());
} else { } else {
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme()); AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
@ -189,8 +184,8 @@ public class AppTheme {
} }
@Override @Override
public String toTranslatedString() { public ObservableValue<String> toTranslatedString() {
return name; return new SimpleStringProperty(name);
} }
} }
@ -211,6 +206,12 @@ public class AppTheme {
// Also include your custom theme here // Also include your custom theme here
public static final List<Theme> ALL = public static final List<Theme> ALL =
List.of(PRIMER_LIGHT, PRIMER_DARK, NORD_LIGHT, NORD_DARK, CUPERTINO_LIGHT, CUPERTINO_DARK, DRACULA); List.of(PRIMER_LIGHT, PRIMER_DARK, NORD_LIGHT, NORD_DARK, CUPERTINO_LIGHT, CUPERTINO_DARK, DRACULA);
protected final String id;
@Getter
protected final String cssId;
protected final atlantafx.base.theme.Theme theme;
static Theme getDefaultLightTheme() { static Theme getDefaultLightTheme() {
return switch (OsType.getLocal()) { return switch (OsType.getLocal()) {
@ -228,13 +229,6 @@ public class AppTheme {
}; };
} }
protected final String id;
@Getter
protected final String cssId;
protected final atlantafx.base.theme.Theme theme;
public boolean isDark() { public boolean isDark() {
return theme.isDarkMode(); return theme.isDarkMode();
} }
@ -244,8 +238,8 @@ public class AppTheme {
} }
@Override @Override
public String toTranslatedString() { public ObservableValue<String> toTranslatedString() {
return theme.getName(); return new SimpleStringProperty(theme.getName());
} }
@Override @Override

View file

@ -14,6 +14,7 @@ public class AppTray {
private static AppTray INSTANCE; private static AppTray INSTANCE;
private final AppTrayIcon icon; private final AppTrayIcon icon;
@Getter @Getter
private final ErrorHandler errorHandler; private final ErrorHandler errorHandler;

View file

@ -14,21 +14,23 @@ public class AppTrayIcon {
private final SystemTray tray; private final SystemTray tray;
private final TrayIcon trayIcon; private final TrayIcon trayIcon;
private final PopupMenu popupMenu = new PopupMenu();
public AppTrayIcon() { public AppTrayIcon() {
ensureSystemTraySupported(); ensureSystemTraySupported();
tray = SystemTray.getSystemTray(); tray = SystemTray.getSystemTray();
var image = switch (OsType.getLocal()) { var image =
switch (OsType.getLocal()) {
case OsType.Windows windows -> "img/logo/logo_16x16.png"; case OsType.Windows windows -> "img/logo/logo_16x16.png";
case OsType.Linux linux -> "img/logo/logo_24x24.png"; case OsType.Linux linux -> "img/logo/logo_24x24.png";
case OsType.MacOs macOs -> "img/logo/logo_macos_tray_24x24.png"; case OsType.MacOs macOs -> "img/logo/logo_macos_tray_24x24.png";
}; };
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, image).orElseThrow(); var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, image).orElseThrow();
this.trayIcon = new TrayIcon(loadImageFromURL(url), App.getApp().getStage().getTitle(), popupMenu); PopupMenu popupMenu = new PopupMenu();
this.trayIcon =
new TrayIcon(loadImageFromURL(url), App.getApp().getStage().getTitle(), popupMenu);
this.trayIcon.setToolTip("XPipe"); this.trayIcon.setToolTip("XPipe");
this.trayIcon.setImageAutoSize(true); this.trayIcon.setImageAutoSize(true);
@ -58,6 +60,19 @@ public class AppTrayIcon {
}); });
} }
private static Image loadImageFromURL(URL iconImagePath) {
try {
return ImageIO.read(iconImagePath);
} catch (IOException e) {
ErrorEvent.fromThrowable(e).handle();
return AppImages.toAwtImage(AppImages.DEFAULT_IMAGE);
}
}
public static boolean isSupported() {
return Desktop.isDesktopSupported() && SystemTray.isSupported();
}
public final TrayIcon getAwtTrayIcon() { public final TrayIcon getAwtTrayIcon() {
return trayIcon; return trayIcon;
} }
@ -65,17 +80,7 @@ public class AppTrayIcon {
private void ensureSystemTraySupported() { private void ensureSystemTraySupported() {
if (!SystemTray.isSupported()) { if (!SystemTray.isSupported()) {
throw new UnsupportedOperationException( throw new UnsupportedOperationException(
"SystemTray icons are not " "SystemTray icons are not " + "supported by the current desktop environment.");
+ "supported by the current desktop environment.");
}
}
private static Image loadImageFromURL(URL iconImagePath) {
try {
return ImageIO.read(iconImagePath);
} catch (IOException e) {
ErrorEvent.fromThrowable(e).handle();
return AppImages.toAwtImage(AppImages.DEFAULT_IMAGE);
} }
} }
@ -129,11 +134,9 @@ public class AppTrayIcon {
public void showInfoMessage(String title, String message) { public void showInfoMessage(String title, String message) {
if (OsType.getLocal().equals(OsType.MACOS)) { if (OsType.getLocal().equals(OsType.MACOS)) {
showMacAlert(title, message,"Information"); showMacAlert(title, message, "Information");
} else { } else {
EventQueue.invokeLater(() -> EventQueue.invokeLater(() -> this.trayIcon.displayMessage(title, message, TrayIcon.MessageType.INFO));
this.trayIcon.displayMessage(
title, message, TrayIcon.MessageType.INFO));
} }
} }
@ -143,11 +146,9 @@ public class AppTrayIcon {
public void showWarningMessage(String title, String message) { public void showWarningMessage(String title, String message) {
if (OsType.getLocal().equals(OsType.MACOS)) { if (OsType.getLocal().equals(OsType.MACOS)) {
showMacAlert(title, message,"Warning"); showMacAlert(title, message, "Warning");
} else { } else {
EventQueue.invokeLater(() -> EventQueue.invokeLater(() -> this.trayIcon.displayMessage(title, message, TrayIcon.MessageType.WARNING));
this.trayIcon.displayMessage(
title, message, TrayIcon.MessageType.WARNING));
} }
} }
@ -157,11 +158,9 @@ public class AppTrayIcon {
public void showErrorMessage(String title, String message) { public void showErrorMessage(String title, String message) {
if (OsType.getLocal().equals(OsType.MACOS)) { if (OsType.getLocal().equals(OsType.MACOS)) {
showMacAlert(title, message,"Error"); showMacAlert(title, message, "Error");
} else { } else {
EventQueue.invokeLater(() -> EventQueue.invokeLater(() -> this.trayIcon.displayMessage(title, message, TrayIcon.MessageType.ERROR));
this.trayIcon.displayMessage(
title, message, TrayIcon.MessageType.ERROR));
} }
} }
@ -171,11 +170,9 @@ public class AppTrayIcon {
public void showMessage(String title, String message) { public void showMessage(String title, String message) {
if (OsType.getLocal().equals(OsType.MACOS)) { if (OsType.getLocal().equals(OsType.MACOS)) {
showMacAlert(title, message,"Message"); showMacAlert(title, message, "Message");
} else { } else {
EventQueue.invokeLater(() -> EventQueue.invokeLater(() -> this.trayIcon.displayMessage(title, message, TrayIcon.MessageType.NONE));
this.trayIcon.displayMessage(
title, message, TrayIcon.MessageType.NONE));
} }
} }
@ -183,26 +180,14 @@ public class AppTrayIcon {
this.showMessage(null, message); this.showMessage(null, message);
} }
public static boolean isSupported() {
return Desktop.isDesktopSupported() && SystemTray.isSupported();
}
private void showMacAlert(String subTitle, String message, String title) { private void showMacAlert(String subTitle, String message, String title) {
String execute = String.format( String execute = String.format(
"display notification \"%s\"" "display notification \"%s\"" + " with title \"%s\"" + " subtitle \"%s\"",
+ " with title \"%s\"" message != null ? message : "", title != null ? title : "", subTitle != null ? subTitle : "");
+ " subtitle \"%s\"",
message != null ? message : "",
title != null ? title : "",
subTitle != null ? subTitle : ""
);
try { try {
Runtime.getRuntime() Runtime.getRuntime().exec(new String[] {"osascript", "-e", execute});
.exec(new String[] { "osascript", "-e", execute });
} catch (IOException e) { } catch (IOException e) {
throw new UnsupportedOperationException( throw new UnsupportedOperationException("Cannot run osascript with given parameters.");
"Cannot run osascript with given parameters.");
} }
} }
} }

View file

@ -3,6 +3,7 @@ package io.xpipe.app.core;
import io.xpipe.app.comp.base.LoadingOverlayComp; import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import javafx.application.Platform; import javafx.application.Platform;
@ -19,6 +20,7 @@ import javafx.scene.layout.Pane;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.stage.Modality;
import javafx.stage.Screen; import javafx.stage.Screen;
import javafx.stage.Stage; import javafx.stage.Stage;
import javafx.stage.Window; import javafx.stage.Window;
@ -63,6 +65,9 @@ public class AppWindowHelper {
public static Stage sideWindow( public static Stage sideWindow(
String title, Function<Stage, Comp<?>> contentFunc, boolean bindSize, ObservableValue<Boolean> loading) { String title, Function<Stage, Comp<?>> contentFunc, boolean bindSize, ObservableValue<Boolean> loading) {
var stage = new Stage(); var stage = new Stage();
if (AppMainWindow.getInstance() != null) {
stage.initOwner(AppMainWindow.getInstance().getStage());
}
stage.setTitle(title); stage.setTitle(title);
if (AppMainWindow.getInstance() != null) { if (AppMainWindow.getInstance() != null) {
stage.initOwner(AppMainWindow.getInstance().getStage()); stage.initOwner(AppMainWindow.getInstance().getStage());
@ -72,6 +77,10 @@ public class AppWindowHelper {
setupContent(stage, contentFunc, bindSize, loading); setupContent(stage, contentFunc, bindSize, loading);
setupStylesheets(stage.getScene()); setupStylesheets(stage.getScene());
if (AppPrefs.get() != null && AppPrefs.get().enforceWindowModality().get()) {
stage.initModality(Modality.WINDOW_MODAL);
}
stage.setOnShown(e -> { stage.setOnShown(e -> {
// If we set the theme pseudo classes earlier when the window is not shown // If we set the theme pseudo classes earlier when the window is not shown
// they do not apply. Is this a bug in JavaFX? // they do not apply. Is this a bug in JavaFX?
@ -100,8 +109,7 @@ public class AppWindowHelper {
childStage.setY(stage.getY() + stage.getHeight() / 2 - childStage.getHeight() / 2); childStage.setY(stage.getY() + stage.getHeight() / 2 - childStage.getHeight() / 2);
} }
public static void showAlert( public static void showAlert(Consumer<Alert> c, Consumer<Optional<ButtonType>> bt) {
Consumer<Alert> c, Consumer<Optional<ButtonType>> bt) {
ThreadHelper.runAsync(() -> { ThreadHelper.runAsync(() -> {
var r = showBlockingAlert(c); var r = showBlockingAlert(c);
if (bt != null) { if (bt != null) {
@ -110,6 +118,36 @@ public class AppWindowHelper {
}); });
} }
public static void setContent(Alert alert, String s) {
alert.getDialogPane().setMinWidth(505);
alert.getDialogPane().setPrefWidth(505);
alert.getDialogPane().setMaxWidth(505);
alert.getDialogPane().setContent(AppWindowHelper.alertContentText(s));
}
public static boolean showConfirmationAlert(String title, String header, String content) {
return AppWindowHelper.showBlockingAlert(alert -> {
alert.titleProperty().bind(AppI18n.observable(title));
alert.headerTextProperty().bind(AppI18n.observable(header));
setContent(alert, AppI18n.get(content));
alert.setAlertType(Alert.AlertType.CONFIRMATION);
})
.map(b -> b.getButtonData().isDefaultButton())
.orElse(false);
}
public static boolean showConfirmationAlert(
ObservableValue<String> title, ObservableValue<String> header, ObservableValue<String> content) {
return AppWindowHelper.showBlockingAlert(alert -> {
alert.titleProperty().bind(title);
alert.headerTextProperty().bind(header);
setContent(alert, content.getValue());
alert.setAlertType(Alert.AlertType.CONFIRMATION);
})
.map(b -> b.getButtonData().isDefaultButton())
.orElse(false);
}
public static Optional<ButtonType> showBlockingAlert(Consumer<Alert> c) { public static Optional<ButtonType> showBlockingAlert(Consumer<Alert> c) {
Supplier<Alert> supplier = () -> { Supplier<Alert> supplier = () -> {
Alert a = AppWindowHelper.createEmptyAlert(); Alert a = AppWindowHelper.createEmptyAlert();
@ -224,7 +262,6 @@ public class AppWindowHelper {
if (event.getCode().equals(KeyCode.W) && event.isShortcutDown()) { if (event.getCode().equals(KeyCode.W) && event.isShortcutDown()) {
stage.close(); stage.close();
event.consume(); event.consume();
return;
} }
} }
}); });
@ -236,7 +273,11 @@ public class AppWindowHelper {
} }
var allScreenBounds = computeWindowScreenBounds(stage); var allScreenBounds = computeWindowScreenBounds(stage);
if (!areNumbersValid(allScreenBounds.getMinX(), allScreenBounds.getMinY(), allScreenBounds.getMaxX(), allScreenBounds.getMaxY())) { if (!areNumbersValid(
allScreenBounds.getMinX(),
allScreenBounds.getMinY(),
allScreenBounds.getMaxX(),
allScreenBounds.getMaxY())) {
return Optional.empty(); return Optional.empty();
} }
@ -287,41 +328,44 @@ public class AppWindowHelper {
private static List<Screen> getWindowScreens(Stage stage) { private static List<Screen> getWindowScreens(Stage stage) {
if (!areNumbersValid(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight())) { if (!areNumbersValid(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight())) {
return stage.getOwner() != null && stage.getOwner() instanceof Stage ownerStage ? getWindowScreens(ownerStage) : List.of(Screen.getPrimary()); return stage.getOwner() != null && stage.getOwner() instanceof Stage ownerStage
? getWindowScreens(ownerStage)
: List.of(Screen.getPrimary());
} }
return Screen.getScreensForRectangle(new Rectangle2D(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight())); return Screen.getScreensForRectangle(
new Rectangle2D(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight()));
} }
private static Rectangle2D computeWindowScreenBounds(Stage stage) { private static Rectangle2D computeWindowScreenBounds(Stage stage) {
double minX = Double.POSITIVE_INFINITY ; double minX = Double.POSITIVE_INFINITY;
double minY = Double.POSITIVE_INFINITY ; double minY = Double.POSITIVE_INFINITY;
double maxX = Double.NEGATIVE_INFINITY ; double maxX = Double.NEGATIVE_INFINITY;
double maxY = Double.NEGATIVE_INFINITY ; double maxY = Double.NEGATIVE_INFINITY;
for (Screen screen : getWindowScreens(stage)) { for (Screen screen : getWindowScreens(stage)) {
Rectangle2D screenBounds = screen.getBounds(); Rectangle2D screenBounds = screen.getBounds();
if (screenBounds.getMinX() < minX) { if (screenBounds.getMinX() < minX) {
minX = screenBounds.getMinX(); minX = screenBounds.getMinX();
} }
if (screenBounds.getMinY() < minY) { if (screenBounds.getMinY() < minY) {
minY = screenBounds.getMinY() ; minY = screenBounds.getMinY();
} }
if (screenBounds.getMaxX() > maxX) { if (screenBounds.getMaxX() > maxX) {
maxX = screenBounds.getMaxX(); maxX = screenBounds.getMaxX();
} }
if (screenBounds.getMaxY() > maxY) { if (screenBounds.getMaxY() > maxY) {
maxY = screenBounds.getMaxY() ; maxY = screenBounds.getMaxY();
} }
} }
// Taskbar adjustment // Taskbar adjustment
maxY -= 50; maxY -= 50;
var w = maxX-minX; var w = maxX - minX;
var h = maxY-minY; var h = maxY - minY;
// This should not happen but on weird Linux systems nothing is impossible // This should not happen but on weird Linux systems nothing is impossible
if (w < 0 || h < 0) { if (w < 0 || h < 0) {
return new Rectangle2D(0,0,800, 600); return new Rectangle2D(0, 0, 800, 600);
} }
return new Rectangle2D(minX, minY, w, h); return new Rectangle2D(minX, minY, w, h);

View file

@ -17,54 +17,6 @@ import java.util.Optional;
public class AppAvCheck { public class AppAvCheck {
@Getter
public static enum AvType {
BITDEFENDER("Bitdefender") {
@Override
public String getDescription() {
return "Bitdefender sometimes isolates XPipe and some shell programs, effectively making it unusable.";
}
@Override
public boolean isActive() {
return WindowsRegistry.exists(WindowsRegistry.HKEY_LOCAL_MACHINE,"SOFTWARE\\Bitdefender", "InstallDir");
}
},
MALWAREBYTES("Malwarebytes") {
@Override
public String getDescription() {
return "The free Malwarebytes version performs less invasive scans, so it shouldn't be a problem. If you are running the paid Malwarebytes Pro version, you will have access to the `Exploit Protection` under the `Real-time Protection` mode. When this setting is active, any shell access is slowed down, resulting in XPipe becoming very slow.";
}
@Override
public boolean isActive() {
return WindowsRegistry.exists(WindowsRegistry.HKEY_LOCAL_MACHINE,"SOFTWARE\\Malwarebytes", "id");
}
},
MCAFEE("McAfee") {
@Override
public String getDescription() {
return "McAfee slows down XPipe considerably. It also sometimes preemptively disables some Win32 commands that XPipe depends on, leading to errors.";
}
@Override
public boolean isActive() {
return WindowsRegistry.exists(WindowsRegistry.HKEY_LOCAL_MACHINE,"SOFTWARE\\McAfee", "mi");
}
};
private final String name;
AvType(String name) {
this.name = name;
}
public abstract String getDescription();
public abstract boolean isActive();
}
private static Optional<AvType> detect() { private static Optional<AvType> detect() {
for (AvType value : AvType.values()) { for (AvType value : AvType.values()) {
if (value.isActive()) { if (value.isActive()) {
@ -93,14 +45,20 @@ public class AppAvCheck {
alert.setTitle(AppI18n.get("antivirusNoticeTitle")); alert.setTitle(AppI18n.get("antivirusNoticeTitle"));
alert.setAlertType(Alert.AlertType.NONE); alert.setAlertType(Alert.AlertType.NONE);
AppResources.with( AppResources.with(AppResources.XPIPE_MODULE, "misc/antivirus.md", file -> {
AppResources.XPIPE_MODULE,
"misc/antivirus.md",
file -> {
var markdown = new MarkdownComp(Files.readString(file), s -> { var markdown = new MarkdownComp(Files.readString(file), s -> {
var t = found.get(); var t = found.get();
return s.formatted(t.getName(), t.getName(), t.getDescription(), AppProperties.get().getVersion(), AppProperties.get().getVersion(), t.getName()); return s.formatted(
}).prefWidth(550).prefHeight(600).createRegion(); t.getName(),
t.getName(),
t.getDescription(),
AppProperties.get().getVersion(),
AppProperties.get().getVersion(),
t.getName());
})
.prefWidth(550)
.prefHeight(600)
.createRegion();
alert.getDialogPane().setContent(markdown); alert.getDialogPane().setContent(markdown);
alert.getDialogPane().setPadding(new Insets(15)); alert.getDialogPane().setPadding(new Insets(15));
}); });
@ -110,4 +68,52 @@ public class AppAvCheck {
a.filter(b -> b.getButtonData().isDefaultButton()) a.filter(b -> b.getButtonData().isDefaultButton())
.ifPresentOrElse(buttonType -> {}, () -> OperationMode.halt(1)); .ifPresentOrElse(buttonType -> {}, () -> OperationMode.halt(1));
} }
@Getter
public enum AvType {
BITDEFENDER("Bitdefender") {
@Override
public String getDescription() {
return "Bitdefender sometimes isolates XPipe and some shell programs, effectively making it unusable.";
}
@Override
public boolean isActive() {
return WindowsRegistry.exists(
WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\Bitdefender", "InstallDir");
}
},
MALWAREBYTES("Malwarebytes") {
@Override
public String getDescription() {
return "The free Malwarebytes version performs less invasive scans, so it shouldn't be a problem. If you are running the paid Malwarebytes Pro version, you will have access to the `Exploit Protection` under the `Real-time Protection` mode. When this setting is active, any shell access is slowed down, resulting in XPipe becoming very slow.";
}
@Override
public boolean isActive() {
return WindowsRegistry.exists(WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\Malwarebytes", "id");
}
},
MCAFEE("McAfee") {
@Override
public String getDescription() {
return "McAfee slows down XPipe considerably. It also sometimes preemptively disables some Win32 commands that XPipe depends on, leading to errors.";
}
@Override
public boolean isActive() {
return WindowsRegistry.exists(WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\McAfee", "mi");
}
};
private final String name;
AvType(String name) {
this.name = name;
}
public abstract String getDescription();
public abstract boolean isActive();
}
} }

View file

@ -0,0 +1,36 @@
package io.xpipe.app.core.check;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.process.OsType;
import java.util.concurrent.TimeUnit;
public class AppCertutilCheck {
private static boolean getResult() {
var fc = new ProcessBuilder(System.getenv("WINDIR") + "\\System32\\certutil")
.redirectError(ProcessBuilder.Redirect.DISCARD);
try {
var proc = fc.start();
var out = new String(proc.getInputStream().readAllBytes());
proc.waitFor(1, TimeUnit.SECONDS);
return proc.exitValue() == 0 && !out.contains("The system cannot execute the specified program");
} catch (Exception e) {
return false;
}
}
public static void check() {
if (AppPrefs.get().disableCertutilUse().get()) {
return;
}
if (!OsType.getLocal().equals(OsType.WINDOWS)) {
return;
}
if (!getResult()) {
AppPrefs.get().disableCertutilUse.set(true);
}
}
}

View file

@ -2,8 +2,8 @@ package io.xpipe.app.core.check;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.LocalShell; import io.xpipe.app.util.LocalShell;
import io.xpipe.core.process.ProcessControlProvider;
import io.xpipe.core.process.ProcessOutputException; import io.xpipe.core.process.ProcessOutputException;
import io.xpipe.core.process.ShellDialects;
import java.util.Optional; import java.util.Optional;
@ -12,7 +12,8 @@ public class AppShellCheck {
public static void check() { public static void check() {
var err = selfTestErrorCheck(); var err = selfTestErrorCheck();
if (err.isPresent()) { if (err.isPresent()) {
var msg = """ var msg =
"""
Shell self-test failed for %s: Shell self-test failed for %s:
%s %s
@ -24,7 +25,12 @@ public class AppShellCheck {
- The operating system is not supported - The operating system is not supported
You can reach out to us if you want to properly diagnose the cause individually and hopefully fix it. You can reach out to us if you want to properly diagnose the cause individually and hopefully fix it.
""".formatted(ShellDialects.getPlatformDefault().getDisplayName(), err.get()); """
.formatted(
ProcessControlProvider.get()
.getEffectiveLocalDialect()
.getDisplayName(),
err.get());
ErrorEvent.fromThrowable(new IllegalStateException(msg)).handle(); ErrorEvent.fromThrowable(new IllegalStateException(msg)).handle();
} }
} }

View file

@ -18,8 +18,8 @@ public class AppTempCheck {
} }
if (dir == null || !Files.exists(dir) || !Files.isDirectory(dir)) { if (dir == null || !Files.exists(dir) || !Files.isDirectory(dir)) {
ErrorEvent.fromThrowable( ErrorEvent.fromThrowable(new IOException("Specified temporary directory " + tmpdir
new IOException("Specified temporary directory " + tmpdir + ", set via the environment variable %TEMP% is invalid.")) + ", set via the environment variable %TEMP% is invalid."))
.term() .term()
.handle(); .handle();
} }

View file

@ -4,16 +4,18 @@ import io.xpipe.app.browser.BrowserModel;
import io.xpipe.app.comp.store.StoreViewState; import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.*; import io.xpipe.app.core.*;
import io.xpipe.app.core.check.AppAvCheck; import io.xpipe.app.core.check.AppAvCheck;
import io.xpipe.app.core.check.AppCertutilCheck;
import io.xpipe.app.core.check.AppShellCheck; import io.xpipe.app.core.check.AppShellCheck;
import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.ext.ActionProvider;
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.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.GitStorageHandler;
import io.xpipe.app.update.XPipeDistributionType; import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.FileBridge; import io.xpipe.app.util.FileBridge;
import io.xpipe.app.util.LicenseProvider; import io.xpipe.app.util.LicenseProvider;
import io.xpipe.app.util.LocalShell; import io.xpipe.app.util.LocalShell;
import io.xpipe.app.util.LockedSecretValue; import io.xpipe.app.util.UnlockAlert;
import io.xpipe.core.util.JacksonMapper; import io.xpipe.core.util.JacksonMapper;
public class BaseMode extends OperationMode { public class BaseMode extends OperationMode {
@ -39,29 +41,29 @@ public class BaseMode extends OperationMode {
// For debugging // For debugging
// if (true) throw new IllegalStateException(); // if (true) throw new IllegalStateException();
TrackEvent.info("mode", "Initializing base mode components ..."); TrackEvent.info("Initializing base mode components ...");
AppExtensionManager.init(true); AppExtensionManager.init(true);
JacksonMapper.initModularized(AppExtensionManager.getInstance().getExtendedLayer()); JacksonMapper.initModularized(AppExtensionManager.getInstance().getExtendedLayer());
JacksonMapper.configure(objectMapper -> {
objectMapper.registerSubtypes(LockedSecretValue.class);
});
// Load translations before storage initialization to localize store error messages
// Also loaded before antivirus alert to localize that
AppI18n.init(); AppI18n.init();
LicenseProvider.get().init(); LicenseProvider.get().init();
AppPrefs.initLocal();
AppCertutilCheck.check();
AppAvCheck.check(); AppAvCheck.check();
LocalShell.init(); LocalShell.init();
AppShellCheck.check();
XPipeDistributionType.init(); XPipeDistributionType.init();
AppPrefs.init(); AppShellCheck.check();
AppCharsets.init(); AppPrefs.setDefaults();
AppCharsetter.init(); // Initialize socket server as we should be prepared for git askpass commands
AppSocketServer.init(); AppSocketServer.init();
GitStorageHandler.getInstance().init();
GitStorageHandler.getInstance().setupRepositoryAndPull();
AppPrefs.initSharedRemote();
UnlockAlert.showIfNeeded();
DataStorage.init(); DataStorage.init();
AppFileWatcher.init(); AppFileWatcher.init();
FileBridge.init(); FileBridge.init();
ActionProvider.initProviders(); ActionProvider.initProviders();
TrackEvent.info("mode", "Finished base components initialization"); TrackEvent.info("Finished base components initialization");
initialized = true; initialized = true;
} }
@ -70,7 +72,7 @@ public class BaseMode extends OperationMode {
@Override @Override
public void finalTeardown() { public void finalTeardown() {
TrackEvent.info("mode", "Background mode shutdown started"); TrackEvent.info("Background mode shutdown started");
BrowserModel.DEFAULT.reset(); BrowserModel.DEFAULT.reset();
StoreViewState.reset(); StoreViewState.reset();
DataStorage.reset(); DataStorage.reset();
@ -80,6 +82,6 @@ public class BaseMode extends OperationMode {
AppDataLock.unlock(); AppDataLock.unlock();
// Shut down socket server last to keep a non-daemon thread running // Shut down socket server last to keep a non-daemon thread running
AppSocketServer.reset(); AppSocketServer.reset();
TrackEvent.info("mode", "Background mode shutdown finished"); TrackEvent.info("Background mode shutdown finished");
} }
} }

Some files were not shown because too many files have changed in this diff Show more