Merge branch 1.7 into master

This commit is contained in:
crschnick 2023-10-04 14:34:03 +00:00
parent 2429a70c42
commit d96a38d7b2
326 changed files with 2633 additions and 10111 deletions

View file

@ -1,12 +0,0 @@
package io.xpipe.api;
import java.io.InputStream;
public interface DataRaw extends DataSource {
InputStream open();
byte[] readAll();
byte[] read(int maxBytes);
}

View file

@ -1,229 +0,0 @@
package io.xpipe.api;
import io.xpipe.api.impl.DataSourceImpl;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.source.DataSourceReference;
import io.xpipe.core.source.DataSourceType;
import io.xpipe.core.store.DataStore;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* Represents a reference to a data source that is managed by XPipe.
* <p>
* The actual data is only queried when required and is not cached.
* Therefore, the queried data is always up-to-date at the point of calling a method that queries the data.
* <p>
* As soon a data source reference is created, the data source is locked
* within XPipe to prevent concurrent modification and the problems that can arise from it.
* By default, the lock is held until the calling program terminates and prevents
* other applications from modifying the data source in any way.
* To unlock the data source earlier, you can make use the {@link #unlock()} method.
*/
public interface DataSource {
/**
* NOT YET IMPLEMENTED!
* <p>
* Creates a new supplier data source that will be interpreted as the generated data source.
* In case this program should be a data source generator, this method has to be called at
* least once to register that it actually generates a data source.
* <p>
* All content that is written to this data source until the generator program terminates is
* will be available later on when the data source is used as a supplier later on.
* <p>
* In case this method is called multiple times, the same data source is returned.
*
* @return the generator data source
*/
static DataSource drain() {
return null;
}
/**
* NOT YET IMPLEMENTED!
* <p>
* Creates a data source sink that will block with any read operations
* until an external data producer routes the output into this sink.
*/
static DataSource sink() {
return null;
}
/**
* Wrapper for {@link #get(DataSourceReference)}.
*
* @throws IllegalArgumentException if {@code id} is not a valid data source id
*/
static DataSource getById(String id) {
return get(DataSourceReference.id(id));
}
/**
* Wrapper for {@link #get(DataSourceReference)} using the latest reference.
*/
static DataSource getLatest() {
return get(DataSourceReference.latest());
}
/**
* Wrapper for {@link #get(DataSourceReference)} using a name reference.
*/
static DataSource getByName(String name) {
return get(DataSourceReference.name(name));
}
/**
* Retrieves the data source for a given reference.
*
* @param ref the data source reference
*/
static DataSource get(DataSourceReference ref) {
return DataSourceImpl.get(ref);
}
/**
* Releases the lock held by this program for this data source such
* that other applications can modify the data source again.
*/
static void unlock() {
throw new UnsupportedOperationException();
}
/**
* Wrapper for {@link #create(DataStoreId, String, InputStream)} that creates an anonymous data source.
*/
static DataSource createAnonymous(String type, Path path) {
return create(null, type, path);
}
/**
* Wrapper for {@link #create(DataStoreId, String, InputStream)}.
*/
static DataSource create(DataStoreId id, String type, Path path) {
try (var in = Files.newInputStream(path)) {
return create(id, type, in);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Wrapper for {@link #create(DataStoreId, String, InputStream)} that creates an anonymous data source.
*/
static DataSource createAnonymous(String type, URL url) {
return create(null, type, url);
}
/**
* Wrapper for {@link #create(DataStoreId, String, InputStream)}.
*/
static DataSource create(DataStoreId id, String type, URL url) {
try (var in = url.openStream()) {
return create(id, type, in);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Wrapper for {@link #create(DataStoreId, String, InputStream)} that creates an anonymous data source.
*/
static DataSource createAnonymous(String type, InputStream in) {
return create(null, type, in);
}
/**
* Creates a new data source from an input stream.
*
* @param id the data source id
* @param type the data source type
* @param in the input stream to read
* @return a {@link DataSource} instances that can be used to access the underlying data
*/
static DataSource create(DataStoreId id, String type, InputStream in) {
return DataSourceImpl.create(id, type, in);
}
/**
* Creates a new data source from an input stream.
*
* @param id the data source id
* @return a {@link DataSource} instances that can be used to access the underlying data
*/
static DataSource create(DataStoreId id, io.xpipe.core.source.DataSource<?> source) {
return DataSourceImpl.create(id, source);
}
/**
* Creates a new data source from an input stream.
* 1
*
* @param id the data source id
* @param type the data source type
* @param in the data store to add
* @return a {@link DataSource} instances that can be used to access the underlying data
*/
static DataSource create(DataStoreId id, String type, DataStore in) {
return DataSourceImpl.create(id, type, in);
}
void forwardTo(DataSource target);
void appendTo(DataSource target);
io.xpipe.core.source.DataSource<?> getInternalSource();
/**
* Returns the id of this data source.
*/
DataStoreId getId();
/**
* Returns the type of this data source.
*/
DataSourceType getType();
DataSourceConfig getConfig();
/**
* Attempts to cast this object to a {@link DataTable}.
*
* @throws UnsupportedOperationException if the data source is not a table
*/
default DataTable asTable() {
throw new UnsupportedOperationException("Data source is not a table");
}
/**
* Attempts to cast this object to a {@link DataStructure}.
*
* @throws UnsupportedOperationException if the data source is not a structure
*/
default DataStructure asStructure() {
throw new UnsupportedOperationException("Data source is not a structure");
}
/**
* Attempts to cast this object to a {@link DataText}.
*
* @throws UnsupportedOperationException if the data source is not a text
*/
default DataText asText() {
throw new UnsupportedOperationException("Data source is not a text");
}
/**
* Attempts to cast this object to a {@link DataRaw}.
*
* @throws UnsupportedOperationException if the data source is not raw
*/
default DataRaw asRaw() {
throw new UnsupportedOperationException("Data source is not raw");
}
}

View file

@ -1,32 +0,0 @@
package io.xpipe.api;
import java.util.Map;
/**
* Represents the current configuration of a data source.
*/
public final class DataSourceConfig {
/**
* The data source provider id.
*/
private final String provider;
/**
* The set configuration parameters.
*/
private final Map<String, String> configInstance;
public DataSourceConfig(String provider, Map<String, String> configInstance) {
this.provider = provider;
this.configInstance = configInstance;
}
public String getProvider() {
return provider;
}
public Map<String, String> getConfig() {
return configInstance;
}
}

View file

@ -1,7 +0,0 @@
package io.xpipe.api;
import io.xpipe.core.data.node.DataStructureNode;
public interface DataStructure extends DataSource {
DataStructureNode read();
}

View file

@ -1,26 +0,0 @@
package io.xpipe.api;
import io.xpipe.core.data.node.ArrayNode;
import io.xpipe.core.data.node.TupleNode;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
public interface DataTable extends Iterable<TupleNode>, DataSource {
Stream<TupleNode> stream();
ArrayNode readAll();
ArrayNode read(int maxRows);
default int countAndDiscard() {
AtomicInteger count = new AtomicInteger();
try (var stream = stream()) {
stream.forEach(dataStructureNodes -> {
count.getAndIncrement();
});
}
return count.get();
}
}

View file

@ -1,52 +0,0 @@
package io.xpipe.api;
import io.xpipe.api.impl.DataTableAccumulatorImpl;
import io.xpipe.core.data.node.DataStructureNode;
import io.xpipe.core.data.node.DataStructureNodeAcceptor;
import io.xpipe.core.data.type.TupleType;
import io.xpipe.core.source.DataStoreId;
/**
* An accumulator for table data.
* <p>
* This class can be used to construct new table data sources by
* accumulating the rows using {@link #add(DataStructureNode)} or {@link #acceptor()} and then calling
* {@link #finish(DataStoreId)} to complete the construction process and create a new data source.
*/
public interface DataTableAccumulator {
static DataTableAccumulator create(TupleType type) {
return new DataTableAccumulatorImpl(type);
}
/**
* Wrapper for {@link #finish(DataStoreId)}.
*/
default DataTable finish(String id) {
return finish(DataStoreId.fromString(id));
}
/**
* Finishes the construction process and returns the data source reference.
*
* @param id the data source id to assign
*/
DataTable finish(DataStoreId id);
/**
* Adds a row to the table.
*
* @param row the row to add
*/
void add(DataStructureNode row);
/**
* Creates a tuple acceptor that adds all accepted tuples to the table.
*/
DataStructureNodeAcceptor<DataStructureNode> acceptor();
/**
* Returns the current amount of rows added to the table.
*/
int getCurrentRows();
}

View file

@ -1,17 +0,0 @@
package io.xpipe.api;
import java.util.List;
import java.util.stream.Stream;
public interface DataText extends DataSource {
List<String> readAllLines();
List<String> readLines(int maxLines);
Stream<String> lines();
String readAll();
String read(int maxCharacters);
}

View file

@ -1,41 +0,0 @@
package io.xpipe.api.impl;
import io.xpipe.api.DataRaw;
import io.xpipe.api.DataSourceConfig;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.source.DataSourceType;
import java.io.InputStream;
public class DataRawImpl extends DataSourceImpl implements DataRaw {
public DataRawImpl(
DataStoreId sourceId, DataSourceConfig sourceConfig, io.xpipe.core.source.DataSource<?> internalSource) {
super(sourceId, sourceConfig, internalSource);
}
@Override
public InputStream open() {
return null;
}
@Override
public byte[] readAll() {
return new byte[0];
}
@Override
public byte[] read(int maxBytes) {
return new byte[0];
}
@Override
public DataSourceType getType() {
return DataSourceType.RAW;
}
@Override
public DataRaw asRaw() {
return this;
}
}

View file

@ -1,161 +0,0 @@
package io.xpipe.api.impl;
import io.xpipe.api.DataSource;
import io.xpipe.api.DataSourceConfig;
import io.xpipe.api.connector.XPipeApiConnection;
import io.xpipe.beacon.exchange.*;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.source.DataSourceReference;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.StreamDataStore;
import java.io.InputStream;
public abstract class DataSourceImpl implements DataSource {
private final DataStoreId sourceId;
private final DataSourceConfig config;
private final io.xpipe.core.source.DataSource<?> internalSource;
public DataSourceImpl(
DataStoreId sourceId, DataSourceConfig config, io.xpipe.core.source.DataSource<?> internalSource) {
this.sourceId = sourceId;
this.config = config;
this.internalSource = internalSource;
}
public static DataSource get(DataSourceReference ds) {
return XPipeApiConnection.execute(con -> {
var req = QueryDataSourceExchange.Request.builder().ref(ds).build();
QueryDataSourceExchange.Response res = con.performSimpleExchange(req);
var config = new DataSourceConfig(res.getProvider(), res.getConfig());
return switch (res.getType()) {
case TABLE -> {
yield new DataTableImpl(res.getId(), config, res.getInternalSource());
}
case STRUCTURE -> {
yield new DataStructureImpl(res.getId(), config, res.getInternalSource());
}
case TEXT -> {
yield new DataTextImpl(res.getId(), config, res.getInternalSource());
}
case RAW -> {
yield new DataRawImpl(res.getId(), config, res.getInternalSource());
}
case COLLECTION -> throw new UnsupportedOperationException("Unimplemented case: " + res.getType());
default -> throw new IllegalArgumentException("Unexpected value: " + res.getType());
};
});
}
public static DataSource create(DataStoreId id, io.xpipe.core.source.DataSource<?> source) {
var startReq =
AddSourceExchange.Request.builder().source(source).target(id).build();
var returnedId = XPipeApiConnection.execute(con -> {
AddSourceExchange.Response r = con.performSimpleExchange(startReq);
return r.getId();
});
var ref = DataSourceReference.id(returnedId);
return get(ref);
}
public static DataSource create(DataStoreId id, String type, DataStore store) {
if (store instanceof StreamDataStore s && s.isContentExclusivelyAccessible()) {
store = XPipeApiConnection.execute(con -> {
var internal = con.createInternalStreamStore();
var req = WriteStreamExchange.Request.builder()
.name(internal.getUuid().toString())
.build();
con.performOutputExchange(req, out -> {
try (InputStream inputStream = s.openInput()) {
inputStream.transferTo(out);
}
});
return internal;
});
}
var startReq = ReadExchange.Request.builder()
.provider(type)
.store(store)
.target(id)
.configureAll(false)
.build();
var startRes = XPipeApiConnection.execute(con -> {
return con.<ReadExchange.Request, ReadExchange.Response>performSimpleExchange(startReq);
});
var configInstance = startRes.getConfig();
XPipeApiConnection.finishDialog(configInstance);
var ref = id != null ? DataSourceReference.id(id) : DataSourceReference.latest();
return get(ref);
}
public static DataSource create(DataStoreId id, String type, InputStream in) {
var store = XPipeApiConnection.execute(con -> {
var internal = con.createInternalStreamStore();
var req = WriteStreamExchange.Request.builder()
.name(internal.getUuid().toString())
.build();
con.performOutputExchange(req, out -> {
in.transferTo(out);
});
return internal;
});
var startReq = ReadExchange.Request.builder()
.provider(type)
.store(store)
.target(id)
.configureAll(false)
.build();
var startRes = XPipeApiConnection.execute(con -> {
return con.<ReadExchange.Request, ReadExchange.Response>performSimpleExchange(startReq);
});
var configInstance = startRes.getConfig();
XPipeApiConnection.finishDialog(configInstance);
var ref = id != null ? DataSourceReference.id(id) : DataSourceReference.latest();
return get(ref);
}
@Override
public void forwardTo(DataSource target) {
XPipeApiConnection.execute(con -> {
var req = ForwardExchange.Request.builder()
.source(DataSourceReference.id(sourceId))
.target(DataSourceReference.id(target.getId()))
.build();
ForwardExchange.Response res = con.performSimpleExchange(req);
});
}
@Override
public void appendTo(DataSource target) {
XPipeApiConnection.execute(con -> {
var req = ForwardExchange.Request.builder()
.source(DataSourceReference.id(sourceId))
.target(DataSourceReference.id(target.getId()))
.append(true)
.build();
ForwardExchange.Response res = con.performSimpleExchange(req);
});
}
public io.xpipe.core.source.DataSource<?> getInternalSource() {
return internalSource;
}
@Override
public DataStoreId getId() {
return sourceId;
}
@Override
public DataSourceConfig getConfig() {
return config;
}
}

View file

@ -1,30 +0,0 @@
package io.xpipe.api.impl;
import io.xpipe.api.DataSourceConfig;
import io.xpipe.api.DataStructure;
import io.xpipe.core.data.node.DataStructureNode;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.source.DataSourceType;
public class DataStructureImpl extends DataSourceImpl implements DataStructure {
DataStructureImpl(
DataStoreId sourceId, DataSourceConfig sourceConfig, io.xpipe.core.source.DataSource<?> internalSource) {
super(sourceId, sourceConfig, internalSource);
}
@Override
public DataSourceType getType() {
return DataSourceType.STRUCTURE;
}
@Override
public DataStructure asStructure() {
return this;
}
@Override
public DataStructureNode read() {
return null;
}
}

View file

@ -1,115 +0,0 @@
package io.xpipe.api.impl;
import io.xpipe.api.DataSource;
import io.xpipe.api.DataTable;
import io.xpipe.api.DataTableAccumulator;
import io.xpipe.api.connector.XPipeApiConnection;
import io.xpipe.api.util.TypeDescriptor;
import io.xpipe.beacon.BeaconException;
import io.xpipe.beacon.exchange.ReadExchange;
import io.xpipe.beacon.exchange.WriteStreamExchange;
import io.xpipe.beacon.exchange.cli.StoreAddExchange;
import io.xpipe.beacon.util.QuietDialogHandler;
import io.xpipe.core.data.node.DataStructureNode;
import io.xpipe.core.data.node.DataStructureNodeAcceptor;
import io.xpipe.core.data.node.TupleNode;
import io.xpipe.core.data.type.TupleType;
import io.xpipe.core.data.typed.TypedDataStreamWriter;
import io.xpipe.core.impl.InternalStreamStore;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.source.DataSourceReference;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
public class DataTableAccumulatorImpl implements DataTableAccumulator {
private final XPipeApiConnection connection;
private final TupleType type;
private int rows;
private final InternalStreamStore store;
private TupleType writtenDescriptor;
private final OutputStream bodyOutput;
public DataTableAccumulatorImpl(TupleType type) {
this.type = type;
connection = XPipeApiConnection.open();
store = new InternalStreamStore();
var addReq = StoreAddExchange.Request.builder()
.storeInput(store)
.name(store.getUuid().toString())
.build();
StoreAddExchange.Response addRes = connection.performSimpleExchange(addReq);
QuietDialogHandler.handle(addRes.getConfig(), connection);
connection.sendRequest(WriteStreamExchange.Request.builder()
.name(store.getUuid().toString())
.build());
bodyOutput = connection.sendBody();
}
@Override
public synchronized DataTable finish(DataStoreId id) {
try {
bodyOutput.close();
} catch (IOException e) {
throw new BeaconException(e);
}
WriteStreamExchange.Response res = connection.receiveResponse();
connection.close();
var req = ReadExchange.Request.builder()
.target(id)
.store(store)
.provider("xpbt")
.configureAll(false)
.build();
ReadExchange.Response response = XPipeApiConnection.execute(con -> {
return con.performSimpleExchange(req);
});
var configInstance = response.getConfig();
XPipeApiConnection.finishDialog(configInstance);
return DataSource.get(DataSourceReference.id(id)).asTable();
}
private void writeDescriptor() {
if (writtenDescriptor != null) {
return;
}
writtenDescriptor = TupleType.tableType(type.getNames());
connection.withOutputStream(out -> {
out.write((TypeDescriptor.create(type.getNames())).getBytes(StandardCharsets.UTF_8));
});
}
@Override
public synchronized void add(DataStructureNode row) {
TupleNode toUse = type.matches(row)
? row.asTuple()
: type.convert(row).orElseThrow().asTuple();
connection.withOutputStream(out -> {
writeDescriptor();
TypedDataStreamWriter.writeStructure(out, toUse, writtenDescriptor);
rows++;
});
}
@Override
public synchronized DataStructureNodeAcceptor<DataStructureNode> acceptor() {
return node -> {
add(node);
return true;
};
}
@Override
public synchronized int getCurrentRows() {
return rows;
}
}

View file

@ -1,61 +0,0 @@
package io.xpipe.api.impl;
import io.xpipe.api.DataSourceConfig;
import io.xpipe.api.DataTable;
import io.xpipe.core.data.node.ArrayNode;
import io.xpipe.core.data.node.DataStructureNode;
import io.xpipe.core.data.node.TupleNode;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.source.DataSourceType;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Stream;
public class DataTableImpl extends DataSourceImpl implements DataTable {
DataTableImpl(DataStoreId id, DataSourceConfig sourceConfig, io.xpipe.core.source.DataSource<?> internalSource) {
super(id, sourceConfig, internalSource);
}
@Override
public DataTable asTable() {
return this;
}
public Stream<TupleNode> stream() {
return Stream.of();
}
@Override
public DataSourceType getType() {
return DataSourceType.TABLE;
}
@Override
public ArrayNode readAll() {
return read(Integer.MAX_VALUE);
}
@Override
public ArrayNode read(int maxRows) {
List<DataStructureNode> nodes = new ArrayList<>();
return ArrayNode.of(nodes);
}
@Override
public Iterator<TupleNode> iterator() {
return new Iterator<>() {
@Override
public boolean hasNext() {
return false;
}
@Override
public TupleNode next() {
return null;
}
};
}
}

View file

@ -1,66 +0,0 @@
package io.xpipe.api.impl;
import io.xpipe.api.DataSourceConfig;
import io.xpipe.api.DataText;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.source.DataSourceType;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class DataTextImpl extends DataSourceImpl implements DataText {
DataTextImpl(
DataStoreId sourceId, DataSourceConfig sourceConfig, io.xpipe.core.source.DataSource<?> internalSource) {
super(sourceId, sourceConfig, internalSource);
}
@Override
public DataSourceType getType() {
return DataSourceType.TEXT;
}
@Override
public DataText asText() {
return this;
}
@Override
public List<String> readAllLines() {
return readLines(Integer.MAX_VALUE);
}
@Override
public List<String> readLines(int maxLines) {
try (Stream<String> lines = lines()) {
return lines.limit(maxLines).toList();
}
}
@Override
public Stream<String> lines() {
return Stream.of();
}
@Override
public String readAll() {
try (Stream<String> lines = lines()) {
return lines.collect(Collectors.joining("\n"));
}
}
@Override
public String read(int maxCharacters) {
StringBuilder builder = new StringBuilder();
lines().takeWhile(s -> {
if (builder.length() > maxCharacters) {
return false;
}
builder.append(s);
return true;
});
return builder.toString();
}
}

View file

@ -1,7 +1,7 @@
package io.xpipe.api.test;
import io.xpipe.api.DataSource;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.store.DataStoreId;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

View file

@ -138,7 +138,7 @@ application {
run {
systemProperty 'io.xpipe.app.useVirtualThreads', 'false'
systemProperty 'io.xpipe.app.mode', 'gui'
systemProperty 'io.xpipe.app.dataDir', "$projectDir/local_git2/"
systemProperty 'io.xpipe.app.dataDir', "$projectDir/local_git3/"
systemProperty 'io.xpipe.app.writeLogs', "true"
systemProperty 'io.xpipe.app.writeSysOut', "true"
systemProperty 'io.xpipe.app.developerMode', "true"

View file

@ -12,11 +12,10 @@ 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.FixedHierarchyStore;
import io.xpipe.core.store.ShellStore;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
@ -53,7 +52,7 @@ final class BrowserBookmarkList extends SimpleComp {
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore
|| storeEntryWrapper.getEntry().getStore() instanceof FixedHierarchyStore)
&& storeEntryWrapper.getEntry().getState().isUsable();
&& storeEntryWrapper.getEntry().getValidity().isUsable();
};
var section = StoreSectionMiniComp.createList(
StoreSection.createTopLevel(
@ -68,7 +67,7 @@ final class BrowserBookmarkList extends SimpleComp {
.pseudoClassStateChanged(
SELECTED,
newValue != null
&& newValue.getStore()
&& newValue.getEntry()
.equals(s.getWrapper()
.getEntry()
.getStore()));
@ -77,13 +76,10 @@ final class BrowserBookmarkList extends SimpleComp {
ThreadHelper.runFailableAsync(() -> {
var entry = s.getWrapper().getEntry();
if (entry.getStore() instanceof ShellStore fileSystem) {
BooleanScope.execute(busy, () -> {
s.getWrapper().refreshIfNeeded();
});
model.openFileSystemAsync(null, fileSystem, null, busy);
model.openFileSystemAsync(entry.ref(), null, busy);
} else if (entry.getStore() instanceof FixedHierarchyStore) {
BooleanScope.execute(busy, () -> {
s.getWrapper().refreshWithChildren();
s.getWrapper().refreshChildren();
});
}
});
@ -125,7 +121,7 @@ final class BrowserBookmarkList extends SimpleComp {
return;
}
Platform.runLater(() -> model.openExistingFileSystemIfPresent(null, store.asNeeded()));
// Platform.runLater(() -> model.openExistingFileSystemIfPresent(store.asNeeded()));
}
};
DROP_TIMER.schedule(activeTask, 500);

View file

@ -4,7 +4,7 @@ import atlantafx.base.controls.Breadcrumbs;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileNames;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBase;

View file

@ -7,7 +7,6 @@ import io.xpipe.app.browser.icon.DirectoryType;
import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.browser.icon.FileType;
import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
@ -252,7 +251,7 @@ public class BrowserComp extends SimpleComp {
.bind(Bindings.createDoubleBinding(
() -> model.getBusy().get() ? -1d : 0, PlatformThread.sync(model.getBusy())));
var image = DataStoreProviders.byStore(model.getStore()).getDisplayIconFileName(model.getStore());
var image = model.getEntry().get().getProvider().getDisplayIconFileName(model.getEntry().getStore());
var logo = PrettyImageHelper.ofFixedSquare(image, 16).createRegion();
tab.graphicProperty()

View file

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

View file

@ -12,7 +12,7 @@ import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.HumanReadableFormat;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileSystem;

View file

@ -2,7 +2,7 @@ package io.xpipe.app.browser;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileSystem;
import javafx.beans.property.Property;

View file

@ -2,10 +2,11 @@ package io.xpipe.app.browser;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.impl.FileStore;
import io.xpipe.core.store.ShellStore;
import io.xpipe.core.store.FileStore;
import io.xpipe.core.store.FileSystemStore;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
@ -76,18 +77,19 @@ public class BrowserModel {
}
public void restoreState(BrowserSavedState.Entry e, BooleanProperty busy) {
var storageEntry = DataStorage.get().getStoreEntry(e.getUuid());
var storageEntry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
storageEntry.ifPresent(entry -> {
openFileSystemAsync(null, entry.getStore().asNeeded(), e.getPath(), busy);
openFileSystemAsync(entry.ref(), e.getPath(), busy);
});
}
public void reset() {
var list = new ArrayList<BrowserSavedState.Entry>();
openFileSystems.forEach(model -> {
var storageEntry = DataStorage.get().getStoreEntryIfPresent(model.getStore());
storageEntry.ifPresent(entry -> list.add(new BrowserSavedState.Entry(
entry.getUuid(), model.getCurrentPath().get())));
if (DataStorage.get().getStoreEntries().contains(model.getEntry().get())) {
list.add(new BrowserSavedState.Entry(
model.getEntry().get().getUuid(), model.getCurrentPath().get()));
}
});
// Don't override state if it is empty
@ -138,18 +140,18 @@ public class BrowserModel {
});
}
public void openExistingFileSystemIfPresent(String name, ShellStore store) {
public void openExistingFileSystemIfPresent(DataStoreEntryRef<? extends FileSystemStore> store) {
var found = openFileSystems.stream()
.filter(model -> Objects.equals(model.getStore(), store))
.filter(model -> Objects.equals(model.getEntry(), store))
.findFirst();
if (found.isPresent()) {
selected.setValue(found.get());
} else {
openFileSystemAsync(name, store, null, null);
openFileSystemAsync(store, null, null);
}
}
public void openFileSystemAsync(String name, ShellStore store, String path, BooleanProperty externalBusy) {
public void openFileSystemAsync(DataStoreEntryRef<? extends FileSystemStore> store, String path, BooleanProperty externalBusy) {
// // Prevent multiple tabs in non browser modes
// if (!mode.equals(Mode.BROWSER)) {
// ThreadHelper.runFailableAsync(() -> {
@ -177,7 +179,7 @@ public class BrowserModel {
// Prevent multiple calls from interfering with each other
synchronized (BrowserModel.this) {
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
model = new OpenFileSystemModel(name, this, store);
model = new OpenFileSystemModel(this, store);
model.initFileSystem();
model.initSavedState();
}

View file

@ -8,7 +8,7 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;

View file

@ -9,7 +9,7 @@ import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileNames;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;

View file

@ -2,7 +2,7 @@ package io.xpipe.app.browser;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;

View file

@ -54,12 +54,12 @@ public class BrowserWelcomeComp extends SimpleComp {
var storeList = new VBox();
storeList.setSpacing(8);
state.getLastSystems().forEach(e -> {
var entry = DataStorage.get().getStoreEntry(e.getUuid());
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
if (entry.isEmpty()) {
return;
}
if (!entry.get().getState().isUsable()) {
if (!entry.get().getValidity().isUsable()) {
return;
}
@ -68,7 +68,7 @@ public class BrowserWelcomeComp extends SimpleComp {
var view = PrettyImageHelper.ofFixedSquare(graphic, 45);
view.padding(new Insets(2, 8, 2, 8));
var tile = new Tile(
DataStorage.get().getStoreBrowserDisplayName(entry.get().getStore()),
DataStorage.get().getStoreBrowserDisplayName(entry.get()),
e.getPath(),
view.createRegion());
tile.setActionHandler(() -> {

View file

@ -1,8 +1,8 @@
package io.xpipe.app.browser;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.LocalStore;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.ConnectionFileSystem;
import io.xpipe.core.store.FileKind;

View file

@ -3,10 +3,11 @@ package io.xpipe.app.browser;
import io.xpipe.app.comp.base.ModalOverlayComp;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.TerminalHelper;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.store.*;
@ -26,7 +27,7 @@ import java.util.stream.Stream;
@Getter
public final class OpenFileSystemModel {
private final FileSystemStore store;
private final DataStoreEntryRef<? extends FileSystemStore> entry;
private FileSystem fileSystem;
private final Property<String> filter = new SimpleStringProperty();
private final BrowserFileListModel fileList;
@ -42,12 +43,11 @@ public final class OpenFileSystemModel {
private final String tooltip;
private boolean local;
public OpenFileSystemModel(String name, BrowserModel browserModel, FileSystemStore store) {
public OpenFileSystemModel(BrowserModel browserModel, DataStoreEntryRef<? extends FileSystemStore> entry) {
this.browserModel = browserModel;
this.store = store;
var e = DataStorage.get().getStoreEntryIfPresent(store);
this.name = name != null ? name : e.isPresent() ? DataStorage.get().getStoreBrowserDisplayName(store) : "?";
this.tooltip = e.isPresent() ? DataStorage.get().getId(e.get()).toString() : name;
this.entry = entry;
this.name = entry.get().getName();
this.tooltip = DataStorage.get().getId(entry.getEntry()).toString();
this.inOverview.bind(Bindings.createBooleanBinding(
() -> {
return currentPath.get() == null;
@ -63,7 +63,7 @@ public final class OpenFileSystemModel {
}
BooleanScope.execute(busy, () -> {
if (store instanceof ShellStore s) {
if (entry.getStore() instanceof ShellStore s) {
c.accept(fileSystem.getShell().orElseThrow());
if (refresh) {
refreshSync();
@ -150,8 +150,7 @@ public final class OpenFileSystemModel {
if (allowCommands && evaluatedPath != null && !FileNames.isAbsolute(evaluatedPath)
&& fileSystem.getShell().isPresent()) {
var directory = currentPath.get();
var name = adjustedPath + " - "
+ DataStorage.get().getStoreDisplayName(store).orElse("?");
var name = adjustedPath + " - " + entry.get().getName();
ThreadHelper.runFailableAsync(() -> {
if (ShellDialects.ALL.stream()
.anyMatch(dialect -> adjustedPath.startsWith(dialect.getOpenCommand()))) {
@ -205,7 +204,7 @@ public final class OpenFileSystemModel {
private void cdSyncWithoutCheck(String path) throws Exception {
if (fileSystem == null) {
var fs = store.createFileSystem();
var fs = entry.getStore().createFileSystem();
fs.open();
this.fileSystem = fs;
}
@ -364,7 +363,7 @@ public final class OpenFileSystemModel {
public void initFileSystem() throws Exception {
BooleanScope.execute(busy, () -> {
var fs = store.createFileSystem();
var fs = entry.getStore().createFileSystem();
fs.open();
this.fileSystem = fs;
this.local =
@ -393,23 +392,17 @@ public final class OpenFileSystemModel {
}
BooleanScope.execute(busy, () -> {
if (store instanceof ShellStore s) {
if (entry.getStore() instanceof ShellStore s) {
var connection = ((ConnectionFileSystem) fileSystem).getShellControl();
var command = s.control()
.initWith(connection.getShellDialect().getCdCommand(directory))
.prepareTerminalOpen(directory + " - "
+ DataStorage.get().getStoreDisplayName(store)
.orElse("?"));
.prepareTerminalOpen(directory + " - " + entry.get().getName());
TerminalHelper.open(directory, command);
}
});
});
}
public OpenFileSystemHistory getHistory() {
return history;
}
public void backSync() throws Exception {
cdSyncWithoutCheck(history.back());
}

View file

@ -12,8 +12,7 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.util.JacksonMapper;
import javafx.application.Platform;
import javafx.collections.FXCollections;
@ -81,11 +80,7 @@ public class OpenFileSystemSavedState {
}
static OpenFileSystemSavedState loadForStore(OpenFileSystemModel model) {
var storageEntry = DataStorage.get()
.getStoreEntryIfPresent(model.getStore())
.map(entry -> entry.getUuid())
.orElse(UUID.randomUUID());
var state = AppCache.get("fs-state-" + storageEntry, OpenFileSystemSavedState.class, () -> {
var state = AppCache.get("fs-state-" + model.getEntry().get().getUuid(), OpenFileSystemSavedState.class, () -> {
return new OpenFileSystemSavedState();
});
state.setModel(model);
@ -127,8 +122,7 @@ public class OpenFileSystemSavedState {
return;
}
var storageEntry = DataStorage.get().getStoreEntryIfPresent(model.getStore());
storageEntry.ifPresent(entry -> AppCache.update("fs-state-" + entry.getUuid(), this));
AppCache.update("fs-state-" + model.getEntry().get().getUuid(), this);
}
public void cd(String dir) {

View file

@ -4,8 +4,9 @@ import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.core.impl.FileStore;
import io.xpipe.core.store.ShellStore;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.FileStore;
import io.xpipe.core.store.FileSystemStore;
import javafx.beans.property.Property;
import javafx.stage.FileChooser;
import javafx.stage.Window;
@ -38,7 +39,7 @@ public class StandaloneFileBrowser {
});
}
public static void openSingleFile(Supplier<ShellStore> store, Consumer<FileStore> file) {
public static void openSingleFile(Supplier<DataStoreEntryRef<? extends FileSystemStore>> store, Consumer<FileStore> file) {
PlatformThread.runLaterIfNeeded(() -> {
var model = new BrowserModel(BrowserModel.Mode.SINGLE_FILE_CHOOSER);
var comp = new BrowserComp(model)
@ -50,7 +51,7 @@ public class StandaloneFileBrowser {
window.close();
});
window.show();
model.openFileSystemAsync(null, store.get(), null, null);
model.openFileSystemAsync(store.get(), null, null);
});
}

View file

@ -5,7 +5,7 @@ import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.ScriptHelper;
import io.xpipe.app.util.TerminalHelper;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.process.ShellControl;
import org.apache.commons.io.FilenameUtils;

View file

@ -1,7 +1,7 @@
package io.xpipe.app.browser.icon;
import io.xpipe.app.core.AppResources;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileSystem;
import lombok.Getter;

View file

@ -3,66 +3,53 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.css.Size;
import javafx.css.SizeUnits;
import javafx.scene.Node;
import javafx.scene.control.MenuButton;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class DropdownComp extends Comp <CompStructure<MenuButton>>{
public class DropdownComp extends Comp<CompStructure<Button>> {
private final ObservableValue<String> name;
private final ObjectProperty<Node> graphic;
private final List<Comp<?>> items;
public DropdownComp(ObservableValue<String> name, List<Comp<?>> items) {
this.name = name;
this.graphic = new SimpleObjectProperty<>(null);
public DropdownComp(List<Comp<?>> items) {
this.items = items;
}
public DropdownComp(ObservableValue<String> name, Node graphic, List<Comp<?>> items) {
this.name = name;
this.graphic = new SimpleObjectProperty<>(graphic);
this.items = items;
}
public Node getGraphic() {
return graphic.get();
}
public ObjectProperty<Node> graphicProperty() {
return graphic;
}
@Override
public CompStructure<MenuButton> createBase() {
var button = new MenuButton(null);
if (name != null) {
button.textProperty().bind(name);
}
var graphic = getGraphic();
if (graphic instanceof FontIcon f) {
public CompStructure<Button> createBase() {
ContextMenu cm = new ContextMenu(items.stream()
.map(comp -> {
return new MenuItem(null, comp.createRegion());
})
.toArray(MenuItem[]::new));
Button button = (Button) new ButtonComp(null, () -> {})
.apply(new ContextMenuAugment<>(e -> true, () -> {
return cm;
}))
.createRegion();
button.visibleProperty()
.bind(BindingsHelper.anyMatch(cm.getItems().stream()
.map(menuItem -> menuItem.getGraphic().visibleProperty())
.toList()));
var graphic = new FontIcon("mdi2c-chevron-double-down");
SimpleChangeListener.apply(button.fontProperty(), c -> {
f.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels());
graphic.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels());
});
}
button.setGraphic(getGraphic());
button.setGraphic(graphic);
button.getStyleClass().add("dropdown-comp");
items.forEach(comp -> {
var i = new MenuItem(null,comp.createRegion());
button.getItems().add(i);
});
return new SimpleCompStructure<>(button);
}
}

View file

@ -7,8 +7,8 @@ import io.xpipe.app.ext.DownloadModuleInstall;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.LabelComp;
import io.xpipe.app.util.DynamicOptionsBuilder;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.OptionsBuilder;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.TextArea;
import javafx.scene.layout.Priority;
@ -27,7 +27,7 @@ public class InstallExtensionComp extends SimpleComp {
@Override
protected Region createSimple() {
var builder = new DynamicOptionsBuilder(false);
var builder = new OptionsBuilder();
builder.addTitle("installRequired");
var header = new LabelComp(AppI18n.observable("extensionInstallDescription"))
.apply(struc -> struc.get().setWrapText(true));

View file

@ -1,5 +1,6 @@
package io.xpipe.app.comp.base;
import atlantafx.base.theme.Styles;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp;
@ -58,6 +59,8 @@ public class IntegratedTextAreaComp extends SimpleComp {
(s) -> {
Platform.runLater(() -> value.setValue(s));
}))
.styleClass("edit-button")
.apply(struc -> struc.get().getStyleClass().remove(Styles.FLAT))
.createRegion();
}
}

View file

@ -67,11 +67,13 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
var newShown = shown.stream()
.map(v -> {
if (!cache.containsKey(v)) {
cache.put(v, compFunction.apply(v).createRegion());
var comp = compFunction.apply(v);
cache.put(v, comp != null ? comp.createRegion() : null);
}
return cache.get(v);
})
.filter(region -> region != null)
.toList();
for (int i = 0; i < newShown.size(); i++) {

View file

@ -29,7 +29,9 @@ public class NamedToggleComp extends SimpleComp {
s.setSelected(newValue);
});
});
if (name != null) {
s.textProperty().bind(PlatformThread.sync(name));
}
return s;
}
}

View file

@ -5,9 +5,12 @@ import io.xpipe.app.core.AppResources;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.StackComp;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.impl.FileNames;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.process.ShellStoreState;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.layout.Region;
import java.nio.file.Files;
@ -18,20 +21,38 @@ import java.util.Map;
public class OsLogoComp extends SimpleComp {
private final StoreEntryWrapper wrapper;
private final ObservableValue<SystemStateComp.State> state;
public OsLogoComp(StoreEntryWrapper wrapper) {
this(wrapper, new SimpleObjectProperty<>(SystemStateComp.State.SUCCESS));
}
public OsLogoComp(StoreEntryWrapper wrapper, ObservableValue<SystemStateComp.State> state) {
this.wrapper = wrapper;
this.state = state;
}
@Override
protected Region createSimple() {
var img = Bindings.createObjectBinding(
var img = BindingsHelper.persist(Bindings.createObjectBinding(
() -> {
return wrapper.getState().getValue() == DataStoreEntry.State.COMPLETE_AND_VALID
? getImage(wrapper.getInformation().get()) : null;
if (state.getValue() != SystemStateComp.State.SUCCESS) {
return null;
}
var ps = wrapper.getPersistentState().getValue();
if (!(ps instanceof ShellStoreState sss)) {
return null;
}
return getImage(sss.getOsName());
},
wrapper.getState(), wrapper.getInformation());
return new StackComp(List.of(new SystemStateComp(wrapper).hide(img.isNotNull()), PrettyImageHelper.ofSvg(img, 24, 24))).createRegion();
wrapper.getPersistentState(), state));
var hide = BindingsHelper.map(img, s -> s != null);
return new StackComp(List.of(
new SystemStateComp(state).hide(hide),
PrettyImageHelper.ofSvg(img, 24, 24).visible(hide)))
.createRegion();
}
private static final Map<String, String> ICONS = new HashMap<>();

View file

@ -5,6 +5,7 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
@ -28,18 +29,22 @@ public class StoreToggleComp extends SimpleComp {
@Override
protected Region createSimple() {
var disable = section.getWrapper().getState().map(state -> state != DataStoreEntry.State.COMPLETE_AND_VALID);
var disable = section.getWrapper().getValidity().map(state -> state != DataStoreEntry.Validity.COMPLETE);
var visible = BindingsHelper.persist(Bindings.createBooleanBinding(
() -> {
return section.getWrapper().getState().getValue() == DataStoreEntry.State.COMPLETE_AND_VALID
return section.getWrapper().getValidity().getValue() == DataStoreEntry.Validity.COMPLETE
&& section.getShowDetails().get();
},
section.getWrapper().getState(),
section.getWrapper().getValidity(),
section.getShowDetails()));
var t = new NamedToggleComp(value, AppI18n.observable(nameKey))
.visible(visible)
.disable(disable);
value.addListener((observable, oldValue, newValue) -> onChange.accept(newValue));
value.addListener((observable, oldValue, newValue) -> {
ThreadHelper.runAsync(() -> {
onChange.accept(newValue);
});
});
return t.createRegion();
}
}

View file

@ -3,16 +3,19 @@ package io.xpipe.app.comp.base;
import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.storage.store.StoreEntryWrapper;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.process.ShellStoreState;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.layout.Region;
import lombok.Getter;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.javafx.StackedFontIcon;
@Getter
public class SystemStateComp extends SimpleComp {
@ -23,23 +26,21 @@ public class SystemStateComp extends SimpleComp {
public enum State {
FAILURE,
SUCCESS,
OTHER
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;
public SystemStateComp(StoreEntryWrapper w) {
this.state = Bindings.createObjectBinding(
() -> {
return w.getState().getValue() == DataStoreEntry.State.COMPLETE_BUT_INVALID
? State.FAILURE
: w.getState().getValue() == DataStoreEntry.State.COMPLETE_AND_VALID
? State.SUCCESS
: State.OTHER;
},
w.getState());
}
@Override
protected Region createSimple() {
var icon = PlatformThread.sync(Bindings.createStringBinding(

View file

@ -1,98 +0,0 @@
package io.xpipe.app.comp.storage;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.core.source.DataSourceType;
import io.xpipe.core.store.DataFlow;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.Map;
public class DataSourceTypeComp extends SimpleComp {
public static final Map<DataSourceType, String> ICONS = Map.of(
DataSourceType.TABLE, "mdi2t-table-large",
DataSourceType.STRUCTURE, "mdi2b-beaker-outline",
DataSourceType.TEXT, "mdi2t-text-box",
DataSourceType.RAW, "mdi2c-card-outline",
DataSourceType.COLLECTION, "mdi2b-briefcase-outline");
private static final String MISSING_ICON = "mdi2c-comment-question-outline";
private static final Color MISSING_COLOR = Color.RED;
private static final Map<DataSourceType, Color> COLORS = Map.of(
DataSourceType.TABLE, Color.rgb(0, 160, 0, 0.5),
DataSourceType.STRUCTURE, Color.ORANGERED,
DataSourceType.TEXT, Color.LIGHTBLUE,
DataSourceType.RAW, Color.GREY,
DataSourceType.COLLECTION, Color.ORCHID.deriveColor(0, 1.0, 0.85, 1.0));
private final DataSourceType type;
private final DataFlow flow;
public DataSourceTypeComp(DataSourceType type, DataFlow flow) {
this.type = type;
this.flow = flow;
}
@Override
protected Region createSimple() {
var bg = new Region();
bg.setBackground(new Background(new BackgroundFill(
type != null ? COLORS.get(type) : MISSING_COLOR, new CornerRadii(12), Insets.EMPTY)));
bg.getStyleClass().add("background");
var sp = new StackPane(bg);
sp.setAlignment(Pos.CENTER);
sp.getStyleClass().add("data-source-type-comp");
var icon = new FontIcon(type != null ? ICONS.get(type) : MISSING_ICON);
icon.iconSizeProperty().bind(Bindings.divide(sp.heightProperty(), 2));
sp.getChildren().add(icon);
if (flow == DataFlow.INPUT || flow == DataFlow.INPUT_OUTPUT) {
var flowIcon = createInputFlowType();
sp.getChildren().add(flowIcon);
}
if (flow == DataFlow.OUTPUT || flow == DataFlow.INPUT_OUTPUT) {
var flowIcon = createOutputFlowType();
sp.getChildren().add(flowIcon);
}
if (flow == DataFlow.TRANSFORMER) {
var flowIcon = createTransformerFlowType();
sp.getChildren().add(flowIcon);
}
return sp;
}
private Region createInputFlowType() {
var icon = new FontIcon("mdi2c-chevron-double-left");
icon.setIconColor(Color.WHITE);
var anchorPane = new AnchorPane(icon);
AnchorPane.setLeftAnchor(icon, 3.0);
AnchorPane.setBottomAnchor(icon, 3.0);
return anchorPane;
}
private Region createOutputFlowType() {
var icon = new FontIcon("mdi2c-chevron-double-right");
icon.setIconColor(Color.WHITE);
var anchorPane = new AnchorPane(icon);
AnchorPane.setRightAnchor(icon, 3.0);
AnchorPane.setBottomAnchor(icon, 3.0);
return anchorPane;
}
private Region createTransformerFlowType() {
var icon = new FontIcon("mdi2t-transfer");
icon.setIconColor(Color.WHITE);
var anchorPane = new AnchorPane(icon);
AnchorPane.setRightAnchor(icon, 3.0);
AnchorPane.setBottomAnchor(icon, 3.0);
return anchorPane;
}
}

View file

@ -31,11 +31,13 @@ public class DenseStoreEntryComp extends StoreEntryComp {
: Comp.empty();
information.setGraphic(state.createRegion());
var summary = wrapper.summary();
var info = wrapper.getEntry().getProvider().informationString(wrapper);
SimpleChangeListener.apply(grid.hoverProperty(), val -> {
if (val && wrapper.getSummary().get() != null && wrapper.getEntry().getProvider().alwaysShowSummary()) {
information.textProperty().bind(PlatformThread.sync(wrapper.getSummary()));
if (val && summary.getValue() != null && wrapper.getEntry().getProvider().alwaysShowSummary()) {
information.textProperty().bind(PlatformThread.sync(summary));
} else {
information.textProperty().bind(PlatformThread.sync(wrapper.getInformation()));
information.textProperty().bind(PlatformThread.sync(info));
}
});
@ -56,7 +58,7 @@ public class DenseStoreEntryComp extends StoreEntryComp {
GridPane.setHalignment(storeIcon, HPos.CENTER);
}
var customSize = content != null ? 300 : 0;
var customSize = content != null ? 200 : 0;
var custom = new ColumnConstraints(0, customSize, customSize);
custom.setHalignment(HPos.RIGHT);

View file

@ -37,7 +37,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
info.setHalignment(HPos.LEFT);
grid.getColumnConstraints().add(info);
var customSize = content != null ? 300 : 0;
var customSize = content != null ? 200 : 0;
var custom = new ColumnConstraints(0, customSize, customSize);
custom.setHalignment(HPos.RIGHT);
var cr = content != null ? content.createRegion() : new Region();

View file

@ -31,7 +31,7 @@ public class StoreCreationMenu {
host.textProperty().bind(AppI18n.observable("addHost"));
host.setOnAction(event -> {
GuiDsStoreCreator.showCreation(DataStoreProviders.byName("ssh").orElseThrow(),
v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.HOST));
v -> DataStoreProvider.CreationCategory.HOST.equals(v.getCreationCategory()));
event.consume();
});
menu.getItems().add(host);
@ -42,7 +42,7 @@ public class StoreCreationMenu {
shell.textProperty().bind(AppI18n.observable("addShell"));
shell.setOnAction(event -> {
GuiDsStoreCreator.showCreation(null,
v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.SHELL));
v -> DataStoreProvider.CreationCategory.SHELL.equals(v.getCreationCategory()));
event.consume();
});
menu.getItems().add(shell);
@ -53,7 +53,7 @@ public class StoreCreationMenu {
cmd.textProperty().bind(AppI18n.observable("addCommand"));
cmd.setOnAction(event -> {
GuiDsStoreCreator.showCreation(null,
v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.COMMAND));
v -> DataStoreProvider.CreationCategory.COMMAND.equals(v.getCreationCategory()));
event.consume();
});
menu.getItems().add(cmd);
@ -64,7 +64,7 @@ public class StoreCreationMenu {
db.textProperty().bind(AppI18n.observable("addDatabase"));
db.setOnAction(event -> {
GuiDsStoreCreator.showCreation(null,
v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.DATABASE));
v -> DataStoreProvider.CreationCategory.DATABASE.equals(v.getCreationCategory()));
event.consume();
});
menu.getItems().add(db);
@ -75,11 +75,22 @@ public class StoreCreationMenu {
tunnel.textProperty().bind(AppI18n.observable("addTunnel"));
tunnel.setOnAction(event -> {
GuiDsStoreCreator.showCreation(null,
v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.TUNNEL));
v -> DataStoreProvider.CreationCategory.TUNNEL.equals(v.getCreationCategory()));
event.consume();
});
menu.getItems().add(tunnel);
}
{
var script = new MenuItem();
script.setGraphic(new FontIcon("mdi2s-script-text-outline"));
script.textProperty().bind(AppI18n.observable("addScript"));
script.setOnAction(event -> {
GuiDsStoreCreator.showCreation(null,
v -> DataStoreProvider.CreationCategory.SCRIPT.equals(v.getCreationCategory()));
event.consume();
});
menu.getItems().add(script);
}
}
}

View file

@ -17,7 +17,6 @@ import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.DesktopHelper;
import io.xpipe.app.util.DesktopShortcuts;
@ -70,7 +69,7 @@ public abstract class StoreEntryComp extends SimpleComp {
public static final ObservableDoubleValue INFO_NO_CONTENT_WIDTH =
App.getApp().getStage().widthProperty().divide(2.2);
public static final ObservableDoubleValue INFO_WITH_CONTENT_WIDTH =
App.getApp().getStage().widthProperty().divide(2.2).add(-300);
App.getApp().getStage().widthProperty().divide(2.2).add(-200);
protected final StoreEntryWrapper wrapper;
protected final Comp<?> content;
@ -91,12 +90,7 @@ public abstract class StoreEntryComp extends SimpleComp {
button.setMaxWidth(3000);
button.setFocusTraversable(true);
button.accessibleTextProperty()
.bind(Bindings.createStringBinding(
() -> {
return wrapper.getName();
},
wrapper.nameProperty()));
button.accessibleHelpProperty().bind(wrapper.getInformation());
.bind(wrapper.nameProperty());
button.setOnAction(event -> {
event.consume();
ThreadHelper.runFailableAsync(() -> {
@ -117,7 +111,8 @@ public abstract class StoreEntryComp extends SimpleComp {
protected Label createInformation() {
var information = new Label();
information.setGraphicTextGap(7);
information.textProperty().bind(PlatformThread.sync(wrapper.getInformation()));
information.textProperty().bind(wrapper.getEntry().getProvider() != null ?
PlatformThread.sync(wrapper.getEntry().getProvider().informationString(wrapper)) : new SimpleStringProperty());
information.getStyleClass().add("information");
AppFont.header(information);
@ -131,14 +126,14 @@ public abstract class StoreEntryComp extends SimpleComp {
protected Label createSummary() {
var summary = new Label();
summary.textProperty().bind(PlatformThread.sync(wrapper.getSummary()));
summary.textProperty().bind(wrapper.summary());
summary.getStyleClass().add("summary");
AppFont.small(summary);
return summary;
}
protected void applyState(Node node) {
SimpleChangeListener.apply(PlatformThread.sync(wrapper.getState()), val -> {
SimpleChangeListener.apply(PlatformThread.sync(wrapper.getValidity()), val -> {
switch (val) {
case LOAD_FAILED -> {
node.pseudoClassStateChanged(FAILED, true);
@ -157,24 +152,7 @@ public abstract class StoreEntryComp extends SimpleComp {
}
protected Comp<?> createName() {
// var filtered = BindingsHelper.filteredContentBinding(
// StoreViewState.get().getAllEntries(),
// other -> other.getEntry().getState().isUsable()
// && entry.getEntry()
// .getStore()
// .equals(other.getEntry()
// .getProvider()
// .getLogicalParent(other.getEntry().getStore())));
LabelComp name = new LabelComp(Bindings.createStringBinding(
() -> {
return wrapper.getName();
// + (filtered.size() > 0 && entry.getEntry().getStore() instanceof
// FixedHierarchyStore
// ? " (" + filtered.size() + ")"
// : "");
},
wrapper.nameProperty(),
wrapper.getInformation()));
LabelComp name = new LabelComp(wrapper.nameProperty());
name.apply(struc -> struc.get().setTextOverrun(OverrunStyle.CENTER_ELLIPSIS))
.apply(struc -> struc.get().setPadding(new Insets(5, 5, 5, 0)));
name.apply(s -> AppFont.header(s.get()));
@ -183,14 +161,14 @@ public abstract class StoreEntryComp extends SimpleComp {
}
protected Node createIcon(int w, int h) {
var img = wrapper.isDisabled()
var img = wrapper.disabledProperty().get()
? "disabled_icon.png"
: wrapper.getEntry()
.getProvider()
.getDisplayIconFileName(wrapper.getEntry().getStore());
var imageComp = PrettyImageHelper.ofFixed(img, w, h);
var storeIcon = imageComp.createRegion();
if (wrapper.getState().getValue().isUsable()) {
if (wrapper.getValidity().getValue().isUsable()) {
new FancyTooltipAugment<>(new SimpleStringProperty(
wrapper.getEntry().getProvider().getDisplayName()))
.augment(storeIcon);
@ -319,7 +297,7 @@ public abstract class StoreEntryComp extends SimpleComp {
sc.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
DesktopShortcuts.create(url,
wrapper.getName() + " (" + p.getKey().getDataStoreCallSite().getName(wrapper.getEntry().getStore().asNeeded()).getValue() + ")");
wrapper.nameProperty().getValue() + " (" + p.getKey().getDataStoreCallSite().getName(wrapper.getEntry().getStore().asNeeded()).getValue() + ")");
});
});
menu.getItems().add(sc);
@ -361,12 +339,6 @@ public abstract class StoreEntryComp extends SimpleComp {
});
contextMenu.getItems().add(move);
var refresh = new MenuItem(AppI18n.get("refresh"), new FontIcon("mdal-360"));
refresh.setOnAction(event -> {
DataStorage.get().refreshAsync(wrapper.getEntry(), true);
});
contextMenu.getItems().add(refresh);
var del = new MenuItem(AppI18n.get("remove"), new FontIcon("mdal-delete_outline"));
del.disableProperty().bind(wrapper.getDeletable().not());
del.setOnAction(event -> wrapper.delete());

View file

@ -9,9 +9,9 @@ import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.FixedHierarchyStore;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import lombok.Getter;
import java.time.Duration;
@ -29,16 +29,13 @@ public class StoreEntryWrapper {
private final BooleanProperty disabled = new SimpleBooleanProperty();
private final BooleanProperty inRefresh = new SimpleBooleanProperty();
private final BooleanProperty observing = new SimpleBooleanProperty();
private final Property<DataStoreEntry.State> state = new SimpleObjectProperty<>();
private final StringProperty information = new SimpleStringProperty();
private final StringProperty summary = new SimpleStringProperty();
private final Property<DataStoreEntry.Validity> validity = new SimpleObjectProperty<>();
private final Map<ActionProvider, BooleanProperty> actionProviders;
private final Property<ActionProvider.DefaultDataStoreCallSite<?>> defaultActionProvider;
private final BooleanProperty deletable = new SimpleBooleanProperty();
private final BooleanProperty expanded = new SimpleBooleanProperty();
private final Property<StoreCategoryWrapper> category = new SimpleObjectProperty<>();
private final Property<StoreEntryWrapper> displayParent = new SimpleObjectProperty<>();
private final IntegerProperty depth = new SimpleIntegerProperty();
private final Property<Object> persistentState = new SimpleObjectProperty<>();
public StoreEntryWrapper(DataStoreEntry entry) {
this.entry = entry;
@ -54,7 +51,8 @@ public class StoreEntryWrapper {
.getApplicableClass()
.isAssignableFrom(entry.getStore().getClass());
})
.sorted(Comparator.comparing(actionProvider -> actionProvider.getDataStoreCallSite().isSystemAction()))
.sorted(Comparator.comparing(
actionProvider -> actionProvider.getDataStoreCallSite().isSystemAction()))
.forEach(dataStoreActionProvider -> {
actionProviders.put(dataStoreActionProvider, new SimpleBooleanProperty(true));
});
@ -74,7 +72,7 @@ public class StoreEntryWrapper {
return null;
}
var p = DataStorage.get().getParent(entry, true).orElse(null);
var p = DataStorage.get().getDisplayParent(entry).orElse(null);
return StoreViewState.get().getAllEntries().stream()
.filter(storeEntryWrapper -> storeEntryWrapper.getEntry().equals(p))
.findFirst()
@ -96,6 +94,24 @@ public class StoreEntryWrapper {
});
}
public ObservableValue<String> summary() {
return PlatformThread.sync(Bindings.createStringBinding(
() -> {
if (!validity.getValue().isUsable()) {
return null;
}
try {
return entry.getProvider().summaryString(this);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
return null;
}
},
validity,
persistentState));
}
private void setupListeners() {
name.addListener((c, o, n) -> {
entry.setName(n);
@ -124,31 +140,15 @@ public class StoreEntryWrapper {
lastAccess.setValue(entry.getLastAccess());
disabled.setValue(entry.isDisabled());
state.setValue(entry.getState());
validity.setValue(entry.getValidity());
expanded.setValue(entry.isExpanded());
observing.setValue(entry.isObserving());
information.setValue(entry.getInformation());
displayParent.setValue(computeDisplayParent());
persistentState.setValue(entry.getStorePersistentState());
inRefresh.setValue(entry.isInRefresh());
if (entry.getState().isUsable()) {
try {
summary.setValue(entry.getProvider().toSummaryString(entry.getStore(), 50));
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
}
deletable.setValue(entry.getConfiguration().isDeletable()
|| AppPrefs.get().developerDisableGuiRestrictions().getValue());
var d = 0;
var c = this;
while ((c = c.getDisplayParent().getValue()) != null) {
d++;
}
depth.setValue(d);
actionProviders.keySet().forEach(dataStoreActionProvider -> {
if (!isInStorage()) {
actionProviders.get(dataStoreActionProvider).set(false);
@ -156,7 +156,7 @@ public class StoreEntryWrapper {
return;
}
if (!entry.getState().isUsable()
if (!entry.getValidity().isUsable()
&& !dataStoreActionProvider
.getDataStoreCallSite()
.activeType()
@ -194,51 +194,20 @@ public class StoreEntryWrapper {
});
}
public void refreshIfNeeded() throws Exception {
if (entry.getState().equals(DataStoreEntry.State.COMPLETE_BUT_INVALID)
|| entry.getState().equals(DataStoreEntry.State.COMPLETE_NOT_VALIDATED)) {
getEntry().refresh(true);
}
}
public void refreshAsync() {
ThreadHelper.runFailableAsync(() -> {
getEntry().refresh(true);
});
}
public void refreshWithChildren() throws Exception {
getEntry().refresh(true);
public void refreshChildren() {
var hasChildren = DataStorage.get().refreshChildren(entry);
PlatformThread.runLaterIfNeeded(() -> {
expanded.set(hasChildren);
});
}
public void refreshWithChildrenAsync() {
ThreadHelper.runFailableAsync(() -> {
refreshWithChildren();
});
}
public void mutateAsync(DataStore newValue) {
ThreadHelper.runAsync(() -> {
var hasChildren = DataStorage.get().setAndRefresh(getEntry(), newValue);
PlatformThread.runLaterIfNeeded(() -> {
expanded.set(hasChildren);
});
});
}
public void executeDefaultAction() throws Exception {
var found = getDefaultActionProvider().getValue();
entry.updateLastUsed();
if (found != null) {
refreshIfNeeded();
found.createAction(entry.getStore().asNeeded()).execute();
} else if (getEntry().getStore() instanceof FixedHierarchyStore) {
refreshWithChildrenAsync();
} else {
entry.setExpanded(!entry.isExpanded());
}
}
@ -247,36 +216,13 @@ public class StoreEntryWrapper {
}
public boolean shouldShow(String filter) {
return filter == null
|| getName().toLowerCase().contains(filter.toLowerCase())
|| (summary.get() != null && summary.get().toLowerCase().contains(filter.toLowerCase()))
|| (information.get() != null && information.get().toLowerCase().contains(filter.toLowerCase()));
}
public String getName() {
return name.getValue();
return filter == null || nameProperty().getValue().toLowerCase().contains(filter.toLowerCase());
}
public Property<String> nameProperty() {
return name;
}
public DataStoreEntry getEntry() {
return entry;
}
public Instant getLastAccess() {
return lastAccess.getValue();
}
public Property<Instant> lastAccessProperty() {
return lastAccess;
}
public boolean isDisabled() {
return disabled.get();
}
public BooleanProperty disabledProperty() {
return disabled;
}

View file

@ -7,7 +7,6 @@ import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.ScanAlert;
import io.xpipe.core.impl.LocalStore;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
@ -38,7 +37,7 @@ public class StoreIntroComp extends SimpleComp {
});
var scanButton = new Button(AppI18n.get("detectConnections"), new FontIcon("mdi2m-magnify"));
scanButton.setOnAction(event -> ScanAlert.showAsync(DataStorage.get().getStoreEntry(new LocalStore())));
scanButton.setOnAction(event -> ScanAlert.showAsync(DataStorage.get().local()));
var scanPane = new StackPane(scanButton);
scanPane.setAlignment(Pos.CENTER);

View file

@ -74,7 +74,7 @@ public class StoreSection {
private static ObservableList<StoreSection> sorted(
ObservableList<StoreSection> list, ObservableValue<StoreCategoryWrapper> category) {
var c = Comparator.<StoreSection>comparingInt(
value -> value.getWrapper().getEntry().getState().isUsable() ? 1 : -1);
value -> value.getWrapper().getEntry().getValidity().isUsable() ? 1 : -1);
category.getValue().getSortMode().addListener((observable, oldValue, newValue) -> {
int a = 0;
});
@ -109,12 +109,12 @@ public class StoreSection {
ordered,
section -> {
var noParent = DataStorage.get()
.getParent(section.getWrapper().getEntry(), true)
.getDisplayParent(section.getWrapper().getEntry())
.isEmpty();
var sameCategory =
category.getValue().contains(section.getWrapper().getEntry());
var diffParentCategory = DataStorage.get()
.getParent(section.getWrapper().getEntry(), true)
.getDisplayParent(section.getWrapper().getEntry())
.map(entry -> !category.getValue().contains(entry))
.orElse(false);
var showFilter = section.shouldShow(filterString.get());
@ -133,13 +133,13 @@ public class StoreSection {
Predicate<StoreEntryWrapper> entryFilter,
ObservableStringValue filterString,
ObservableValue<StoreCategoryWrapper> category) {
if (e.getEntry().getState() == DataStoreEntry.State.LOAD_FAILED) {
if (e.getEntry().getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {
return new StoreSection(e, FXCollections.observableArrayList(), FXCollections.observableArrayList(), depth);
}
var allChildren = BindingsHelper.filteredContentBinding(all, other -> {
return DataStorage.get()
.getParent(other.getEntry(), true)
.getDisplayParent(other.getEntry())
.map(found -> found.equals(e.getEntry()))
.orElse(false);
});

View file

@ -20,7 +20,7 @@ public interface StoreSortMode {
@Override
public Comparator<StoreSection> comparator() {
return Comparator.<StoreSection, String>comparing(
e -> e.getWrapper().getName().toLowerCase(Locale.ROOT));
e -> e.getWrapper().nameProperty().getValue().toLowerCase(Locale.ROOT));
}
};
@ -33,7 +33,7 @@ public interface StoreSortMode {
@Override
public Comparator<StoreSection> comparator() {
return Comparator.<StoreSection, String>comparing(
e -> e.getWrapper().getName().toLowerCase(Locale.ROOT))
e -> e.getWrapper().nameProperty().getValue().toLowerCase(Locale.ROOT))
.reversed();
}
};

View file

@ -89,6 +89,15 @@ public class StoreViewState {
.orElseThrow();
}
public StoreCategoryWrapper getScriptsCategory() {
return categories.stream()
.filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.SCRIPTS_CATEGORY_UUID))
.findFirst()
.orElseThrow();
}
public static void init() {
new StoreViewState();
}

View file

@ -51,7 +51,7 @@ public class DsStoreProviderChoiceComp extends Comp<CompStructure<ComboBox<Node>
var comboBox = new CustomComboBoxBuilder<>(provider, this::createGraphic, createDefaultNode(), v -> true);
comboBox.setAccessibleNames(dataStoreProvider -> dataStoreProvider.getDisplayName());
var l = getProviders().stream()
.filter(p -> AppPrefs.get().developerShowHiddenProviders().get() || p.canManuallyCreate() || staticDisplay).toList();
.filter(p -> AppPrefs.get().developerShowHiddenProviders().get() || p.getCreationCategory() != null || staticDisplay).toList();
l
.forEach(comboBox::add);
if (l.size() == 1) {

View file

@ -109,7 +109,7 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
newE -> {
ThreadHelper.runAsync(() -> {
if (!DataStorage.get().getStoreEntries().contains(e)) {
DataStorage.get().addStoreEntry(newE);
DataStorage.get().addStoreEntryIfNotPresent(newE);
} else {
DataStorage.get().updateEntry(e, newE);
}
@ -126,7 +126,7 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
filter,
e -> {
try {
DataStorage.get().addStoreEntry(e);
DataStorage.get().addStoreEntryIfNotPresent(e);
if (e.getProvider().shouldHaveChildren()) {
ScanAlert.showAsync(e);
}
@ -260,7 +260,7 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
// return;
// }
var d = n.guiDialog(store);
var d = n.guiDialog(entry.getValue(), store);
var propVal = new SimpleValidator();
var propR = createStoreProperties(d == null || d.getComp() == null ? null : d.getComp(), propVal);
@ -349,7 +349,7 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
ThreadHelper.runAsync(() -> {
try (var b = new BooleanScope(busy).start()) {
entry.getValue().refresh(true);
entry.getValue().validateOrThrow();
finished.setValue(true);
PlatformThread.runLaterIfNeeded(parent::next);
} catch (Exception ex) {

View file

@ -5,6 +5,7 @@ import io.xpipe.app.comp.AppLayoutComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.core.process.OsType;
import javafx.application.Application;
@ -29,6 +30,7 @@ public class App extends Application {
TrackEvent.info("Application launched");
APP = this;
stage = primaryStage;
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

View file

@ -55,7 +55,7 @@ public class AppCache {
return notPresent.get();
}
return (T) JacksonMapper.newMapper().treeToValue(tree, type);
return (T) JacksonMapper.getDefault().treeToValue(tree, type);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omit().handle();
FileUtils.deleteQuietly(path.toFile());
@ -69,7 +69,7 @@ public class AppCache {
try {
FileUtils.forceMkdirParent(path.toFile());
var tree = JacksonMapper.newMapper().valueToTree(val);
var tree = JacksonMapper.getDefault().valueToTree(val);
JsonConfigHelper.writeConfig(path, tree);
} catch (Exception e) {
ErrorEvent.fromThrowable("Could not parse cached data for key " + key, e)

View file

@ -42,12 +42,16 @@ public class AppExtensionManager {
INSTANCE.determineExtensionDirectories();
INSTANCE.loadBaseExtension();
INSTANCE.loadAllExtensions();
}
}
if (load) {
INSTANCE.addNativeLibrariesToPath();
try {
XPipeServiceProviders.load(INSTANCE.extendedLayer);
MessageExchangeImpls.loadAll();
} catch (Throwable t) {
throw new ExtensionException("Service provider initialization failed. Is the installation data corrupt?", t);
}
}
}

View file

@ -7,7 +7,6 @@ import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.prefs.SupportedLocale;
import io.xpipe.app.util.DynamicOptionsBuilder;
import io.xpipe.app.util.OptionsBuilder;
import io.xpipe.app.util.Translatable;
import io.xpipe.core.util.ModuleHelper;
@ -149,8 +148,7 @@ public class AppI18n {
|| caller.equals(FancyTooltipAugment.class)
|| caller.equals(PrefsChoiceValue.class)
|| caller.equals(Translatable.class)
|| caller.equals(OptionsBuilder.class)
|| caller.equals(DynamicOptionsBuilder.class)) {
|| caller.equals(OptionsBuilder.class)) {
continue;
}
var split = caller.getModule().getName().split("\\.");

View file

@ -117,7 +117,7 @@ public class AppSocketServer {
JsonNode node;
try (InputStream blockIn = BeaconFormat.readBlocks(clientSocket.getInputStream())) {
node = JacksonMapper.newMapper().readTree(blockIn);
node = JacksonMapper.getDefault().readTree(blockIn);
}
if (node.isMissingNode()) {
TrackEvent.trace("beacon", "Received EOF");
@ -183,14 +183,14 @@ public class AppSocketServer {
try {
JsonNode informationNode;
try (InputStream blockIn = BeaconFormat.readBlocks(clientSocket.getInputStream())) {
informationNode = JacksonMapper.newMapper().readTree(blockIn);
informationNode = JacksonMapper.getDefault().readTree(blockIn);
}
if (informationNode.isMissingNode()) {
TrackEvent.trace("beacon", "Received EOF");
return;
}
var information =
JacksonMapper.newMapper().treeToValue(informationNode, BeaconClient.ClientInformation.class);
JacksonMapper.getDefault().treeToValue(informationNode, BeaconClient.ClientInformation.class);
TrackEvent.builder()
.category("beacon")
@ -276,7 +276,7 @@ public class AppSocketServer {
}
public <T extends ResponseMessage> void sendResponse(Socket outSocket, T obj) throws Exception {
ObjectNode json = JacksonMapper.newMapper().valueToTree(obj);
ObjectNode json = JacksonMapper.getDefault().valueToTree(obj);
var prov = MessageExchanges.byResponse(obj).get();
json.set("messageType", new TextNode(prov.getId()));
json.set("messagePhase", new TextNode("response"));
@ -284,7 +284,7 @@ public class AppSocketServer {
msg.set("xPipeMessage", json);
var writer = new StringWriter();
var mapper = JacksonMapper.newMapper();
var mapper = JacksonMapper.getDefault();
try (JsonGenerator g = mapper.createGenerator(writer).setPrettyPrinter(new DefaultPrettyPrinter())) {
g.writeTree(msg);
} catch (IOException ex) {
@ -300,14 +300,14 @@ public class AppSocketServer {
public void sendClientErrorResponse(Socket outSocket, String message) throws Exception {
var err = new ClientErrorMessage(message);
ObjectNode json = JacksonMapper.newMapper().valueToTree(err);
ObjectNode json = JacksonMapper.getDefault().valueToTree(err);
var msg = JsonNodeFactory.instance.objectNode();
msg.set("xPipeClientError", json);
// Don't log this as it clutters the output
// TrackEvent.trace("beacon", "Sending raw client error:\n" + json.toPrettyString());
var mapper = JacksonMapper.newMapper();
var mapper = JacksonMapper.getDefault();
try (OutputStream blockOut = BeaconFormat.writeBlocks(outSocket.getOutputStream())) {
var gen = mapper.createGenerator(blockOut);
gen.writeTree(msg);
@ -316,14 +316,14 @@ public class AppSocketServer {
public void sendServerErrorResponse(Socket outSocket, Throwable ex) throws Exception {
var err = new ServerErrorMessage(UUID.randomUUID(), ex);
ObjectNode json = JacksonMapper.newMapper().valueToTree(err);
ObjectNode json = JacksonMapper.getDefault().valueToTree(err);
var msg = JsonNodeFactory.instance.objectNode();
msg.set("xPipeServerError", json);
// Don't log this as it clutters the output
// TrackEvent.trace("beacon", "Sending raw server error:\n" + json.toPrettyString());
var mapper = JacksonMapper.newMapper();
var mapper = JacksonMapper.getDefault();
try (OutputStream blockOut = BeaconFormat.writeBlocks(outSocket.getOutputStream())) {
var gen = mapper.createGenerator(blockOut);
gen.writeTree(msg);
@ -347,7 +347,7 @@ public class AppSocketServer {
throw new IllegalArgumentException("Unknown request id: " + type);
}
var reader = JacksonMapper.newMapper().readerFor(prov.get().getRequestClass());
var reader = JacksonMapper.getDefault().readerFor(prov.get().getRequestClass());
return reader.readValue(content);
}
}

View file

@ -14,7 +14,6 @@ import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
@ -84,12 +83,6 @@ public class AppTheme {
AppPrefs.get().theme.addListener((c, o, n) -> {
changeTheme(n);
});
Window.getWindows().addListener((ListChangeListener<? super Window>) c -> {
c.getList().forEach(window -> {
window.opacityProperty().bind(AppPrefs.get().windowOpacity());
});
});
}
private static void setDefault(boolean dark) {

View file

@ -9,7 +9,7 @@ import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.FeatureProvider;
import io.xpipe.app.util.FileBridge;
import io.xpipe.app.util.LockedSecretValue;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.store.LocalStore;
import io.xpipe.core.util.JacksonMapper;
public class BaseMode extends OperationMode {

View file

@ -6,7 +6,7 @@ import io.xpipe.app.issue.*;
import io.xpipe.app.launcher.LauncherCommand;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.app.util.XPipeSession;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.store.LocalStore;
import io.xpipe.core.util.FailableRunnable;
import io.xpipe.core.util.XPipeDaemonMode;
import io.xpipe.core.util.XPipeInstallation;

View file

@ -1,7 +1,5 @@
package io.xpipe.app.exchange;
import io.xpipe.app.ext.DataSourceProvider;
import io.xpipe.app.ext.DataSourceProviders;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.beacon.BeaconHandler;
@ -9,49 +7,11 @@ import io.xpipe.beacon.ClientException;
import io.xpipe.beacon.RequestMessage;
import io.xpipe.beacon.ResponseMessage;
import io.xpipe.beacon.exchange.MessageExchange;
import io.xpipe.core.dialog.Dialog;
import io.xpipe.core.impl.NamedStore;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.DataStoreId;
import lombok.NonNull;
import java.util.Optional;
public interface MessageExchangeImpl<RQ extends RequestMessage, RS extends ResponseMessage> extends MessageExchange {
@SuppressWarnings("unchecked")
default <T extends DataSource<?>> Dialog toCompleteConfig(
@NonNull DataSource<?> source, @NonNull DataSourceProvider<T> p, boolean all) throws ClientException {
var dialog = p.configDialog((T) source, all);
if (dialog == null) {
throw new ClientException(
String.format("Data source of type %s does not support editing from the CLI", p.getId()));
}
return dialog;
}
default DataSourceProvider<?> getProvider(@NonNull String id) throws ClientException {
Optional<DataSourceProvider<?>> prov = DataSourceProviders.byName(id);
if (prov.isEmpty()) {
throw new ClientException("No matching data source type found for type " + id);
}
return prov.get();
}
default DataStore resolveStore(@NonNull DataStore in, boolean acceptDisabled) throws ClientException {
try {
if (in instanceof NamedStore n) {
var found = DataStorage.get().getStoreEntry(n.getName(), acceptDisabled);
return found.getStore();
}
} catch (IllegalArgumentException ex) {
throw new ClientException(ex.getMessage());
}
return in;
}
default DataStoreEntry getStoreEntryByName(@NonNull String name, boolean acceptDisabled) throws ClientException {
var store = DataStorage.get().getStoreEntryIfPresent(name);
if (store.isEmpty()) {
@ -73,7 +33,7 @@ public interface MessageExchangeImpl<RQ extends RequestMessage, RS extends Respo
throw new ClientException(
String.format("Store %s is disabled", store.get().getName()));
}
if (!store.get().getState().isUsable() && !acceptUnusable) {
if (!store.get().getValidity().isUsable() && !acceptUnusable) {
throw new ClientException(
String.format("Store %s is not completely configured", store.get().getName()));
}

View file

@ -1,16 +0,0 @@
package io.xpipe.app.exchange;
import io.xpipe.beacon.BeaconHandler;
import io.xpipe.beacon.exchange.ProxyFunctionExchange;
public class ProxyFunctionExchangeImpl extends ProxyFunctionExchange
implements MessageExchangeImpl<ProxyFunctionExchange.Request, ProxyFunctionExchange.Response> {
@Override
public Response handleRequest(BeaconHandler handler, Request msg) {
msg.getFunction().callLocal();
return ProxyFunctionExchange.Response.builder()
.function(msg.getFunction())
.build();
}
}

View file

@ -1,27 +0,0 @@
package io.xpipe.app.exchange;
import io.xpipe.beacon.BeaconHandler;
import io.xpipe.beacon.exchange.ProxyReadConnectionExchange;
import io.xpipe.core.impl.OutputStreamStore;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.source.DataSourceReadConnection;
import io.xpipe.core.source.WriteMode;
public class ProxyReadConnectionExchangeImpl extends ProxyReadConnectionExchange
implements MessageExchangeImpl<ProxyReadConnectionExchange.Request, ProxyReadConnectionExchange.Response> {
@Override
public Response handleRequest(BeaconHandler handler, Request msg) {
handler.postResponse(() -> {
var outputSource = DataSource.createInternalDataSource(
msg.getSource().getType(), new OutputStreamStore(handler.sendBody()));
try (DataSourceReadConnection r = msg.getSource().openReadConnection();
var w = outputSource.openWriteConnection(WriteMode.REPLACE)) {
r.init();
w.init();
r.forward(w);
}
});
return ProxyReadConnectionExchange.Response.builder().build();
}
}

View file

@ -1,25 +0,0 @@
package io.xpipe.app.exchange;
import io.xpipe.beacon.BeaconHandler;
import io.xpipe.beacon.exchange.ProxyWriteConnectionExchange;
import io.xpipe.core.impl.InputStreamStore;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.source.DataSourceReadConnection;
public class ProxyWriteConnectionExchangeImpl extends ProxyWriteConnectionExchange
implements MessageExchangeImpl<ProxyWriteConnectionExchange.Request, ProxyWriteConnectionExchange.Response> {
@Override
public Response handleRequest(BeaconHandler handler, Request msg) throws Exception {
var outputSource = msg.getSource();
var inputSource = DataSource.createInternalDataSource(
outputSource.getType(), new InputStreamStore(handler.receiveBody()));
try (DataSourceReadConnection r = inputSource.openReadConnection();
var w = outputSource.openWriteConnection(msg.getMode())) {
r.init();
w.init();
r.forward(w);
}
return Response.builder().build();
}
}

View file

@ -10,12 +10,10 @@ public class QueryStoreExchangeImpl extends QueryStoreExchange
@Override
public Response handleRequest(BeaconHandler handler, Request msg) throws Exception {
var store = getStoreEntryByName(msg.getName(), true);
var information = store.getInformation();
var summary = store.getProvider().toSummaryString(store.getStore(), 100);
var summary = "";
var dialog = store.getProvider().dialogForStore(store.getStore().asNeeded());
var config = new DialogMapper(dialog).handle();
return Response.builder()
.information(information)
.summary(summary)
.internalStore(store.getStore())
.provider(store.getProvider().getId())

View file

@ -1,49 +0,0 @@
package io.xpipe.app.exchange;
import io.xpipe.app.ext.DataSourceProvider;
import io.xpipe.app.ext.DataSourceProviders;
import io.xpipe.beacon.BeaconHandler;
import io.xpipe.beacon.ClientException;
import io.xpipe.beacon.exchange.ReadExchange;
import io.xpipe.core.dialog.Dialog;
import io.xpipe.core.dialog.QueryConverter;
public class ReadExchangeImpl extends ReadExchange
implements MessageExchangeImpl<ReadExchange.Request, ReadExchange.Response> {
@Override
public Response handleRequest(BeaconHandler handler, Request msg) throws Exception {
var store = resolveStore(msg.getStore(), false);
DataSourceProvider<?> provider;
if (msg.getProvider() == null) {
provider = DataSourceProviders.byPreferredStore(store, null).orElse(null);
} else {
provider = getProvider(msg.getProvider());
}
var typeQ = Dialog.skipIf(
Dialog.retryIf(
Dialog.query(
"Data source type could not be determined.\nSpecify type explicitly",
true,
true,
false,
null,
QueryConverter.STRING),
(String type) -> {
return DataSourceProviders.byName(type).isEmpty() ? "Unknown type: " + type : null;
}),
() -> provider != null);
var config = Dialog.lazy(() -> {
if (!provider.couldSupportStore(store)) {
throw new ClientException("Type " + provider.getId() + " does not support store");
}
var defaultDesc = provider.createDefaultSource(store);
return toCompleteConfig(defaultDesc, provider, msg.isConfigureAll());
});
return Response.builder().build();
}
}

View file

@ -4,7 +4,7 @@ import io.xpipe.app.exchange.MessageExchangeImpl;
import io.xpipe.app.update.XPipeInstanceHelper;
import io.xpipe.beacon.BeaconHandler;
import io.xpipe.beacon.exchange.cli.InstanceExchange;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.store.LocalStore;
public class InstanceExchangeImpl extends InstanceExchange
implements MessageExchangeImpl<InstanceExchange.Request, InstanceExchange.Response> {

View file

@ -19,7 +19,6 @@ public class ListStoresExchangeImpl extends ListStoresExchange
.map(col -> StoreListEntry.builder()
.id(DataStorage.get().getId(col))
.type(col.getProvider().getId())
.information(col.getInformation())
.build())
.sorted(Comparator.comparing(en -> en.getId().toString()))
.toList();

View file

@ -11,7 +11,7 @@ public class RenameStoreExchangeImpl extends RenameStoreExchange
@Override
public Response handleRequest(BeaconHandler handler, Request msg) {
var s = DataStorage.get().getStoreEntry(msg.getStoreName(), true);
DataStorage.get().renameStoreEntry(s, msg.getNewName());
s.setName(msg.getNewName());
return Response.builder().build();
}
}

View file

@ -4,7 +4,6 @@ import io.xpipe.app.exchange.DialogExchangeImpl;
import io.xpipe.app.exchange.MessageExchangeImpl;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.issue.ExceptionConverter;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.beacon.BeaconHandler;
import io.xpipe.beacon.ClientException;
@ -51,7 +50,7 @@ public class StoreAddExchangeImpl extends StoreAddExchange
return;
}
DataStorage.get().addStoreEntry(name.getValue(), store);
DataStorage.get().addStoreIfNotPresent(name.getValue(), store);
});
return StoreAddExchange.Response.builder().config(config).build();
@ -64,12 +63,6 @@ public class StoreAddExchangeImpl extends StoreAddExchange
return "Store is null";
}
try {
store.validate();
} catch (Exception ex) {
return ExceptionConverter.convertMessage(ex);
}
return null;
})
.map((String msg) -> {
@ -95,7 +88,6 @@ public class StoreAddExchangeImpl extends StoreAddExchange
DataStore s = creator.getResult();
String d = "";
try {
d = provider.queryInformationString(s, 50);
} catch (Exception ignored) {
}
if (d != null) {

View file

@ -15,23 +15,23 @@ public class StoreProviderListExchangeImpl extends StoreProviderListExchange
@Override
public Response handleRequest(BeaconHandler handler, Request msg) {
var categories = DataStoreProvider.DisplayCategory.values();
var categories = DataStoreProvider.CreationCategory.values();
var all = DataStoreProviders.getAll();
var map = Arrays.stream(categories)
.collect(Collectors.toMap(category -> getName(category), category -> all.stream()
.filter(dataStoreProvider ->
dataStoreProvider.getDisplayCategory().equals(category))
category.equals(dataStoreProvider.getCreationCategory()))
.map(p -> ProviderEntry.builder()
.id(p.getId())
.description(p.getDisplayDescription())
.hidden(!p.canManuallyCreate())
.hidden(p.getCreationCategory() == null)
.build())
.toList()));
return Response.builder().entries(map).build();
}
private String getName(DataStoreProvider.DisplayCategory category) {
private String getName(DataStoreProvider.CreationCategory category) {
return category.name().substring(0, 1).toUpperCase()
+ category.name().substring(1).toLowerCase();
}

View file

@ -1,7 +1,6 @@
package io.xpipe.app.ext;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.ModuleLayerLoader;
import javafx.beans.value.ObservableValue;
@ -91,10 +90,6 @@ public interface ActionProvider {
return null;
}
default DataSourceCallSite<?> getDataSourceCallSite() {
return null;
}
interface DefaultDataStoreCallSite<T extends DataStore> {
Action createAction(T store);
@ -142,27 +137,4 @@ public interface ActionProvider {
return ActiveType.ONLY_SHOW_IF_ENABLED;
}
}
interface DataSourceCallSite<T extends DataSource<?>> {
Action createAction(T source);
Class<T> getApplicableClass();
default boolean isMajor() {
return false;
}
default boolean isApplicable(T o) {
return true;
}
ObservableValue<String> getName(T source);
String getIcon(T source);
default boolean showIfDisabled() {
return true;
}
}
}

View file

@ -1,139 +0,0 @@
package io.xpipe.app.ext;
import io.xpipe.app.core.AppI18n;
import io.xpipe.core.dialog.Dialog;
import io.xpipe.core.source.CollectionDataSource;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.source.DataSourceType;
import io.xpipe.core.store.DataStore;
import javafx.beans.property.Property;
import javafx.scene.layout.Region;
import java.util.List;
import java.util.Map;
public interface DataSourceProvider<T extends DataSource<?>> {
default CollectionDataSource<?> createContainer(T source) {
return null;
}
default void validate() {
getCategory();
getSourceClass();
}
default Category getCategory() {
if (getFileProvider() != null) {
return Category.STREAM;
}
throw new ExtensionException("Provider has no set general type");
}
default boolean supportsConversion(T in, DataSourceType t) {
return false;
}
default DataSource<?> convert(T in, DataSourceType t) throws Exception {
throw new ExtensionException();
}
default void init() throws Exception {}
default String i18n(String key) {
return AppI18n.get(i18nKey(key));
}
default String i18nKey(String key) {
return getId() + "." + key;
}
default Region configGui(Property<T> source, boolean preferQuiet) throws Exception {
return null;
}
default String getDisplayName() {
return i18n("displayName");
}
default String getDisplayDescription() {
return i18n("displayDescription");
}
default String getModuleName() {
var n = getClass().getModule().getName();
var i = n.lastIndexOf('.');
return i != -1 ? n.substring(i + 1) : n;
}
default String queryInformationString(DataStore store, int length) {
return getDisplayName();
}
default String getDisplayIconFileName() {
return getModuleName() + ":" + getId() + "_icon.png";
}
Dialog configDialog(T source, boolean all);
default boolean shouldShow(DataSourceType type) {
return type == null || type == getPrimaryType();
}
DataSourceType getPrimaryType();
/**
* Checks whether this provider prefers a certain kind of store.
* This is important for the correct autodetection of a store.
*/
boolean prefersStore(DataStore store, DataSourceType type);
/**
* Checks whether this provider supports the store in principle.
* This method should not perform any further checks,
* just check whether it may be possible that the store is supported.
* <p>
* This method will be called for validation purposes.
*/
boolean couldSupportStore(DataStore store);
/**
* Performs a deep inspection to check whether this provider supports a given store.
* <p>
* This functionality will be used in case no preferred provider has been found.
*/
default boolean supportsStore(DataStore store) {
return false;
}
default FileProvider getFileProvider() {
return null;
}
default String getId() {
return getPossibleNames().get(0);
}
/**
* Attempt to create a useful data source descriptor from a data store.
* The result does not need to be always right, it should only reflect the best effort.
*/
T createDefaultSource(DataStore input) throws Exception;
Class<T> getSourceClass();
List<String> getPossibleNames();
enum Category {
STREAM,
DATABASE
}
interface FileProvider {
String getFileName();
Map<String, List<String>> getFileExtensions();
}
}

View file

@ -1,175 +0,0 @@
package io.xpipe.app.ext;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.impl.FileStore;
import io.xpipe.core.source.*;
import io.xpipe.core.store.DataStore;
import lombok.SneakyThrows;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.stream.Collectors;
public class DataSourceProviders {
private static List<DataSourceProvider<?>> ALL;
public static void init(ModuleLayer layer) {
if (ALL == null) {
ALL = ServiceLoader.load(layer, DataSourceProvider.class).stream()
.map(p -> (DataSourceProvider<?>) p.get())
.sorted(Comparator.comparing(DataSourceProvider::getId))
.collect(Collectors.toList());
ALL.removeIf(p -> {
try {
p.init();
p.validate();
return false;
} catch (Throwable e) {
ErrorEvent.fromThrowable(e).handle();
return true;
}
});
}
}
public static DataSourceProvider<?> getInternalProviderForType(DataSourceType t) {
try {
return switch (t) {
case TABLE -> DataSourceProviders.byId("xpbt");
case STRUCTURE -> DataSourceProviders.byId("xpbs");
case TEXT -> DataSourceProviders.byId("text");
case RAW -> DataSourceProviders.byId("binary");
// TODO
case COLLECTION -> null;
};
} catch (Exception ex) {
throw new AssertionError(ex);
}
}
@SuppressWarnings("unchecked")
@SneakyThrows
public static StructureDataSource<FileStore> createLocalStructureDescriptor(DataStore store) {
return (StructureDataSource<FileStore>) DataSourceProviders.byId("xpbs")
.getSourceClass()
.getDeclaredConstructors()[0]
.newInstance(store);
}
@SuppressWarnings("unchecked")
@SneakyThrows
public static RawDataSource<FileStore> createLocalRawDescriptor(DataStore store) {
return (RawDataSource<FileStore>) DataSourceProviders.byId("binary")
.getSourceClass()
.getDeclaredConstructors()[0]
.newInstance(store);
}
@SuppressWarnings("unchecked")
@SneakyThrows
public static RawDataSource<FileStore> createLocalCollectionDescriptor(DataStore store) {
return (RawDataSource<FileStore>) DataSourceProviders.byId("br")
.getSourceClass()
.getDeclaredConstructors()[0]
.newInstance(store);
}
@SuppressWarnings("unchecked")
@SneakyThrows
public static TextDataSource<FileStore> createLocalTextDescriptor(DataStore store) {
return (TextDataSource<FileStore>) DataSourceProviders.byId("text")
.getSourceClass()
.getDeclaredConstructors()[0]
.newInstance(store);
}
@SuppressWarnings("unchecked")
@SneakyThrows
public static TableDataSource<FileStore> createLocalTableDescriptor(DataStore store) {
return (TableDataSource<FileStore>) DataSourceProviders.byId("xpbt")
.getSourceClass()
.getDeclaredConstructors()[0]
.newInstance(store);
}
@SuppressWarnings("unchecked")
public static <T extends DataSourceProvider<?>> T byId(String name) {
if (ALL == null) {
throw new IllegalStateException("Not initialized");
}
return (T) ALL.stream()
.filter(d -> d.getId().equals(name))
.findAny()
.orElseThrow(() -> new IllegalArgumentException("Provider " + name + " not found"));
}
@SuppressWarnings("unchecked")
public static <C extends DataSource<?>, T extends DataSourceProvider<C>> T byDataSourceClass(Class<C> c) {
if (ALL == null) {
throw new IllegalStateException("Not initialized");
}
return (T) ALL.stream()
.filter(d -> d.getSourceClass().equals(c))
.findAny()
.orElseThrow(() -> new IllegalArgumentException("Provider for " + c.getSimpleName() + " not found"));
}
public static Optional<DataSourceProvider<?>> byName(String name) {
if (ALL == null) {
throw new IllegalStateException("Not initialized");
}
return ALL.stream()
.filter(d -> d.getPossibleNames().stream()
.anyMatch(s -> nameAlternatives(s).stream().anyMatch(s1 -> s1.equalsIgnoreCase(name)))
|| d.getId().equalsIgnoreCase(name))
.findAny();
}
private static List<String> nameAlternatives(String name) {
var split = List.of(name.split("_"));
return List.of(
String.join(" ", split),
String.join("_", split),
String.join("-", split),
split.stream()
.map(s -> s.equals(split.get(0)) ? s : s.substring(0, 1).toUpperCase() + s.substring(1))
.collect(Collectors.joining()));
}
public static DataSource<?> createDefault(DataStore store) {
if (ALL == null) {
throw new IllegalStateException("Not initialized");
}
var preferred = DataSourceProviders.byPreferredStore(store, null);
try {
return preferred.isPresent()
? preferred.get().createDefaultSource(store).asNeeded()
: null;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static Optional<DataSourceProvider<?>> byPreferredStore(DataStore store, DataSourceType type) {
if (ALL == null) {
throw new IllegalStateException("Not initialized");
}
return ALL.stream()
.filter(d -> type == null || d.getPrimaryType() == type)
.filter(d -> d.getFileProvider() != null)
.filter(d -> d.prefersStore(store, type))
.findAny();
}
public static List<DataSourceProvider<?>> getAll() {
return ALL;
}
}

View file

@ -1,100 +0,0 @@
package io.xpipe.app.ext;
import io.xpipe.app.util.Validator;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.util.ModuleLayerLoader;
import javafx.beans.value.ObservableValue;
import javafx.scene.layout.Region;
import lombok.AllArgsConstructor;
import lombok.Value;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.ServiceLoader;
public interface DataSourceTarget {
List<DataSourceTarget> ALL = new ArrayList<>();
class Loader implements ModuleLayerLoader {
@Override
public void init(ModuleLayer layer) {
ALL.clear();
ALL.addAll(ServiceLoader.load(layer, DataSourceTarget.class).stream()
.map(ServiceLoader.Provider::get)
.toList());
}
@Override
public boolean requiresFullDaemon() {
return true;
}
@Override
public boolean prioritizeLoading() {
return false;
}
}
static Optional<DataSourceTarget> byId(String id) {
return ALL.stream().filter(d -> d.getId().equals(id)).findAny();
}
static List<DataSourceTarget> getAll() {
return ALL;
}
default InstructionsDisplay createRetrievalInstructions(DataSource<?> source, ObservableValue<DataStoreId> id) {
return null;
}
default InstructionsDisplay createUpdateInstructions(DataSource<?> source, ObservableValue<DataStoreId> id) {
return null;
}
String getId();
ObservableValue<String> getName();
Category getCategory();
AccessType getAccessType();
String getSetupGuideURL();
default String getGraphicIcon() {
return null;
}
default boolean isApplicable(DataSource<?> source) {
return true;
}
enum Category {
PROGRAMMING_LANGUAGE,
APPLICATION,
OTHER
}
enum AccessType {
ACTIVE,
PASSIVE
}
@Value
@AllArgsConstructor
class InstructionsDisplay {
Region region;
Runnable onFinish;
Validator validator;
public InstructionsDisplay(Region region) {
this.region = region;
onFinish = null;
validator = null;
}
}
}

View file

@ -1,18 +1,21 @@
package io.xpipe.app.ext;
import io.xpipe.app.comp.base.MarkdownComp;
import io.xpipe.app.comp.base.SystemStateComp;
import io.xpipe.app.comp.storage.store.*;
import io.xpipe.app.comp.storage.store.StoreEntryComp;
import io.xpipe.app.comp.storage.store.StoreEntryWrapper;
import io.xpipe.app.comp.storage.store.StoreSection;
import io.xpipe.app.comp.storage.store.StoreSectionComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppImages;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.dialog.Dialog;
import io.xpipe.core.store.*;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.JacksonizedValue;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import java.util.List;
@ -36,8 +39,6 @@ public interface DataStoreProvider {
}
}
default void preAdd(DataStore store) {}
default String browserDisplayName(DataStore store) {
var e = DataStorage.get().getStoreDisplayName(store);
return e.orElse("?");
@ -64,16 +65,7 @@ public interface DataStoreProvider {
}
default Comp<?> stateDisplay(StoreEntryWrapper w) {
var state = Bindings.createObjectBinding(
() -> {
return w.getState().getValue() == DataStoreEntry.State.COMPLETE_BUT_INVALID
? SystemStateComp.State.FAILURE
: w.getState().getValue() == DataStoreEntry.State.COMPLETE_AND_VALID
? SystemStateComp.State.SUCCESS
: SystemStateComp.State.OTHER;
},
w.getState());
return new SystemStateComp(state);
return Comp.empty();
}
default Comp<?> createInsightsComp(ObservableValue<DataStore> store) {
@ -97,19 +89,15 @@ public interface DataStoreProvider {
return null;
}
default DisplayCategory getDisplayCategory() {
return DisplayCategory.OTHER;
}
default DataStore getLogicalParent(DataStore store) {
default CreationCategory getCreationCategory() {
return null;
}
default DataStore getDisplayParent(DataStore store) {
return getLogicalParent(store);
default DataStoreEntry getDisplayParent(DataStoreEntry store) {
return null;
}
default GuiDialog guiDialog(Property<DataStore> store) {
default GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {
return null;
}
@ -126,16 +114,12 @@ public interface DataStoreProvider {
return false;
}
default String queryInformationString(DataStore store, int length) throws Exception {
default String summaryString(StoreEntryWrapper wrapper) {
return null;
}
default String queryInvalidInformationString(DataStore store, int length) {
return "Connection failed";
}
default String toSummaryString(DataStore store, int length) {
return null;
default ObservableValue<String> informationString(StoreEntryWrapper wrapper) {
return new SimpleStringProperty(null);
}
default String i18n(String key) {
@ -173,10 +157,6 @@ public interface DataStoreProvider {
return null;
}
default boolean requiresFrequentRefresh() {
return getStoreClasses().stream().anyMatch(aClass -> FixedHierarchyStore.class.isAssignableFrom(aClass));
}
default DataStore defaultStore() {
return null;
}
@ -189,22 +169,12 @@ public interface DataStoreProvider {
List<Class<?>> getStoreClasses();
default boolean canManuallyCreate() {
return true;
}
enum DataCategory {
STREAM,
SHELL,
DATABASE
}
enum DisplayCategory {
enum CreationCategory {
HOST,
DATABASE,
SHELL,
COMMAND,
TUNNEL,
OTHER
SCRIPT
}
}

View file

@ -19,14 +19,6 @@ public class XPipeServiceProviders {
ProcessControlProvider.init(layer);
TrackEvent.info("Loading extension providers ...");
DataSourceProviders.init(layer);
for (DataSourceProvider<?> p : DataSourceProviders.getAll()) {
TrackEvent.trace("Loaded data source provider " + p.getId());
JacksonMapper.configure(objectMapper -> {
objectMapper.registerSubtypes(new NamedType(p.getSourceClass()));
});
}
DataStoreProviders.init(layer);
for (DataStoreProvider p : DataStoreProviders.getAll()) {
TrackEvent.trace("Loaded data store provider " + p.getId());

View file

@ -44,5 +44,10 @@ public class ContextMenuAugment<S extends CompStructure<?>> implements Augment<S
event.consume();
}
});
r.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
if (show.test(event)) {
event.consume();
}
});
}
}

View file

@ -10,6 +10,8 @@ import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.DataStoreCategoryChoiceComp;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.ShellStore;
@ -37,47 +39,51 @@ import java.util.function.Predicate;
public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
public static <T extends DataStore> DataStoreChoiceComp<T> other(
Property<T> selected, Class<T> clazz, Predicate<T> filter) {
Property<DataStoreEntryRef<T>> selected, Class<T> clazz, Predicate<DataStoreEntryRef<T>> filter) {
return new DataStoreChoiceComp<>(Mode.OTHER, null, selected, clazz, filter);
}
public static DataStoreChoiceComp<ShellStore> proxy(Property<ShellStore> selected) {
return new DataStoreChoiceComp<>(Mode.PROXY, null, selected, ShellStore.class, shellStore -> true);
public static DataStoreChoiceComp<ShellStore> proxy(Property<DataStoreEntryRef<ShellStore>> selected) {
return new DataStoreChoiceComp<>(Mode.PROXY, null, selected, ShellStore.class, null);
}
public static DataStoreChoiceComp<ShellStore> host(Property<ShellStore> selected) {
return new DataStoreChoiceComp<>(Mode.HOST, null, selected, ShellStore.class, shellStore -> true);
public static DataStoreChoiceComp<ShellStore> host(Property<DataStoreEntryRef<ShellStore>> selected) {
return new DataStoreChoiceComp<>(Mode.HOST, null, selected, ShellStore.class, null);
}
public static DataStoreChoiceComp<ShellStore> environment(ShellStore self, Property<ShellStore> selected) {
return new DataStoreChoiceComp<>(Mode.ENVIRONMENT, self, selected, ShellStore.class, shellStore -> true);
public static DataStoreChoiceComp<ShellStore> environment(
DataStoreEntry self, Property<DataStoreEntryRef<ShellStore>> selected) {
return new DataStoreChoiceComp<>(Mode.HOST, self, selected, ShellStore.class, shellStoreDataStoreEntryRef -> shellStoreDataStoreEntryRef.get().getProvider().canHaveSubShells());
}
public static DataStoreChoiceComp<ShellStore> proxy(ShellStore self, Property<ShellStore> selected) {
return new DataStoreChoiceComp<>(Mode.PROXY, self, selected, ShellStore.class, shellStore -> true);
public static DataStoreChoiceComp<ShellStore> proxy(
DataStoreEntry self, Property<DataStoreEntryRef<ShellStore>> selected) {
return new DataStoreChoiceComp<>(Mode.PROXY, self, selected, ShellStore.class, null);
}
public static DataStoreChoiceComp<ShellStore> host(ShellStore self, Property<ShellStore> selected) {
return new DataStoreChoiceComp<>(Mode.HOST, self, selected, ShellStore.class, shellStore -> true);
public static DataStoreChoiceComp<ShellStore> host(
DataStoreEntry self, Property<DataStoreEntryRef<ShellStore>> selected) {
return new DataStoreChoiceComp<>(Mode.HOST, self, selected, ShellStore.class, null);
}
public enum Mode {
HOST,
ENVIRONMENT,
OTHER,
PROXY
}
private final Mode mode;
private final T self;
private final Property<T> selected;
private final DataStoreEntry self;
private final Property<DataStoreEntryRef<T>> selected;
private final Class<T> storeClass;
private final Predicate<T> applicableCheck;
private final Predicate<DataStoreEntryRef<T>> applicableCheck;
private Popover popover;
private Popover getPopover() {
if (popover == null) {
// Rebuild popover if we have a non-null condition to allow for the content to be updated in case the condition
// changed
if (popover == null || applicableCheck != null) {
var selectedCategory = new SimpleObjectProperty<>(
StoreViewState.get().getActiveCategory().getValue());
var filterText = new SimpleStringProperty();
@ -85,14 +91,7 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
var e = storeEntryWrapper.getEntry();
if (e.getStore() == self) {
return false;
}
var store = e.getStore();
if (!(mode == Mode.ENVIRONMENT)
&& e.getProvider() != null
&& !e.getProvider().canHaveSubShells()) {
if (e.equals(self)) {
return false;
}
@ -100,15 +99,18 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
if (e.getStore() == null) {
return false;
}
return storeClass.isAssignableFrom(e.getStore().getClass()) && e.getState().isUsable() && applicableCheck.test(e.getStore().asNeeded());
return storeClass.isAssignableFrom(e.getStore().getClass())
&& e.getValidity().isUsable()
&& (applicableCheck == null
|| applicableCheck.test(e.ref()));
};
var section = StoreSectionMiniComp.createList(
StoreSection.createTopLevel(
StoreViewState.get().getAllEntries(), applicable, filterText, selectedCategory),
(s, comp) -> {
comp.apply(struc -> struc.get().setOnAction(event -> {
selected.setValue(
s.getWrapper().getEntry().getStore().asNeeded());
selected.setValue(s.getWrapper().getEntry().ref());
popover.hide();
event.consume();
}));
@ -137,7 +139,10 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
MenuButton m = new MenuButton(null, new FontIcon("mdi2p-plus-box-outline"));
StoreCreationMenu.addButtons(m);
return m;
}).padding(new Insets(-2)).styleClass(Styles.RIGHT_PILL).grow(false, true);
})
.padding(new Insets(-2))
.styleClass(Styles.RIGHT_PILL)
.grow(false, true);
var top = new HorizontalComp(List.of(category, filter.hgrow(), addButton))
.styleClass("top")
@ -182,16 +187,18 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
return new Label(name, imgView);
}
private String toName(DataStore store) {
if (store == null) {
private String toName(DataStoreEntry entry) {
if (entry == null) {
return null;
}
if (mode == Mode.PROXY && store instanceof ShellStore && ShellStore.isLocal(store.asNeeded())) {
if (mode == Mode.PROXY
&& entry.getStore() instanceof ShellStore
&& ShellStore.isLocal(entry.getStore().asNeeded())) {
return AppI18n.get("none");
}
return DataStorage.get().getStoreDisplayName(store).orElse("?");
return entry.getName();
}
@Override
@ -199,7 +206,7 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
var button = new ButtonComp(
Bindings.createStringBinding(
() -> {
return toName(selected.getValue());
return selected.getValue() != null ? toName(selected.getValue().getEntry()) : null;
},
selected),
() -> {});
@ -207,17 +214,18 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
struc.get().setMaxWidth(2000);
struc.get().setAlignment(Pos.CENTER_LEFT);
struc.get()
.setGraphic(PrettyImageHelper.ofSvg(Bindings.createStringBinding(
.setGraphic(PrettyImageHelper.ofSvg(
Bindings.createStringBinding(
() -> {
if (selected.getValue() == null) {
return null;
}
return DataStorage.get()
.getStoreEntryIfPresent(selected.getValue())
.map(entry -> entry.getProvider()
.getDisplayIconFileName(selected.getValue()))
.orElse(null);
return selected.getValue()
.get()
.getProvider()
.getDisplayIconFileName(selected.getValue()
.getStore());
},
selected),
16,

View file

@ -0,0 +1,59 @@
package io.xpipe.app.fxcomps.impl;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.DataStore;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.util.List;
import java.util.function.Predicate;
public class DataStoreListChoiceComp<T extends DataStore> extends SimpleComp {
private final ListProperty<DataStoreEntryRef<T>> selectedList;
private final Class<T> storeClass;
private final Predicate<DataStoreEntryRef<T>> applicableCheck;
public DataStoreListChoiceComp(ListProperty<DataStoreEntryRef<T>> selectedList, Class<T> storeClass, Predicate<DataStoreEntryRef<T>> applicableCheck) {
this.selectedList = selectedList;
this.storeClass = storeClass;
this.applicableCheck = applicableCheck;
}
@Override
protected Region createSimple() {
var list = new ListBoxViewComp<>(selectedList, selectedList, t -> {
if (t == null) {
return null;
}
var label = new LabelComp(t.get().getName()).apply(struc -> struc.get().setGraphic(PrettyImageHelper.ofFixedSmallSquare(
t.get().getProvider().getDisplayIconFileName(t.getStore())).createRegion()));
var delete = new IconButtonComp("mdal-delete_outline", () -> {
selectedList.remove(t);
});
var hbox = new HorizontalComp(List.of(label, Comp.hspacer(), delete)).styleClass("entry");
return hbox;
}).padding(new Insets(0)).apply(struc -> struc.get().setMinHeight(0)).apply(struc -> ((VBox) struc.get().getContent()).setSpacing(5));
var selected = new SimpleObjectProperty<DataStoreEntryRef<T>>();
var add = new DataStoreChoiceComp<T>(DataStoreChoiceComp.Mode.OTHER, null, selected, storeClass, applicableCheck);
selected.addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
if (!selectedList.contains(newValue)
&& (applicableCheck == null
|| applicableCheck.test(newValue))) {
selectedList.add(newValue);
}
selected.setValue(null);
}
});
var vbox = new VerticalComp(List.of(list, Comp.vspacer(5), add)).apply(struc -> struc.get().setFillWidth(true));
return vbox.styleClass("data-store-list-choice-comp").createRegion();
}
}

View file

@ -1,136 +0,0 @@
package io.xpipe.app.fxcomps.impl;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import java.util.ArrayList;
import java.util.List;
public class DynamicOptionsComp extends Comp<CompStructure<Pane>> {
private final List<Entry> entries;
private final boolean wrap;
public DynamicOptionsComp(List<Entry> entries, boolean wrap) {
this.entries = entries;
this.wrap = wrap;
}
public Entry queryEntry(String key) {
return entries.stream()
.filter(entry -> entry.key != null && entry.key.equals(key))
.findAny()
.orElseThrow();
}
@Override
public CompStructure<Pane> createBase() {
Pane pane;
if (wrap) {
var content = new FlowPane(Orientation.HORIZONTAL);
content.setAlignment(Pos.CENTER);
content.setHgap(14);
content.setVgap(7);
pane = content;
} else {
var content = new VBox();
content.setSpacing(7);
pane = content;
}
var nameRegions = new ArrayList<Region>();
var compRegions = new ArrayList<Region>();
for (var entry : getEntries()) {
Region compRegion = null;
if (entry.comp() != null) {
compRegion = entry.comp().createRegion();
}
if (entry.name() != null) {
var line = new HBox();
line.setFillHeight(true);
if (!wrap) {
line.prefWidthProperty().bind(pane.widthProperty());
}
line.setSpacing(8);
var name = new Label();
name.textProperty().bind(entry.name());
name.prefHeightProperty().bind(line.heightProperty());
name.setMinWidth(Region.USE_PREF_SIZE);
name.setAlignment(Pos.CENTER_LEFT);
if (compRegion != null) {
name.visibleProperty().bind(PlatformThread.sync(compRegion.visibleProperty()));
name.managedProperty().bind(PlatformThread.sync(compRegion.managedProperty()));
}
nameRegions.add(name);
line.getChildren().add(name);
if (compRegion != null) {
compRegions.add(compRegion);
line.getChildren().add(compRegion);
if (!wrap) {
HBox.setHgrow(compRegion, Priority.ALWAYS);
}
}
pane.getChildren().add(line);
} else {
if (compRegion != null) {
compRegions.add(compRegion);
pane.getChildren().add(compRegion);
}
}
}
if (wrap) {
var compWidthBinding = Bindings.createDoubleBinding(
() -> {
if (compRegions.stream().anyMatch(r -> r.getWidth() == 0)) {
return Region.USE_COMPUTED_SIZE;
}
return compRegions.stream()
.map(Region::getWidth)
.max(Double::compareTo)
.orElse(0.0);
},
compRegions.stream().map(Region::widthProperty).toList().toArray(new Observable[0]));
compRegions.forEach(r -> r.prefWidthProperty().bind(compWidthBinding));
}
if (entries.stream().anyMatch(entry -> entry.name() != null)) {
var nameWidthBinding = Bindings.createDoubleBinding(
() -> {
if (nameRegions.stream().anyMatch(r -> r.getWidth() == 0)) {
return Region.USE_COMPUTED_SIZE;
}
return nameRegions.stream()
.map(Region::getWidth)
.max(Double::compareTo)
.orElse(0.0);
},
nameRegions.stream().map(Region::widthProperty).toList().toArray(new Observable[0]));
nameRegions.forEach(r -> r.prefWidthProperty().bind(nameWidthBinding));
}
return new SimpleCompStructure<>(pane);
}
public List<Entry> getEntries() {
return entries;
}
public record Entry(String key, ObservableValue<String> name, Comp<?> comp) {}
}

View file

@ -5,7 +5,6 @@ import io.xpipe.app.browser.StandaloneFileBrowser;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.core.store.FileSystemStore;
import io.xpipe.core.store.ShellStore;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
@ -22,10 +21,6 @@ public class FileStoreChoiceComp extends SimpleComp {
private final Property<FileSystemStore> fileSystem;
private final Property<String> filePath;
public FileStoreChoiceComp(Property<String> filePath) {
this(true, new SimpleObjectProperty<>(), filePath);
}
public FileStoreChoiceComp(boolean hideFileSystem, Property<FileSystemStore> fileSystem, Property<String> filePath) {
this.hideFileSystem = hideFileSystem;
this.fileSystem = fileSystem != null ? fileSystem : new SimpleObjectProperty<>();
@ -46,7 +41,7 @@ public class FileStoreChoiceComp extends SimpleComp {
.grow(false, true);
var fileBrowseButton = new ButtonComp(null, new FontIcon("mdi2f-folder-open-outline"), () -> {
StandaloneFileBrowser.openSingleFile(() -> (ShellStore) fileSystem.getValue(), fileStore -> {
StandaloneFileBrowser.openSingleFile(() -> null, fileStore -> {
if (fileStore == null) {
filePath.setValue(null);
fileSystem.setValue(null);

View file

@ -16,6 +16,10 @@ public class HorizontalComp extends Comp<CompStructure<HBox>> {
entries = List.copyOf(comps);
}
public Comp<CompStructure<HBox>> spacing(double spacing) {
return apply(struc -> struc.get().setSpacing(spacing));
}
@Override
public CompStructure<HBox> createBase() {
HBox b = new HBox();

View file

@ -5,7 +5,7 @@ import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileNames;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;

View file

@ -2,14 +2,14 @@ package io.xpipe.app.fxcomps.impl;
import io.xpipe.app.core.AppImages;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileNames;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
public class PrettyImageHelper {
public static Comp<?> ofFixedSquare(String img, int size) {
if (img.endsWith(".svg")) {
if (img != null && img.endsWith(".svg")) {
var base = FileNames.getBaseName(img);
var renderedName = base + "-" + size + ".png";
if (AppImages.hasNormalImage(base + "-" + size + ".png")) {

View file

@ -5,7 +5,7 @@ import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileNames;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;

View file

@ -1,71 +0,0 @@
package io.xpipe.app.fxcomps.impl;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.SimpleValidator;
import io.xpipe.app.util.Validatable;
import io.xpipe.app.util.Validator;
import io.xpipe.core.source.WriteMode;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.layout.Region;
import lombok.EqualsAndHashCode;
import lombok.Value;
import net.synedra.validatorfx.Check;
import java.util.LinkedHashMap;
import java.util.Map;
@Value
@EqualsAndHashCode(callSuper = true)
public class WriteModeChoiceComp extends SimpleComp implements Validatable {
Property<WriteMode> selected;
ObservableList<WriteMode> available;
Validator validator = new SimpleValidator();
Check check;
public WriteModeChoiceComp(Property<WriteMode> selected, ObservableList<WriteMode> available) {
this.selected = selected;
this.available = available;
if (available.size() == 1) {
selected.setValue(available.get(0));
}
check = Validator.nonNull(validator, AppI18n.observable("mode"), selected);
}
@Override
protected Region createSimple() {
var a = available;
Property<Map<WriteMode, ObservableValue<String>>> map = new SimpleObjectProperty<>(new LinkedHashMap<>());
for (WriteMode writeMode : a) {
map.getValue().put(writeMode, AppI18n.observable(writeMode.getId()));
}
PlatformThread.sync(available).addListener((ListChangeListener<? super WriteMode>) c -> {
var newMap = new LinkedHashMap<WriteMode, ObservableValue<String>>();
for (WriteMode writeMode : c.getList()) {
newMap.put(writeMode, AppI18n.observable(writeMode.getId()));
}
map.setValue(newMap);
if (c.getList().size() == 1) {
selected.setValue(c.getList().get(0));
}
});
return new ToggleGroupComp<>(selected, map)
.apply(struc -> {
for (int i = 0; i < a.size(); i++) {
new FancyTooltipAugment<>(a.get(i).getId() + "Description")
.augment(struc.get().getChildren().get(i));
}
})
.apply(struc -> check.decorates(struc.get()))
.createRegion();
}
}

View file

@ -101,7 +101,7 @@ public class BindingsHelper {
public static <T,U> ObservableValue<U> map(ObservableValue<T> observableValue, Function<? super T, ? extends U> mapper) {
return persist(Bindings.createObjectBinding(() -> {
return mapper.apply(observableValue.getValue());
},observableValue));
}, observableValue));
}
public static <T,U> ObservableValue<U> flatMap(ObservableValue<T> observableValue, Function<? super T, ? extends ObservableValue<? extends U>> mapper) {
@ -117,6 +117,12 @@ public class BindingsHelper {
return prop;
}
public static <T,U> ObservableValue<Boolean> anyMatch(List<? extends ObservableValue<Boolean>> l) {
return BindingsHelper.persist(Bindings.createBooleanBinding(() -> {
return l.stream().anyMatch(booleanObservableValue -> booleanObservableValue.getValue());
}, l.toArray(ObservableValue[]::new)));
}
public static <T, V> void bindMappedContent(ObservableList<T> l1, ObservableList<V> l2, Function<V, T> map) {
Runnable runnable = () -> {
setContent(l1, l2.stream().map(map).toList());

View file

@ -18,6 +18,7 @@ import java.util.concurrent.CountDownLatch;
public class PlatformThread {
public static Observable sync(Observable o) {
Objects.requireNonNull(o);
return new Observable() {
private final Map<InvalidationListener, InvalidationListener> invListenerMap = new ConcurrentHashMap<>();
@ -40,6 +41,7 @@ public class PlatformThread {
}
public static <T> ObservableValue<T> sync(ObservableValue<T> ov) {
Objects.requireNonNull(ov);
ObservableValue<T> obs = new ObservableValue<>() {
private final Map<ChangeListener<? super T>, ChangeListener<? super T>> changeListenerMap =
@ -86,6 +88,7 @@ public class PlatformThread {
}
public static <T> ObservableList<T> sync(ObservableList<T> ol) {
Objects.requireNonNull(ol);
ObservableList<T> obs = new ObservableList<>() {
private final Map<ListChangeListener<? super T>, ListChangeListener<? super T>> listChangeListenerMap =

View file

@ -4,11 +4,13 @@ import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.TitledPaneComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.util.JfxHelper;
import io.xpipe.app.util.LicenseException;
import io.xpipe.app.util.PlatformState;
import javafx.application.Platform;
import javafx.beans.property.Property;
@ -192,6 +194,26 @@ public class ErrorHandlerComp extends SimpleComp {
actionBox.getStyleClass().add("actions");
actionBox.setFillWidth(true);
if (event.getThrowable() instanceof LicenseException) {
event.getCustomActions().add(new ErrorAction() {
@Override
public String getName() {
return AppI18n.get("upgrade");
}
@Override
public String getDescription() {
return AppI18n.get("seeTiers");
}
@Override
public boolean handle(ErrorEvent event) {
AppLayoutModel.get().selectLicense();
return true;
}
});
}
var custom = event.getCustomActions();
for (var c : custom) {
var ac = createActionComp(c);

View file

@ -6,7 +6,7 @@ import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.core.store.ShellStore;
import io.xpipe.app.storage.DataStorage;
import lombok.Getter;
import lombok.Value;
@ -116,7 +116,7 @@ public abstract class LauncherInput {
var dir = Files.isDirectory(file) ? file : file.getParent();
AppLayoutModel.get().selectBrowser();
BrowserModel.DEFAULT.openFileSystemAsync(null, ShellStore.createLocal(), dir.toString(), null);
BrowserModel.DEFAULT.openFileSystemAsync( DataStorage.get().local().ref(), dir.toString(), null);
}
@Override

View file

@ -19,7 +19,7 @@ import io.xpipe.app.fxcomps.impl.StackComp;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.*;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.store.LocalStore;
import io.xpipe.core.util.ModuleHelper;
import io.xpipe.core.util.SecretValue;
import javafx.beans.binding.Bindings;
@ -198,10 +198,6 @@ public class AppPrefs {
private final BooleanField automaticallyCheckForUpdatesField =
BooleanField.ofBooleanType(automaticallyCheckForUpdates).render(() -> new CustomToggleControl());
private final BooleanProperty checkForPrereleases = typed(new SimpleBooleanProperty(false), Boolean.class);
private final BooleanField checkForPrereleasesField =
BooleanField.ofBooleanType(checkForPrereleases).render(() -> new CustomToggleControl());
private final BooleanProperty confirmDeletions = typed(new SimpleBooleanProperty(true), Boolean.class);
// Storage
@ -309,10 +305,6 @@ public class AppPrefs {
return automaticallyCheckForUpdates;
}
public ReadOnlyBooleanProperty updateToPrereleases() {
return checkForPrereleases;
}
public ReadOnlyBooleanProperty confirmDeletions() {
return confirmDeletions;
}
@ -569,16 +561,15 @@ public class AppPrefs {
"appBehaviour",
Setting.of("startupBehaviour", startupBehaviourControl, startupBehaviour),
Setting.of("closeBehaviour", closeBehaviourControl, closeBehaviour)),
Group.of(
"advanced",
Setting.of("developerMode", developerModeField, internalDeveloperMode)),
Group.of(
"updates",
Setting.of(
"automaticallyUpdate",
automaticallyCheckForUpdatesField,
automaticallyCheckForUpdates),
Setting.of("updateToPrereleases", checkForPrereleasesField, checkForPrereleases)),
Group.of(
"advanced",
Setting.of("developerMode", developerModeField, internalDeveloperMode))),
automaticallyCheckForUpdates))),
new VaultCategory(this).create(),
Category.of(
"appearance",

View file

@ -2,7 +2,7 @@ package io.xpipe.app.prefs;
import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.store.LocalStore;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialects;

View file

@ -6,8 +6,8 @@ import io.xpipe.app.util.ApplicationHelper;
import io.xpipe.app.util.MacOsPermissions;
import io.xpipe.app.util.ScriptHelper;
import io.xpipe.app.util.WindowsRegistry;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.LocalStore;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellControl;
@ -170,7 +170,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
@Override
public void launch(String name, String file) throws Exception {
try (ShellControl pc = LocalStore.getShell()) {
ApplicationHelper.checkSupport(pc, executable, toTranslatedString(), null);
ApplicationHelper.isInPath(pc, executable, toTranslatedString(), null);
var toExecute = executable + " " + toCommand(name, file).build(pc);
// In order to fix this bug which also affects us:
@ -590,7 +590,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
@Override
public void launch(String name, String file) throws Exception {
try (ShellControl pc = LocalStore.getShell()) {
ApplicationHelper.checkSupport(pc, executable, toTranslatedString(), null);
ApplicationHelper.isInPath(pc, executable, toTranslatedString(), null);
var toExecute = executable + " " + toCommand(name, file).build(pc);
if (pc.getOsType().equals(OsType.WINDOWS)) {

View file

@ -51,7 +51,7 @@ public class JsonStorageHandler implements StorageHandler {
var id = getSaveId(breadcrumb);
var tree = object instanceof PrefsChoiceValue prefsChoiceValue
? new TextNode(prefsChoiceValue.getId())
: (object != null ? JacksonMapper.newMapper().valueToTree(object) : NullNode.getInstance());
: (object != null ? JacksonMapper.getDefault().valueToTree(object) : NullNode.getInstance());
setContent(id, tree);
}
@ -102,7 +102,7 @@ public class JsonStorageHandler implements StorageHandler {
try {
TrackEvent.debug("Loading preferences value for key " + breadcrumb + " from value " + tree);
return JacksonMapper.newMapper().treeToValue(tree, type);
return JacksonMapper.getDefault().treeToValue(tree, type);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omit().handle();
return defaultObject;

View file

@ -10,7 +10,7 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.TextFieldComp;
import io.xpipe.app.util.TerminalHelper;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.store.LocalStore;
import io.xpipe.core.process.CommandControl;
import io.xpipe.core.process.ShellDialects;
import javafx.beans.property.Property;

View file

@ -9,9 +9,9 @@ import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.UserReportComp;
import io.xpipe.app.util.*;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.ShellStore;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.LocalStore;
import io.xpipe.core.util.XPipeInstallation;
import java.util.List;
@ -58,7 +58,7 @@ public class TroubleshootComp extends Comp<CompStructure<?>> {
.addComp(
new TileButtonComp("launchDebugMode", "launchDebugModeDescription", "mdmz-refresh", e -> {
OperationMode.executeAfterShutdown(() -> {
try (var sc = ShellStore.createLocal()
try (var sc = new LocalStore()
.control()
.start()) {
var script = FileNames.join(

View file

@ -1,17 +1,18 @@
package io.xpipe.app.storage;
import io.xpipe.core.store.StatefulDataStore;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.DataStoreState;
import io.xpipe.core.util.DataStateProvider;
import java.nio.file.Path;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Supplier;
public class DataStateProviderImpl extends DataStateProvider {
@Override
public void putState(DataStore store, String key, Object value) {
public void setState(DataStore store, Object value) {
if (DataStorage.get() == null) {
return;
}
@ -21,14 +22,12 @@ public class DataStateProviderImpl extends DataStateProvider {
return;
}
var old = entry.get().getElementState().put(key, value);
if (!Objects.equals(old, value)) {
entry.get().simpleRefresh();
}
entry.get().setStorePersistentState(value);
}
@Override
public <T> T getState(DataStore store, String key, Class<T> c, Supplier<T> def) {
@SuppressWarnings("unchecked")
public <T extends DataStoreState> T getState(DataStore store, Supplier<T> def) {
if (DataStorage.get() == null) {
return def.get();
}
@ -38,7 +37,43 @@ public class DataStateProviderImpl extends DataStateProvider {
return def.get();
}
var result = entry.get().getElementState().computeIfAbsent(key, k -> def.get());
if (!(store instanceof StatefulDataStore<?> sds)) {
return def.get();
}
var found = entry.get().getStorePersistentState();
if (found == null) {
entry.get().setStorePersistentState(def.get());
}
return (T) entry.get().getStorePersistentState();
}
@Override
public void putCache(DataStore store, String key, Object value) {
if (DataStorage.get() == null) {
return;
}
var entry = DataStorage.get().getStoreEntryIfPresent(store);
if (entry.isEmpty()) {
return;
}
var old = entry.get().getStoreCache().put(key, value);
}
@Override
public <T> T getCache(DataStore store, String key, Class<T> c, Supplier<T> def) {
if (DataStorage.get() == null) {
return def.get();
}
var entry = DataStorage.get().getStoreEntryIfPresent(store);
if (entry.isEmpty()) {
return def.get();
}
var result = entry.get().getStoreCache().computeIfAbsent(key, k -> def.get());
return c.cast(result);
}

View file

@ -3,12 +3,12 @@ package io.xpipe.app.storage;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.FixedHierarchyStore;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.DataStoreId;
import io.xpipe.core.store.FixedChildStore;
import io.xpipe.core.store.FixedHierarchyStore;
import io.xpipe.core.store.LocalStore;
import io.xpipe.core.util.FailableRunnable;
import javafx.util.Pair;
import lombok.Getter;
@ -18,12 +18,18 @@ import lombok.Setter;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
public abstract class DataStorage {
public static final UUID ALL_CATEGORY_UUID = UUID.fromString("bfb0b51a-e7a3-4ce4-8878-8d4cb5828d6c");
public static final UUID SCRIPTS_CATEGORY_UUID = UUID.fromString("19024cf9-d192-41a9-88a6-a22694cf716a");
public static final UUID PREDEFINED_SCRIPTS_CATEGORY_UUID = UUID.fromString("5faf1d71-0efc-4293-8b70-299406396973");
public static final UUID CUSTOM_SCRIPTS_CATEGORY_UUID = UUID.fromString("d3496db5-b709-41f9-abc0-ee0a660fbab9");
public static final UUID DEFAULT_CATEGORY_UUID = UUID.fromString("97458c07-75c0-4f9d-a06e-92d8cdf67c40");
public static final UUID LOCAL_ID = UUID.fromString("f0ec68aa-63f5-405c-b178-9a4454556d6b");
private static final String PERSIST_PROP = "io.xpipe.storage.persist";
private static final String IMMUTABLE_PROP = "io.xpipe.storage.immutable";
@ -45,11 +51,23 @@ public abstract class DataStorage {
this.storeCategories = new CopyOnWriteArrayList<>();
}
public DataStoreCategory getDefaultCategory(){
protected void refreshValidities(boolean makeValid) {
var changed = new AtomicBoolean(false);
do {
changed.set(false);
storeEntries.forEach(dataStoreEntry -> {
if (makeValid ? dataStoreEntry.tryMakeValid() : dataStoreEntry.tryMakeInvalid()) {
changed.set(true);
}
});
} while (changed.get());
}
public DataStoreCategory getDefaultCategory() {
return getStoreCategoryIfPresent(DEFAULT_CATEGORY_UUID).orElseThrow();
}
public DataStoreCategory getAllCategory(){
public DataStoreCategory getAllCategory() {
return getStoreCategoryIfPresent(ALL_CATEGORY_UUID).orElseThrow();
}
@ -69,8 +87,6 @@ public abstract class DataStorage {
INSTANCE = shouldPersist() ? new StandardStorage() : new ImpersistentStorage();
INSTANCE.load();
INSTANCE.storeEntries.forEach(entry -> entry.simpleRefresh());
DataStoreProviders.getAll().forEach(dataStoreProvider -> {
try {
dataStoreProvider.storageInit();
@ -94,10 +110,6 @@ public abstract class DataStorage {
}
public boolean refreshChildren(DataStoreEntry e) {
return refreshChildren(e, null);
}
public boolean refreshChildren(DataStoreEntry e, DataStore newValue) {
if (!(e.getStore() instanceof FixedHierarchyStore)) {
return false;
}
@ -105,7 +117,7 @@ public abstract class DataStorage {
e.setInRefresh(true);
Map<String, FixedChildStore> newChildren;
try {
newChildren = ((FixedHierarchyStore) (newValue != null ? newValue : e.getStore())).listChildren();
newChildren = ((FixedHierarchyStore) (e.getStore())).listChildren(e);
e.setInRefresh(false);
} catch (Exception ex) {
e.setInRefresh(false);
@ -113,12 +125,11 @@ public abstract class DataStorage {
return false;
}
synchronized (this) {
var oldChildren = getStoreChildren(e, false, false);
var oldChildren = getStoreChildren(e, false);
var toRemove = oldChildren.stream()
.filter(entry -> newChildren.entrySet().stream()
.noneMatch(nc ->
nc.getValue().getFixedId() == ((FixedChildStore) entry.getStore()).getFixedId()))
.noneMatch(
nc -> nc.getValue().getFixedId() == ((FixedChildStore) entry.getStore()).getFixedId()))
.toList();
var toAdd = newChildren.entrySet().stream()
.filter(entry -> oldChildren.stream()
@ -136,12 +147,12 @@ public abstract class DataStorage {
.filter(en -> en.getValue() != null)
.toList();
if (newValue != null) {
e.setStoreInternal(newValue, false);
if (newChildren.size() > 0) {
e.setExpanded(true);
}
deleteWithChildren(toRemove.toArray(DataStoreEntry[]::new));
addStoreEntries(toAdd.stream()
addStoreEntriesIfNotPresent(toAdd.stream()
.map(stringDataStoreEntry -> DataStoreEntry.createNew(
UUID.randomUUID(),
e.getCategoryUuid(),
@ -158,67 +169,90 @@ public abstract class DataStorage {
saveAsync();
return !newChildren.isEmpty();
}
}
public void deleteWithChildren(DataStoreEntry... entries) {
var toDelete = Arrays.stream(entries)
.flatMap(entry -> {
// Reverse to delete deepest children first
var ordered = getStoreChildren(entry, false, true);
var ordered = getStoreChildren(entry, true);
Collections.reverse(ordered);
ordered.add(entry);
return ordered.stream();
})
.toList();
synchronized (this) {
toDelete.forEach(entry -> entry.finalizeEntry());
this.storeEntries.removeAll(toDelete);
this.listeners.forEach(l -> l.onStoreRemove(toDelete.toArray(DataStoreEntry[]::new)));
}
refreshValidities(false);
saveAsync();
}
public void deleteChildren(DataStoreEntry e, boolean deep) {
// Reverse to delete deepest children first
var ordered = getStoreChildren(e, true, deep);
var ordered = getStoreChildren(e, deep);
Collections.reverse(ordered);
synchronized (this) {
ordered.forEach(entry -> entry.finalizeEntry());
this.storeEntries.removeAll(ordered);
this.listeners.forEach(l -> l.onStoreRemove(ordered.toArray(DataStoreEntry[]::new)));
}
refreshValidities(false);
saveAsync();
}
public Optional<DataStoreEntry> getParent(DataStoreEntry entry, boolean display) {
if (entry.getState() == DataStoreEntry.State.LOAD_FAILED) {
public Optional<DataStoreEntry> getDisplayParent(DataStoreEntry entry) {
if (entry.getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {
return Optional.empty();
}
try {
var provider = entry.getProvider();
var parent =
display ? provider.getDisplayParent(entry.getStore()) : provider.getLogicalParent(entry.getStore());
return parent != null ? getStoreEntryIfPresent(parent) : Optional.empty();
return Optional.ofNullable(provider.getDisplayParent(entry)).filter(dataStoreEntry -> storeEntries.contains(dataStoreEntry));
} catch (Exception ex) {
return Optional.empty();
}
}
public synchronized List<DataStoreEntry> getStoreChildren(DataStoreEntry entry, boolean display, boolean deep) {
if (entry.getState() == DataStoreEntry.State.LOAD_FAILED) {
public Optional<DataStoreEntry> findEntry(DataStore store) {
if (store == null) {
return Optional.empty();
}
for (DataStoreEntry entry : storeEntries) {
if (entry.getStore() == null) {
continue;
}
if (!entry.getStore()
.getClass()
.equals(store.getClass())) {
continue;
}
if (entry.getStore().equals(store)) {
return Optional.of(entry);
}
}
return Optional.empty();
}
public List<DataStoreEntry> getStoreChildren(DataStoreEntry entry, boolean deep) {
if (entry.getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {
return List.of();
}
var children = new ArrayList<>(getStoreEntries().stream()
var entries = getStoreEntries();
if (!entries.contains(entry)) {
return List.of();
}
var children = new ArrayList<>(entries.stream()
.filter(other -> {
if (other.getState() == DataStoreEntry.State.LOAD_FAILED) {
if (other.getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {
return false;
}
var parent = getParent(other, display);
var parent = getDisplayParent(other);
return parent.isPresent()
&& entry.getStore()
.getClass()
@ -229,7 +263,7 @@ public abstract class DataStorage {
if (deep) {
for (DataStoreEntry dataStoreEntry : new ArrayList<>(children)) {
children.addAll(getStoreChildren(dataStoreEntry, display, true));
children.addAll(getStoreChildren(dataStoreEntry, true));
}
}
@ -258,22 +292,14 @@ public abstract class DataStorage {
return dir.resolve("categories");
}
public synchronized List<DataStore> getUsableStores() {
public List<DataStore> getUsableStores() {
return new ArrayList<>(getStoreEntries().stream()
.filter(entry -> entry.getState().isUsable())
.filter(entry -> entry.getValidity().isUsable())
.map(DataStoreEntry::getStore)
.toList());
}
public synchronized void renameStoreEntry(DataStoreEntry entry, String name) {
if (getStoreEntryIfPresent(name).isPresent()) {
throw new IllegalArgumentException("Store with name " + name + " already exists");
}
entry.setName(name);
}
public synchronized DataStoreEntry getStoreEntry(@NonNull String name, boolean acceptDisabled) {
public DataStoreEntry getStoreEntry(@NonNull String name, boolean acceptDisabled) {
var entry = storeEntries.stream()
.filter(n -> n.getName().equalsIgnoreCase(name))
.findFirst()
@ -286,15 +312,15 @@ public abstract class DataStorage {
public DataStoreId getId(DataStoreEntry entry) {
var names = new ArrayList<String>();
names.add(entry.getName());
names.add(entry.getName().replaceAll(":", "_"));
DataStoreEntry current = entry;
while ((current = getParent(current, false).orElse(null)) != null) {
while ((current = getDisplayParent(current).orElse(null)) != null) {
if (new LocalStore().equals(current.getStore())) {
break;
}
names.add(0, current.getName());
names.add(0, current.getName().replaceAll(":", "_"));
}
return DataStoreId.create(names.toArray(String[]::new));
@ -313,7 +339,7 @@ public abstract class DataStorage {
var current = getStoreEntryIfPresent(id.getNames().get(0));
if (current.isPresent()) {
for (int i = 1; i < id.getNames().size(); i++) {
var children = getStoreChildren(current.get(), false, false);
var children = getStoreChildren(current.get(), false);
int finalI = i;
current = children.stream()
.filter(dataStoreEntry -> dataStoreEntry
@ -344,6 +370,7 @@ public abstract class DataStorage {
public abstract boolean supportsSharing();
public Optional<DataStoreCategory> getStoreCategoryIfPresent(UUID uuid) {
if (uuid == null) {
return Optional.empty();
@ -362,40 +389,30 @@ public abstract class DataStorage {
.findFirst();
}
public boolean setAndRefresh(DataStoreEntry entry, DataStore s) {
var old = entry.getStore();
deleteChildren(entry, true);
try {
entry.setStoreInternal(s, false);
entry.refresh(true);
return DataStorage.get().refreshChildren(entry, s);
} catch (Exception e) {
entry.setStoreInternal(old, false);
entry.simpleRefresh();
return false;
}
}
public void updateEntry(DataStoreEntry entry, DataStoreEntry newEntry) {
var oldParent = DataStorage.get().getParent(entry, false);
var newParent = DataStorage.get().getParent(newEntry, false);
var oldParent = DataStorage.get().getDisplayParent(entry);
var newParent = DataStorage.get().getDisplayParent(newEntry);
var diffParent = Objects.equals(oldParent, newParent);
propagateUpdate(
() -> {
newEntry.finalizeEntry();
var children = getStoreChildren(entry, false, true);
if (!Objects.equals(oldParent, newParent)) {
var toRemove = Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new);
var children = getStoreChildren(entry, true);
if (!diffParent) {
var toRemove = Stream.concat(Stream.of(entry), children.stream())
.toArray(DataStoreEntry[]::new);
listeners.forEach(storageListener -> storageListener.onStoreRemove(toRemove));
}
entry.applyChanges(newEntry);
entry.initializeEntry();
if (!Objects.equals(oldParent, newParent)) {
var toAdd = Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new);
if (!diffParent) {
var toAdd = Stream.concat(Stream.of(entry), children.stream())
.toArray(DataStoreEntry[]::new);
listeners.forEach(storageListener -> storageListener.onStoreAdd(toAdd));
refreshValidities(true);
}
},
entry);
@ -404,79 +421,99 @@ public abstract class DataStorage {
public void updateCategory(DataStoreEntry entry, DataStoreCategory newCategory) {
propagateUpdate(
() -> {
var children = getStoreChildren(entry, false, true);
var toRemove = Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new);
var children = getStoreChildren(entry, true);
var toRemove =
Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new);
listeners.forEach(storageListener -> storageListener.onStoreRemove(toRemove));
entry.setCategoryUuid(newCategory.getUuid());
children.forEach(child -> child.setCategoryUuid(newCategory.getUuid()));
var toAdd = Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new);
var toAdd =
Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new);
listeners.forEach(storageListener -> storageListener.onStoreAdd(toAdd));
},
entry);
}
public boolean refresh(DataStoreEntry element, boolean deep) {
if (element.isInRefresh()) {
return false;
}
try {
propagateUpdate(() -> element.refresh(deep), element);
} catch (Exception e) {
ErrorEvent.fromThrowable(e).reportable(false).handle();
return false;
}
saveAsync();
return true;
}
public void refreshAsync(DataStoreEntry element, boolean deep) {
ThreadHelper.runAsync(() -> {
try {
propagateUpdate(() -> element.refresh(deep), element);
} catch (Exception e) {
ErrorEvent.fromThrowable(e).reportable(false).handle();
}
saveAsync();
});
}
<T extends Throwable> void propagateUpdate(FailableRunnable<T> runnable, DataStoreEntry origin) throws T {
var children = getStoreChildren(origin, false, true);
var children = getStoreChildren(origin, true);
runnable.run();
children.forEach(entry -> {
entry.simpleRefresh();
entry.refresh();
});
}
public DataStoreCategory addStoreCategoryIfNotPresent(@NonNull DataStoreCategory cat) {
if (storeCategories.contains(cat)) {
return cat;
}
var byId = getStoreCategoryIfPresent(cat.getUuid()).orElse(null);
if (byId != null) {
return byId;
}
addStoreCategory(cat);
return cat;
}
public void addStoreCategory(@NonNull DataStoreCategory cat) {
synchronized (this) {
cat.setDirectory(getCategoriesDir().resolve(cat.getUuid().toString()));
this.storeCategories.add(cat);
}
saveAsync();
this.listeners.forEach(l -> l.onCategoryAdd(cat));
}
public void addStoreEntry(@NonNull DataStoreEntry e) {
e.getProvider().preAdd(e.getStore());
synchronized (this) {
public DataStoreEntry addStoreEntryIfNotPresent(@NonNull DataStoreEntry e) {
if (storeEntries.contains(e)) {
return e;
}
var byId = getStoreEntryIfPresent(e.getUuid()).orElse(null);
if (byId != null) {
return byId;
}
if (e.getValidity().isUsable()) {
var displayParent = e.getProvider().getDisplayParent(e);
if (displayParent != null) {
displayParent.setExpanded(true);
addStoreEntryIfNotPresent(displayParent);
}
}
e.setDirectory(getStoresDir().resolve(e.getUuid().toString()));
this.storeEntries.add(e);
}
saveAsync();
this.listeners.forEach(l -> l.onStoreAdd(e));
e.initializeEntry();
refreshValidities(true);
return e;
}
public void addStoreEntries(@NonNull DataStoreEntry... es) {
synchronized (this) {
public DataStoreEntry getOrCreateNewEntry(String name, DataStore store) {
var found = findEntry(store);
if (found.isPresent()) {
return found.get();
}
return DataStoreEntry.createNew(UUID.randomUUID(), selectedCategory.getUuid(), name, store);
}
public void addStoreEntriesIfNotPresent(@NonNull DataStoreEntry... es) {
for (DataStoreEntry e : es) {
e.getProvider().preAdd(e.getStore());
if (storeEntries.contains(e) || findEntry(e.getStore()).isPresent()) {
return;
}
var displayParent = e.getProvider().getDisplayParent(e);
if (displayParent != null) {
addStoreEntryIfNotPresent(displayParent);
}
e.setDirectory(getStoresDir().resolve(e.getUuid().toString()));
this.storeEntries.add(e);
}
@ -484,24 +521,18 @@ public abstract class DataStorage {
for (DataStoreEntry e : es) {
e.initializeEntry();
}
}
refreshValidities(true);
saveAsync();
}
public DataStoreEntry addStoreEntryIfNotPresent(@NonNull String name, DataStore store) {
var found = getStoreEntryIfPresent(store);
if (found.isPresent()) {
return found.get();
public DataStoreEntry addStoreIfNotPresent(@NonNull String name, DataStore store) {
var f = findEntry(store);
if (f.isPresent()) {
return f.get();
}
var e = DataStoreEntry.createNew(UUID.randomUUID(), selectedCategory.getUuid(), name, store);
addStoreEntry(e);
return e;
}
public DataStoreEntry addStoreEntry(@NonNull String name, DataStore store) {
var e = DataStoreEntry.createNew(UUID.randomUUID(), selectedCategory.getUuid(), name, store);
addStoreEntry(e);
addStoreEntryIfNotPresent(e);
return e;
}
@ -510,31 +541,27 @@ public abstract class DataStorage {
return Optional.empty();
}
return DataStorage.get().getStoreEntries().stream()
.filter(entry -> !entry.isDisabled() && entry.getStore().equals(store))
.findFirst()
.map(entry -> entry.getName());
return findEntry(store).map(dataStoreEntry -> dataStoreEntry.getName());
}
public String getStoreBrowserDisplayName(DataStore store) {
public String getStoreBrowserDisplayName(DataStoreEntry store) {
if (store == null) {
return "?";
}
return getStoreEntryIfPresent(store).map(entry -> entry.getProvider().browserDisplayName(store)).orElse("?");
return store.getProvider().browserDisplayName(store.getStore());
}
public void deleteStoreEntry(@NonNull DataStoreEntry store) {
propagateUpdate(
() -> {
store.finalizeEntry();
synchronized (this) {
this.storeEntries.remove(store);
}
},
store);
saveAsync();
this.listeners.forEach(l -> l.onStoreRemove(store));
refreshValidities(false);
saveAsync();
}
public void deleteStoreCategory(@NonNull DataStoreCategory cat) {
@ -566,10 +593,18 @@ public abstract class DataStorage {
public abstract void save();
public Optional<DataStoreEntry> getStoreEntry(UUID id) {
public Optional<DataStoreEntry> getStoreEntryIfPresent(UUID id) {
return storeEntries.stream().filter(e -> e.getUuid().equals(id)).findAny();
}
public DataStoreEntry getStoreEntry(UUID id) {
return getStoreEntryIfPresent(id).orElseThrow();
}
public DataStoreEntry local() {
return getStoreEntryIfPresent(LOCAL_ID).orElse(null);
}
public List<DataStoreEntry> getStoreEntries() {
return new ArrayList<>(storeEntries);
}

View file

@ -1,36 +1,14 @@
package io.xpipe.app.storage;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.NullNode;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.JacksonMapper;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
public class DataStorageParser {
public static DataSource<?> sourceFromNode(JsonNode node) {
node = replaceReferenceIds(node);
var mapper = JacksonMapper.newMapper();
try {
return mapper.treeToValue(node, DataSource.class);
} catch (Throwable e) {
ErrorEvent.fromThrowable(e).handle();
return null;
}
}
public static DataStore storeFromNode(JsonNode node) {
node = replaceReferenceIds(node);
var mapper = JacksonMapper.newMapper();
var mapper = JacksonMapper.getDefault();
try {
return mapper.treeToValue(node, DataStore.class);
} catch (Throwable e) {
@ -38,74 +16,4 @@ public class DataStorageParser {
return null;
}
}
private static JsonNode replaceReferenceIds(JsonNode node) {
return replaceReferenceIds(node, new HashSet<>());
}
private static JsonNode replaceReferenceIds(JsonNode node, Set<UUID> seenIds) {
var mapper = JacksonMapper.newMapper();
node = replaceReferenceIdsForType(node, "storeId", id -> {
if (seenIds.contains(id)) {
TrackEvent.withWarn("storage", "Encountered cycle").tag("id", id);
return Optional.empty();
}
var entry = DataStorage.get().getStoreEntry(id);
if (entry.isEmpty()) {
TrackEvent.withWarn("storage", "Encountered unknown store").tag("id", id);
return Optional.empty();
}
var testNode = entry.get().getResolvedNode();
if (testNode == null) {
TrackEvent.withWarn("storage", "Encountered disabled store").tag("id", id);
return Optional.empty();
}
var newSeenIds = new HashSet<>(seenIds);
newSeenIds.add(id);
return Optional.of(replaceReferenceIds(entry.get().getStoreNode(), newSeenIds));
});
return node;
}
private static JsonNode replaceReferenceIdsForType(
JsonNode node, String replacementKeyName, Function<UUID, Optional<JsonNode>> function) {
var value = getReferenceIdFromNode(node, replacementKeyName).orElse(null);
if (value != null) {
var found = function.apply(value);
if (found.isEmpty() || found.get().isNull()) {
TrackEvent.withWarn("storage", "Encountered unknown reference").tag("id", value);
}
return found.orElseGet(NullNode::getInstance);
}
if (!node.isObject()) {
return node;
}
var replacement = JsonNodeFactory.instance.objectNode();
var iterator = node.fields();
while (iterator.hasNext()) {
var stringJsonNodeEntry = iterator.next();
var resolved = replaceReferenceIdsForType(stringJsonNodeEntry.getValue(), replacementKeyName, function);
replacement.set(stringJsonNodeEntry.getKey(), resolved);
}
return replacement;
}
private static Optional<UUID> getReferenceIdFromNode(JsonNode node, String key) {
if (node.isObject()) {
var found = node.get(key);
if (found != null && found.isTextual()) {
var id = UUID.fromString(found.textValue());
return Optional.of(id);
}
}
return Optional.empty();
}
}

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