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; package io.xpipe.api.test;
import io.xpipe.api.DataSource; 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.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;

View file

@ -138,7 +138,7 @@ application {
run { run {
systemProperty 'io.xpipe.app.useVirtualThreads', 'false' systemProperty 'io.xpipe.app.useVirtualThreads', 'false'
systemProperty 'io.xpipe.app.mode', 'gui' 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.writeLogs', "true"
systemProperty 'io.xpipe.app.writeSysOut', "true" systemProperty 'io.xpipe.app.writeSysOut', "true"
systemProperty 'io.xpipe.app.developerMode', "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.fxcomps.util.PlatformThread;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.DataStoreCategoryChoiceComp; import io.xpipe.app.util.DataStoreCategoryChoiceComp;
import io.xpipe.app.util.FixedHierarchyStore;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.DataStore; import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.FixedHierarchyStore;
import io.xpipe.core.store.ShellStore; import io.xpipe.core.store.ShellStore;
import javafx.application.Platform;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
@ -53,7 +52,7 @@ final class BrowserBookmarkList extends SimpleComp {
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> { Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore
|| storeEntryWrapper.getEntry().getStore() instanceof FixedHierarchyStore) || storeEntryWrapper.getEntry().getStore() instanceof FixedHierarchyStore)
&& storeEntryWrapper.getEntry().getState().isUsable(); && storeEntryWrapper.getEntry().getValidity().isUsable();
}; };
var section = StoreSectionMiniComp.createList( var section = StoreSectionMiniComp.createList(
StoreSection.createTopLevel( StoreSection.createTopLevel(
@ -68,7 +67,7 @@ final class BrowserBookmarkList extends SimpleComp {
.pseudoClassStateChanged( .pseudoClassStateChanged(
SELECTED, SELECTED,
newValue != null newValue != null
&& newValue.getStore() && newValue.getEntry()
.equals(s.getWrapper() .equals(s.getWrapper()
.getEntry() .getEntry()
.getStore())); .getStore()));
@ -77,13 +76,10 @@ final class BrowserBookmarkList extends SimpleComp {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
var entry = s.getWrapper().getEntry(); var entry = s.getWrapper().getEntry();
if (entry.getStore() instanceof ShellStore fileSystem) { if (entry.getStore() instanceof ShellStore fileSystem) {
BooleanScope.execute(busy, () -> { model.openFileSystemAsync(entry.ref(), null, busy);
s.getWrapper().refreshIfNeeded();
});
model.openFileSystemAsync(null, fileSystem, null, busy);
} else if (entry.getStore() instanceof FixedHierarchyStore) { } else if (entry.getStore() instanceof FixedHierarchyStore) {
BooleanScope.execute(busy, () -> { BooleanScope.execute(busy, () -> {
s.getWrapper().refreshWithChildren(); s.getWrapper().refreshChildren();
}); });
} }
}); });
@ -125,7 +121,7 @@ final class BrowserBookmarkList extends SimpleComp {
return; return;
} }
Platform.runLater(() -> model.openExistingFileSystemIfPresent(null, store.asNeeded())); // Platform.runLater(() -> model.openExistingFileSystemIfPresent(store.asNeeded()));
} }
}; };
DROP_TIMER.schedule(activeTask, 500); 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.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.core.impl.FileNames; import io.xpipe.core.store.FileNames;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.ButtonBase; 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.FileIconManager;
import io.xpipe.app.browser.icon.FileType; import io.xpipe.app.browser.icon.FileType;
import io.xpipe.app.comp.base.MultiContentComp; import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
@ -252,7 +251,7 @@ public class BrowserComp extends SimpleComp {
.bind(Bindings.createDoubleBinding( .bind(Bindings.createDoubleBinding(
() -> model.getBusy().get() ? -1d : 0, PlatformThread.sync(model.getBusy()))); () -> 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(); var logo = PrettyImageHelper.ofFixedSquare(image, 16).createRegion();
tab.graphicProperty() 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.DirectoryType;
import io.xpipe.app.browser.icon.FileType; 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.FileKind;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileSystem;
import lombok.Getter; 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.BooleanScope;
import io.xpipe.app.util.HumanReadableFormat; import io.xpipe.app.util.HumanReadableFormat;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.impl.FileNames; import io.xpipe.core.store.FileNames;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileSystem; 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.fxcomps.util.BindingsHelper;
import io.xpipe.app.issue.ErrorEvent; 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.FileKind;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileSystem;
import javafx.beans.property.Property; 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.fxcomps.util.BindingsHelper;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.impl.FileStore; import io.xpipe.core.store.FileStore;
import io.xpipe.core.store.ShellStore; import io.xpipe.core.store.FileSystemStore;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
@ -76,18 +77,19 @@ public class BrowserModel {
} }
public void restoreState(BrowserSavedState.Entry e, BooleanProperty busy) { 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 -> { storageEntry.ifPresent(entry -> {
openFileSystemAsync(null, entry.getStore().asNeeded(), e.getPath(), busy); openFileSystemAsync(entry.ref(), e.getPath(), busy);
}); });
} }
public void reset() { public void reset() {
var list = new ArrayList<BrowserSavedState.Entry>(); var list = new ArrayList<BrowserSavedState.Entry>();
openFileSystems.forEach(model -> { openFileSystems.forEach(model -> {
var storageEntry = DataStorage.get().getStoreEntryIfPresent(model.getStore()); if (DataStorage.get().getStoreEntries().contains(model.getEntry().get())) {
storageEntry.ifPresent(entry -> list.add(new BrowserSavedState.Entry( list.add(new BrowserSavedState.Entry(
entry.getUuid(), model.getCurrentPath().get()))); model.getEntry().get().getUuid(), model.getCurrentPath().get()));
}
}); });
// Don't override state if it is empty // 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() var found = openFileSystems.stream()
.filter(model -> Objects.equals(model.getStore(), store)) .filter(model -> Objects.equals(model.getEntry(), store))
.findFirst(); .findFirst();
if (found.isPresent()) { if (found.isPresent()) {
selected.setValue(found.get()); selected.setValue(found.get());
} else { } 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 // // Prevent multiple tabs in non browser modes
// if (!mode.equals(Mode.BROWSER)) { // if (!mode.equals(Mode.BROWSER)) {
// ThreadHelper.runFailableAsync(() -> { // ThreadHelper.runFailableAsync(() -> {
@ -177,7 +179,7 @@ public class BrowserModel {
// Prevent multiple calls from interfering with each other // Prevent multiple calls from interfering with each other
synchronized (BrowserModel.this) { synchronized (BrowserModel.this) {
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) { 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.initFileSystem();
model.initSavedState(); 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.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.core.impl.FileNames; import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileSystem;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;

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.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.core.impl.FileNames; import io.xpipe.core.store.FileNames;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.geometry.Insets; import javafx.geometry.Insets;

View file

@ -2,7 +2,7 @@ package io.xpipe.app.browser;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.BooleanScope; 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 io.xpipe.core.store.FileSystem;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;

View file

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

View file

@ -1,8 +1,8 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.impl.FileNames; import io.xpipe.core.store.FileNames;
import io.xpipe.core.impl.LocalStore; import io.xpipe.core.store.LocalStore;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import io.xpipe.core.store.ConnectionFileSystem; import io.xpipe.core.store.ConnectionFileSystem;
import io.xpipe.core.store.FileKind; 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.comp.base.ModalOverlayComp;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.TerminalHelper; import io.xpipe.app.util.TerminalHelper;
import io.xpipe.app.util.ThreadHelper; 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.ShellControl;
import io.xpipe.core.process.ShellDialects; import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.store.*; import io.xpipe.core.store.*;
@ -26,7 +27,7 @@ import java.util.stream.Stream;
@Getter @Getter
public final class OpenFileSystemModel { public final class OpenFileSystemModel {
private final FileSystemStore store; private final DataStoreEntryRef<? extends FileSystemStore> entry;
private FileSystem fileSystem; private FileSystem fileSystem;
private final Property<String> filter = new SimpleStringProperty(); private final Property<String> filter = new SimpleStringProperty();
private final BrowserFileListModel fileList; private final BrowserFileListModel fileList;
@ -42,12 +43,11 @@ public final class OpenFileSystemModel {
private final String tooltip; private final String tooltip;
private boolean local; private boolean local;
public OpenFileSystemModel(String name, BrowserModel browserModel, FileSystemStore store) { public OpenFileSystemModel(BrowserModel browserModel, DataStoreEntryRef<? extends FileSystemStore> entry) {
this.browserModel = browserModel; this.browserModel = browserModel;
this.store = store; this.entry = entry;
var e = DataStorage.get().getStoreEntryIfPresent(store); this.name = entry.get().getName();
this.name = name != null ? name : e.isPresent() ? DataStorage.get().getStoreBrowserDisplayName(store) : "?"; this.tooltip = DataStorage.get().getId(entry.getEntry()).toString();
this.tooltip = e.isPresent() ? DataStorage.get().getId(e.get()).toString() : name;
this.inOverview.bind(Bindings.createBooleanBinding( this.inOverview.bind(Bindings.createBooleanBinding(
() -> { () -> {
return currentPath.get() == null; return currentPath.get() == null;
@ -63,7 +63,7 @@ public final class OpenFileSystemModel {
} }
BooleanScope.execute(busy, () -> { BooleanScope.execute(busy, () -> {
if (store instanceof ShellStore s) { if (entry.getStore() instanceof ShellStore s) {
c.accept(fileSystem.getShell().orElseThrow()); c.accept(fileSystem.getShell().orElseThrow());
if (refresh) { if (refresh) {
refreshSync(); refreshSync();
@ -150,8 +150,7 @@ public final class OpenFileSystemModel {
if (allowCommands && evaluatedPath != null && !FileNames.isAbsolute(evaluatedPath) if (allowCommands && evaluatedPath != null && !FileNames.isAbsolute(evaluatedPath)
&& fileSystem.getShell().isPresent()) { && fileSystem.getShell().isPresent()) {
var directory = currentPath.get(); var directory = currentPath.get();
var name = adjustedPath + " - " var name = adjustedPath + " - " + entry.get().getName();
+ DataStorage.get().getStoreDisplayName(store).orElse("?");
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
if (ShellDialects.ALL.stream() if (ShellDialects.ALL.stream()
.anyMatch(dialect -> adjustedPath.startsWith(dialect.getOpenCommand()))) { .anyMatch(dialect -> adjustedPath.startsWith(dialect.getOpenCommand()))) {
@ -205,7 +204,7 @@ public final class OpenFileSystemModel {
private void cdSyncWithoutCheck(String path) throws Exception { private void cdSyncWithoutCheck(String path) throws Exception {
if (fileSystem == null) { if (fileSystem == null) {
var fs = store.createFileSystem(); var fs = entry.getStore().createFileSystem();
fs.open(); fs.open();
this.fileSystem = fs; this.fileSystem = fs;
} }
@ -364,7 +363,7 @@ public final class OpenFileSystemModel {
public void initFileSystem() throws Exception { public void initFileSystem() throws Exception {
BooleanScope.execute(busy, () -> { BooleanScope.execute(busy, () -> {
var fs = store.createFileSystem(); var fs = entry.getStore().createFileSystem();
fs.open(); fs.open();
this.fileSystem = fs; this.fileSystem = fs;
this.local = this.local =
@ -393,23 +392,17 @@ public final class OpenFileSystemModel {
} }
BooleanScope.execute(busy, () -> { BooleanScope.execute(busy, () -> {
if (store instanceof ShellStore s) { if (entry.getStore() instanceof ShellStore s) {
var connection = ((ConnectionFileSystem) fileSystem).getShellControl(); var connection = ((ConnectionFileSystem) fileSystem).getShellControl();
var command = s.control() var command = s.control()
.initWith(connection.getShellDialect().getCdCommand(directory)) .initWith(connection.getShellDialect().getCdCommand(directory))
.prepareTerminalOpen(directory + " - " .prepareTerminalOpen(directory + " - " + entry.get().getName());
+ DataStorage.get().getStoreDisplayName(store)
.orElse("?"));
TerminalHelper.open(directory, command); TerminalHelper.open(directory, command);
} }
}); });
}); });
} }
public OpenFileSystemHistory getHistory() {
return history;
}
public void backSync() throws Exception { public void backSync() throws Exception {
cdSyncWithoutCheck(history.back()); 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.node.ObjectNode;
import com.fasterxml.jackson.databind.ser.std.StdSerializer; import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import io.xpipe.app.core.AppCache; import io.xpipe.app.core.AppCache;
import io.xpipe.app.storage.DataStorage; import io.xpipe.core.store.FileNames;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.util.JacksonMapper; import io.xpipe.core.util.JacksonMapper;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
@ -81,11 +80,7 @@ public class OpenFileSystemSavedState {
} }
static OpenFileSystemSavedState loadForStore(OpenFileSystemModel model) { static OpenFileSystemSavedState loadForStore(OpenFileSystemModel model) {
var storageEntry = DataStorage.get() var state = AppCache.get("fs-state-" + model.getEntry().get().getUuid(), OpenFileSystemSavedState.class, () -> {
.getStoreEntryIfPresent(model.getStore())
.map(entry -> entry.getUuid())
.orElse(UUID.randomUUID());
var state = AppCache.get("fs-state-" + storageEntry, OpenFileSystemSavedState.class, () -> {
return new OpenFileSystemSavedState(); return new OpenFileSystemSavedState();
}); });
state.setModel(model); state.setModel(model);
@ -127,8 +122,7 @@ public class OpenFileSystemSavedState {
return; return;
} }
var storageEntry = DataStorage.get().getStoreEntryIfPresent(model.getStore()); AppCache.update("fs-state-" + model.getEntry().get().getUuid(), this);
storageEntry.ifPresent(entry -> AppCache.update("fs-state-" + entry.getUuid(), this));
} }
public void cd(String dir) { 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.AppI18n;
import io.xpipe.app.core.AppWindowHelper; import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.core.impl.FileStore; import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.ShellStore; import io.xpipe.core.store.FileStore;
import io.xpipe.core.store.FileSystemStore;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.stage.Window; 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(() -> { PlatformThread.runLaterIfNeeded(() -> {
var model = new BrowserModel(BrowserModel.Mode.SINGLE_FILE_CHOOSER); var model = new BrowserModel(BrowserModel.Mode.SINGLE_FILE_CHOOSER);
var comp = new BrowserComp(model) var comp = new BrowserComp(model)
@ -50,7 +51,7 @@ public class StandaloneFileBrowser {
window.close(); window.close();
}); });
window.show(); 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.prefs.AppPrefs;
import io.xpipe.app.util.ScriptHelper; import io.xpipe.app.util.ScriptHelper;
import io.xpipe.app.util.TerminalHelper; 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 io.xpipe.core.process.ShellControl;
import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FilenameUtils;

View file

@ -1,7 +1,7 @@
package io.xpipe.app.browser.icon; package io.xpipe.app.browser.icon;
import io.xpipe.app.core.AppResources; 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.FileKind;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileSystem;
import lombok.Getter; 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.Comp;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.SimpleChangeListener; 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.Size;
import javafx.css.SizeUnits; import javafx.css.SizeUnits;
import javafx.scene.Node; import javafx.scene.control.Button;
import javafx.scene.control.MenuButton; import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem; import javafx.scene.control.MenuItem;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List; 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; private final List<Comp<?>> items;
public DropdownComp(ObservableValue<String> name, List<Comp<?>> items) { public DropdownComp(List<Comp<?>> items) {
this.name = name;
this.graphic = new SimpleObjectProperty<>(null);
this.items = 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 @Override
public CompStructure<MenuButton> createBase() { public CompStructure<Button> createBase() {
var button = new MenuButton(null); ContextMenu cm = new ContextMenu(items.stream()
if (name != null) { .map(comp -> {
button.textProperty().bind(name); return new MenuItem(null, comp.createRegion());
} })
var graphic = getGraphic(); .toArray(MenuItem[]::new));
if (graphic instanceof FontIcon f) {
SimpleChangeListener.apply(button.fontProperty(), c -> {
f.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels());
});
}
button.setGraphic(getGraphic()); Button button = (Button) new ButtonComp(null, () -> {})
button.getStyleClass().add("dropdown-comp"); .apply(new ContextMenuAugment<>(e -> true, () -> {
return cm;
}))
.createRegion();
items.forEach(comp -> { button.visibleProperty()
var i = new MenuItem(null,comp.createRegion()); .bind(BindingsHelper.anyMatch(cm.getItems().stream()
button.getItems().add(i); .map(menuItem -> menuItem.getGraphic().visibleProperty())
.toList()));
var graphic = new FontIcon("mdi2c-chevron-double-down");
SimpleChangeListener.apply(button.fontProperty(), c -> {
graphic.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels());
}); });
button.setGraphic(graphic);
button.getStyleClass().add("dropdown-comp");
return new SimpleCompStructure<>(button); 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.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.LabelComp; import io.xpipe.app.fxcomps.impl.LabelComp;
import io.xpipe.app.util.DynamicOptionsBuilder;
import io.xpipe.app.util.Hyperlinks; import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.OptionsBuilder;
import javafx.scene.control.Hyperlink; import javafx.scene.control.Hyperlink;
import javafx.scene.control.TextArea; import javafx.scene.control.TextArea;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
@ -27,7 +27,7 @@ public class InstallExtensionComp extends SimpleComp {
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var builder = new DynamicOptionsBuilder(false); var builder = new OptionsBuilder();
builder.addTitle("installRequired"); builder.addTitle("installRequired");
var header = new LabelComp(AppI18n.observable("extensionInstallDescription")) var header = new LabelComp(AppI18n.observable("extensionInstallDescription"))
.apply(struc -> struc.get().setWrapText(true)); .apply(struc -> struc.get().setWrapText(true));

View file

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

View file

@ -67,11 +67,13 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
var newShown = shown.stream() var newShown = shown.stream()
.map(v -> { .map(v -> {
if (!cache.containsKey(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); return cache.get(v);
}) })
.filter(region -> region != null)
.toList(); .toList();
for (int i = 0; i < newShown.size(); i++) { for (int i = 0; i < newShown.size(); i++) {

View file

@ -29,7 +29,9 @@ public class NamedToggleComp extends SimpleComp {
s.setSelected(newValue); s.setSelected(newValue);
}); });
}); });
s.textProperty().bind(PlatformThread.sync(name)); if (name != null) {
s.textProperty().bind(PlatformThread.sync(name));
}
return s; 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.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.StackComp; import io.xpipe.app.fxcomps.impl.StackComp;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.core.impl.FileNames; import io.xpipe.core.store.FileNames;
import io.xpipe.core.process.ShellStoreState;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import java.nio.file.Files; import java.nio.file.Files;
@ -18,20 +21,38 @@ import java.util.Map;
public class OsLogoComp extends SimpleComp { public class OsLogoComp extends SimpleComp {
private final StoreEntryWrapper wrapper; private final StoreEntryWrapper wrapper;
private final ObservableValue<SystemStateComp.State> state;
public OsLogoComp(StoreEntryWrapper wrapper) { public OsLogoComp(StoreEntryWrapper wrapper) {
this(wrapper, new SimpleObjectProperty<>(SystemStateComp.State.SUCCESS));
}
public OsLogoComp(StoreEntryWrapper wrapper, ObservableValue<SystemStateComp.State> state) {
this.wrapper = wrapper; this.wrapper = wrapper;
this.state = state;
} }
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var img = Bindings.createObjectBinding( var img = BindingsHelper.persist(Bindings.createObjectBinding(
() -> { () -> {
return wrapper.getState().getValue() == DataStoreEntry.State.COMPLETE_AND_VALID if (state.getValue() != SystemStateComp.State.SUCCESS) {
? getImage(wrapper.getInformation().get()) : null; return null;
}
var ps = wrapper.getPersistentState().getValue();
if (!(ps instanceof ShellStoreState sss)) {
return null;
}
return getImage(sss.getOsName());
}, },
wrapper.getState(), wrapper.getInformation()); wrapper.getPersistentState(), state));
return new StackComp(List.of(new SystemStateComp(wrapper).hide(img.isNotNull()), PrettyImageHelper.ofSvg(img, 24, 24))).createRegion(); 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<>(); 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.SimpleComp;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
@ -28,18 +29,22 @@ public class StoreToggleComp extends SimpleComp {
@Override @Override
protected Region createSimple() { 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( 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.getShowDetails().get();
}, },
section.getWrapper().getState(), section.getWrapper().getValidity(),
section.getShowDetails())); section.getShowDetails()));
var t = new NamedToggleComp(value, AppI18n.observable(nameKey)) var t = new NamedToggleComp(value, AppI18n.observable(nameKey))
.visible(visible) .visible(visible)
.disable(disable); .disable(disable);
value.addListener((observable, oldValue, newValue) -> onChange.accept(newValue)); value.addListener((observable, oldValue, newValue) -> {
ThreadHelper.runAsync(() -> {
onChange.accept(newValue);
});
});
return t.createRegion(); return t.createRegion();
} }
} }

View file

@ -3,16 +3,19 @@ package io.xpipe.app.comp.base;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.storage.store.StoreEntryWrapper; import io.xpipe.app.comp.storage.store.StoreEntryWrapper;
import io.xpipe.app.fxcomps.SimpleComp; 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.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener; 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.binding.Bindings;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import lombok.Getter;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.javafx.StackedFontIcon; import org.kordamp.ikonli.javafx.StackedFontIcon;
@Getter
public class SystemStateComp extends SimpleComp { public class SystemStateComp extends SimpleComp {
@ -23,23 +26,21 @@ public class SystemStateComp extends SimpleComp {
public enum State { public enum State {
FAILURE, FAILURE,
SUCCESS, 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; 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 @Override
protected Region createSimple() { protected Region createSimple() {
var icon = PlatformThread.sync(Bindings.createStringBinding( 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(); : Comp.empty();
information.setGraphic(state.createRegion()); information.setGraphic(state.createRegion());
var summary = wrapper.summary();
var info = wrapper.getEntry().getProvider().informationString(wrapper);
SimpleChangeListener.apply(grid.hoverProperty(), val -> { SimpleChangeListener.apply(grid.hoverProperty(), val -> {
if (val && wrapper.getSummary().get() != null && wrapper.getEntry().getProvider().alwaysShowSummary()) { if (val && summary.getValue() != null && wrapper.getEntry().getProvider().alwaysShowSummary()) {
information.textProperty().bind(PlatformThread.sync(wrapper.getSummary())); information.textProperty().bind(PlatformThread.sync(summary));
} else { } 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); GridPane.setHalignment(storeIcon, HPos.CENTER);
} }
var customSize = content != null ? 300 : 0; var customSize = content != null ? 200 : 0;
var custom = new ColumnConstraints(0, customSize, customSize); var custom = new ColumnConstraints(0, customSize, customSize);
custom.setHalignment(HPos.RIGHT); custom.setHalignment(HPos.RIGHT);

View file

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

View file

@ -31,7 +31,7 @@ public class StoreCreationMenu {
host.textProperty().bind(AppI18n.observable("addHost")); host.textProperty().bind(AppI18n.observable("addHost"));
host.setOnAction(event -> { host.setOnAction(event -> {
GuiDsStoreCreator.showCreation(DataStoreProviders.byName("ssh").orElseThrow(), GuiDsStoreCreator.showCreation(DataStoreProviders.byName("ssh").orElseThrow(),
v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.HOST)); v -> DataStoreProvider.CreationCategory.HOST.equals(v.getCreationCategory()));
event.consume(); event.consume();
}); });
menu.getItems().add(host); menu.getItems().add(host);
@ -42,7 +42,7 @@ public class StoreCreationMenu {
shell.textProperty().bind(AppI18n.observable("addShell")); shell.textProperty().bind(AppI18n.observable("addShell"));
shell.setOnAction(event -> { shell.setOnAction(event -> {
GuiDsStoreCreator.showCreation(null, GuiDsStoreCreator.showCreation(null,
v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.SHELL)); v -> DataStoreProvider.CreationCategory.SHELL.equals(v.getCreationCategory()));
event.consume(); event.consume();
}); });
menu.getItems().add(shell); menu.getItems().add(shell);
@ -53,7 +53,7 @@ public class StoreCreationMenu {
cmd.textProperty().bind(AppI18n.observable("addCommand")); cmd.textProperty().bind(AppI18n.observable("addCommand"));
cmd.setOnAction(event -> { cmd.setOnAction(event -> {
GuiDsStoreCreator.showCreation(null, GuiDsStoreCreator.showCreation(null,
v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.COMMAND)); v -> DataStoreProvider.CreationCategory.COMMAND.equals(v.getCreationCategory()));
event.consume(); event.consume();
}); });
menu.getItems().add(cmd); menu.getItems().add(cmd);
@ -64,7 +64,7 @@ public class StoreCreationMenu {
db.textProperty().bind(AppI18n.observable("addDatabase")); db.textProperty().bind(AppI18n.observable("addDatabase"));
db.setOnAction(event -> { db.setOnAction(event -> {
GuiDsStoreCreator.showCreation(null, GuiDsStoreCreator.showCreation(null,
v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.DATABASE)); v -> DataStoreProvider.CreationCategory.DATABASE.equals(v.getCreationCategory()));
event.consume(); event.consume();
}); });
menu.getItems().add(db); menu.getItems().add(db);
@ -75,11 +75,22 @@ public class StoreCreationMenu {
tunnel.textProperty().bind(AppI18n.observable("addTunnel")); tunnel.textProperty().bind(AppI18n.observable("addTunnel"));
tunnel.setOnAction(event -> { tunnel.setOnAction(event -> {
GuiDsStoreCreator.showCreation(null, GuiDsStoreCreator.showCreation(null,
v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.TUNNEL)); v -> DataStoreProvider.CreationCategory.TUNNEL.equals(v.getCreationCategory()));
event.consume(); event.consume();
}); });
menu.getItems().add(tunnel); 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.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.update.XPipeDistributionType; import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.DesktopHelper; import io.xpipe.app.util.DesktopHelper;
import io.xpipe.app.util.DesktopShortcuts; import io.xpipe.app.util.DesktopShortcuts;
@ -70,7 +69,7 @@ public abstract class StoreEntryComp extends SimpleComp {
public static final ObservableDoubleValue INFO_NO_CONTENT_WIDTH = public static final ObservableDoubleValue INFO_NO_CONTENT_WIDTH =
App.getApp().getStage().widthProperty().divide(2.2); App.getApp().getStage().widthProperty().divide(2.2);
public static final ObservableDoubleValue INFO_WITH_CONTENT_WIDTH = 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 StoreEntryWrapper wrapper;
protected final Comp<?> content; protected final Comp<?> content;
@ -91,12 +90,7 @@ public abstract class StoreEntryComp extends SimpleComp {
button.setMaxWidth(3000); button.setMaxWidth(3000);
button.setFocusTraversable(true); button.setFocusTraversable(true);
button.accessibleTextProperty() button.accessibleTextProperty()
.bind(Bindings.createStringBinding( .bind(wrapper.nameProperty());
() -> {
return wrapper.getName();
},
wrapper.nameProperty()));
button.accessibleHelpProperty().bind(wrapper.getInformation());
button.setOnAction(event -> { button.setOnAction(event -> {
event.consume(); event.consume();
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
@ -117,7 +111,8 @@ public abstract class StoreEntryComp extends SimpleComp {
protected Label createInformation() { protected Label createInformation() {
var information = new Label(); var information = new Label();
information.setGraphicTextGap(7); information.setGraphicTextGap(7);
information.textProperty().bind(PlatformThread.sync(wrapper.getInformation())); information.textProperty().bind(wrapper.getEntry().getProvider() != null ?
PlatformThread.sync(wrapper.getEntry().getProvider().informationString(wrapper)) : new SimpleStringProperty());
information.getStyleClass().add("information"); information.getStyleClass().add("information");
AppFont.header(information); AppFont.header(information);
@ -131,14 +126,14 @@ public abstract class StoreEntryComp extends SimpleComp {
protected Label createSummary() { protected Label createSummary() {
var summary = new Label(); var summary = new Label();
summary.textProperty().bind(PlatformThread.sync(wrapper.getSummary())); summary.textProperty().bind(wrapper.summary());
summary.getStyleClass().add("summary"); summary.getStyleClass().add("summary");
AppFont.small(summary); AppFont.small(summary);
return summary; return summary;
} }
protected void applyState(Node node) { protected void applyState(Node node) {
SimpleChangeListener.apply(PlatformThread.sync(wrapper.getState()), val -> { SimpleChangeListener.apply(PlatformThread.sync(wrapper.getValidity()), val -> {
switch (val) { switch (val) {
case LOAD_FAILED -> { case LOAD_FAILED -> {
node.pseudoClassStateChanged(FAILED, true); node.pseudoClassStateChanged(FAILED, true);
@ -157,24 +152,7 @@ public abstract class StoreEntryComp extends SimpleComp {
} }
protected Comp<?> createName() { protected Comp<?> createName() {
// var filtered = BindingsHelper.filteredContentBinding( LabelComp name = new LabelComp(wrapper.nameProperty());
// 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()));
name.apply(struc -> struc.get().setTextOverrun(OverrunStyle.CENTER_ELLIPSIS)) name.apply(struc -> struc.get().setTextOverrun(OverrunStyle.CENTER_ELLIPSIS))
.apply(struc -> struc.get().setPadding(new Insets(5, 5, 5, 0))); .apply(struc -> struc.get().setPadding(new Insets(5, 5, 5, 0)));
name.apply(s -> AppFont.header(s.get())); name.apply(s -> AppFont.header(s.get()));
@ -183,14 +161,14 @@ public abstract class StoreEntryComp extends SimpleComp {
} }
protected Node createIcon(int w, int h) { protected Node createIcon(int w, int h) {
var img = wrapper.isDisabled() var img = wrapper.disabledProperty().get()
? "disabled_icon.png" ? "disabled_icon.png"
: wrapper.getEntry() : wrapper.getEntry()
.getProvider() .getProvider()
.getDisplayIconFileName(wrapper.getEntry().getStore()); .getDisplayIconFileName(wrapper.getEntry().getStore());
var imageComp = PrettyImageHelper.ofFixed(img, w, h); var imageComp = PrettyImageHelper.ofFixed(img, w, h);
var storeIcon = imageComp.createRegion(); var storeIcon = imageComp.createRegion();
if (wrapper.getState().getValue().isUsable()) { if (wrapper.getValidity().getValue().isUsable()) {
new FancyTooltipAugment<>(new SimpleStringProperty( new FancyTooltipAugment<>(new SimpleStringProperty(
wrapper.getEntry().getProvider().getDisplayName())) wrapper.getEntry().getProvider().getDisplayName()))
.augment(storeIcon); .augment(storeIcon);
@ -319,7 +297,7 @@ public abstract class StoreEntryComp extends SimpleComp {
sc.setOnAction(event -> { sc.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
DesktopShortcuts.create(url, 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); menu.getItems().add(sc);
@ -361,12 +339,6 @@ public abstract class StoreEntryComp extends SimpleComp {
}); });
contextMenu.getItems().add(move); 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")); var del = new MenuItem(AppI18n.get("remove"), new FontIcon("mdal-delete_outline"));
del.disableProperty().bind(wrapper.getDeletable().not()); del.disableProperty().bind(wrapper.getDeletable().not());
del.setOnAction(event -> wrapper.delete()); 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.DataStoreCategory;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.DataStore; import javafx.beans.binding.Bindings;
import io.xpipe.core.store.FixedHierarchyStore;
import javafx.beans.property.*; import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import lombok.Getter; import lombok.Getter;
import java.time.Duration; import java.time.Duration;
@ -29,16 +29,13 @@ public class StoreEntryWrapper {
private final BooleanProperty disabled = new SimpleBooleanProperty(); private final BooleanProperty disabled = new SimpleBooleanProperty();
private final BooleanProperty inRefresh = new SimpleBooleanProperty(); private final BooleanProperty inRefresh = new SimpleBooleanProperty();
private final BooleanProperty observing = new SimpleBooleanProperty(); private final BooleanProperty observing = new SimpleBooleanProperty();
private final Property<DataStoreEntry.State> state = new SimpleObjectProperty<>(); private final Property<DataStoreEntry.Validity> validity = new SimpleObjectProperty<>();
private final StringProperty information = new SimpleStringProperty();
private final StringProperty summary = new SimpleStringProperty();
private final Map<ActionProvider, BooleanProperty> actionProviders; private final Map<ActionProvider, BooleanProperty> actionProviders;
private final Property<ActionProvider.DefaultDataStoreCallSite<?>> defaultActionProvider; private final Property<ActionProvider.DefaultDataStoreCallSite<?>> defaultActionProvider;
private final BooleanProperty deletable = new SimpleBooleanProperty(); private final BooleanProperty deletable = new SimpleBooleanProperty();
private final BooleanProperty expanded = new SimpleBooleanProperty(); private final BooleanProperty expanded = new SimpleBooleanProperty();
private final Property<StoreCategoryWrapper> category = new SimpleObjectProperty<>(); private final Property<StoreCategoryWrapper> category = new SimpleObjectProperty<>();
private final Property<StoreEntryWrapper> displayParent = new SimpleObjectProperty<>(); private final Property<Object> persistentState = new SimpleObjectProperty<>();
private final IntegerProperty depth = new SimpleIntegerProperty();
public StoreEntryWrapper(DataStoreEntry entry) { public StoreEntryWrapper(DataStoreEntry entry) {
this.entry = entry; this.entry = entry;
@ -54,7 +51,8 @@ public class StoreEntryWrapper {
.getApplicableClass() .getApplicableClass()
.isAssignableFrom(entry.getStore().getClass()); .isAssignableFrom(entry.getStore().getClass());
}) })
.sorted(Comparator.comparing(actionProvider -> actionProvider.getDataStoreCallSite().isSystemAction())) .sorted(Comparator.comparing(
actionProvider -> actionProvider.getDataStoreCallSite().isSystemAction()))
.forEach(dataStoreActionProvider -> { .forEach(dataStoreActionProvider -> {
actionProviders.put(dataStoreActionProvider, new SimpleBooleanProperty(true)); actionProviders.put(dataStoreActionProvider, new SimpleBooleanProperty(true));
}); });
@ -74,7 +72,7 @@ public class StoreEntryWrapper {
return null; return null;
} }
var p = DataStorage.get().getParent(entry, true).orElse(null); var p = DataStorage.get().getDisplayParent(entry).orElse(null);
return StoreViewState.get().getAllEntries().stream() return StoreViewState.get().getAllEntries().stream()
.filter(storeEntryWrapper -> storeEntryWrapper.getEntry().equals(p)) .filter(storeEntryWrapper -> storeEntryWrapper.getEntry().equals(p))
.findFirst() .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() { private void setupListeners() {
name.addListener((c, o, n) -> { name.addListener((c, o, n) -> {
entry.setName(n); entry.setName(n);
@ -124,31 +140,15 @@ public class StoreEntryWrapper {
lastAccess.setValue(entry.getLastAccess()); lastAccess.setValue(entry.getLastAccess());
disabled.setValue(entry.isDisabled()); disabled.setValue(entry.isDisabled());
state.setValue(entry.getState()); validity.setValue(entry.getValidity());
expanded.setValue(entry.isExpanded()); expanded.setValue(entry.isExpanded());
observing.setValue(entry.isObserving()); observing.setValue(entry.isObserving());
information.setValue(entry.getInformation()); persistentState.setValue(entry.getStorePersistentState());
displayParent.setValue(computeDisplayParent());
inRefresh.setValue(entry.isInRefresh()); 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() deletable.setValue(entry.getConfiguration().isDeletable()
|| AppPrefs.get().developerDisableGuiRestrictions().getValue()); || AppPrefs.get().developerDisableGuiRestrictions().getValue());
var d = 0;
var c = this;
while ((c = c.getDisplayParent().getValue()) != null) {
d++;
}
depth.setValue(d);
actionProviders.keySet().forEach(dataStoreActionProvider -> { actionProviders.keySet().forEach(dataStoreActionProvider -> {
if (!isInStorage()) { if (!isInStorage()) {
actionProviders.get(dataStoreActionProvider).set(false); actionProviders.get(dataStoreActionProvider).set(false);
@ -156,7 +156,7 @@ public class StoreEntryWrapper {
return; return;
} }
if (!entry.getState().isUsable() if (!entry.getValidity().isUsable()
&& !dataStoreActionProvider && !dataStoreActionProvider
.getDataStoreCallSite() .getDataStoreCallSite()
.activeType() .activeType()
@ -194,51 +194,20 @@ public class StoreEntryWrapper {
}); });
} }
public void refreshIfNeeded() throws Exception { public void refreshChildren() {
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);
var hasChildren = DataStorage.get().refreshChildren(entry); var hasChildren = DataStorage.get().refreshChildren(entry);
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {
expanded.set(hasChildren); 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 { public void executeDefaultAction() throws Exception {
var found = getDefaultActionProvider().getValue(); var found = getDefaultActionProvider().getValue();
entry.updateLastUsed(); entry.updateLastUsed();
if (found != null) { if (found != null) {
refreshIfNeeded();
found.createAction(entry.getStore().asNeeded()).execute(); found.createAction(entry.getStore().asNeeded()).execute();
} else if (getEntry().getStore() instanceof FixedHierarchyStore) {
refreshWithChildrenAsync();
} else { } else {
entry.setExpanded(!entry.isExpanded());
} }
} }
@ -247,36 +216,13 @@ public class StoreEntryWrapper {
} }
public boolean shouldShow(String filter) { public boolean shouldShow(String filter) {
return filter == null return filter == null || nameProperty().getValue().toLowerCase().contains(filter.toLowerCase());
|| 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();
} }
public Property<String> nameProperty() { public Property<String> nameProperty() {
return name; 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() { public BooleanProperty disabledProperty() {
return disabled; 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.storage.DataStorage;
import io.xpipe.app.util.Hyperlinks; import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.ScanAlert; import io.xpipe.app.util.ScanAlert;
import io.xpipe.core.impl.LocalStore;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Orientation; import javafx.geometry.Orientation;
import javafx.geometry.Pos; import javafx.geometry.Pos;
@ -38,7 +37,7 @@ public class StoreIntroComp extends SimpleComp {
}); });
var scanButton = new Button(AppI18n.get("detectConnections"), new FontIcon("mdi2m-magnify")); var scanButton = new Button(AppI18n.get("detectConnections"), new FontIcon("mdi2m-magnify"));
scanButton.setOnAction(event -> ScanAlert.showAsync(DataStorage.get().getStoreEntry(new LocalStore()))); scanButton.setOnAction(event -> ScanAlert.showAsync(DataStorage.get().local()));
var scanPane = new StackPane(scanButton); var scanPane = new StackPane(scanButton);
scanPane.setAlignment(Pos.CENTER); scanPane.setAlignment(Pos.CENTER);

View file

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

View file

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

View file

@ -89,6 +89,15 @@ public class StoreViewState {
.orElseThrow(); .orElseThrow();
} }
public StoreCategoryWrapper getScriptsCategory() {
return categories.stream()
.filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.SCRIPTS_CATEGORY_UUID))
.findFirst()
.orElseThrow();
}
public static void init() { public static void init() {
new StoreViewState(); 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); var comboBox = new CustomComboBoxBuilder<>(provider, this::createGraphic, createDefaultNode(), v -> true);
comboBox.setAccessibleNames(dataStoreProvider -> dataStoreProvider.getDisplayName()); comboBox.setAccessibleNames(dataStoreProvider -> dataStoreProvider.getDisplayName());
var l = getProviders().stream() 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 l
.forEach(comboBox::add); .forEach(comboBox::add);
if (l.size() == 1) { if (l.size() == 1) {

View file

@ -109,7 +109,7 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
newE -> { newE -> {
ThreadHelper.runAsync(() -> { ThreadHelper.runAsync(() -> {
if (!DataStorage.get().getStoreEntries().contains(e)) { if (!DataStorage.get().getStoreEntries().contains(e)) {
DataStorage.get().addStoreEntry(newE); DataStorage.get().addStoreEntryIfNotPresent(newE);
} else { } else {
DataStorage.get().updateEntry(e, newE); DataStorage.get().updateEntry(e, newE);
} }
@ -126,7 +126,7 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
filter, filter,
e -> { e -> {
try { try {
DataStorage.get().addStoreEntry(e); DataStorage.get().addStoreEntryIfNotPresent(e);
if (e.getProvider().shouldHaveChildren()) { if (e.getProvider().shouldHaveChildren()) {
ScanAlert.showAsync(e); ScanAlert.showAsync(e);
} }
@ -260,7 +260,7 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
// return; // return;
// } // }
var d = n.guiDialog(store); var d = n.guiDialog(entry.getValue(), store);
var propVal = new SimpleValidator(); var propVal = new SimpleValidator();
var propR = createStoreProperties(d == null || d.getComp() == null ? null : d.getComp(), propVal); 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(() -> { ThreadHelper.runAsync(() -> {
try (var b = new BooleanScope(busy).start()) { try (var b = new BooleanScope(busy).start()) {
entry.getValue().refresh(true); entry.getValue().validateOrThrow();
finished.setValue(true); finished.setValue(true);
PlatformThread.runLaterIfNeeded(parent::next); PlatformThread.runLaterIfNeeded(parent::next);
} catch (Exception ex) { } 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.fxcomps.util.PlatformThread;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.update.XPipeDistributionType; import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import javafx.application.Application; import javafx.application.Application;
@ -29,6 +30,7 @@ public class App extends Application {
TrackEvent.info("Application launched"); TrackEvent.info("Application launched");
APP = this; APP = this;
stage = primaryStage; stage = primaryStage;
stage.opacityProperty().bind(AppPrefs.get().windowOpacity());
// Set dock icon explicitly on mac // Set dock icon explicitly on mac
// This is necessary in case XPipe was started through a script as it will have no icon otherwise // 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 notPresent.get();
} }
return (T) JacksonMapper.newMapper().treeToValue(tree, type); return (T) JacksonMapper.getDefault().treeToValue(tree, type);
} catch (Exception ex) { } catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omit().handle(); ErrorEvent.fromThrowable(ex).omit().handle();
FileUtils.deleteQuietly(path.toFile()); FileUtils.deleteQuietly(path.toFile());
@ -69,7 +69,7 @@ public class AppCache {
try { try {
FileUtils.forceMkdirParent(path.toFile()); FileUtils.forceMkdirParent(path.toFile());
var tree = JacksonMapper.newMapper().valueToTree(val); var tree = JacksonMapper.getDefault().valueToTree(val);
JsonConfigHelper.writeConfig(path, tree); JsonConfigHelper.writeConfig(path, tree);
} catch (Exception e) { } catch (Exception e) {
ErrorEvent.fromThrowable("Could not parse cached data for key " + key, e) ErrorEvent.fromThrowable("Could not parse cached data for key " + key, e)

View file

@ -36,18 +36,22 @@ public class AppExtensionManager {
public static void init(boolean loadProviders) { public static void init(boolean loadProviders) {
var load = INSTANCE == null || !INSTANCE.loadedProviders && loadProviders; var load = INSTANCE == null || !INSTANCE.loadedProviders && loadProviders;
if (INSTANCE == null) { if (INSTANCE == null) {
INSTANCE = new AppExtensionManager(loadProviders); INSTANCE = new AppExtensionManager(loadProviders);
INSTANCE.determineExtensionDirectories(); INSTANCE.determineExtensionDirectories();
INSTANCE.loadBaseExtension(); INSTANCE.loadBaseExtension();
INSTANCE.loadAllExtensions(); INSTANCE.loadAllExtensions();
} }
if (load) { if (load) {
INSTANCE.addNativeLibrariesToPath(); INSTANCE.addNativeLibrariesToPath();
XPipeServiceProviders.load(INSTANCE.extendedLayer); try {
MessageExchangeImpls.loadAll(); 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.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.prefs.SupportedLocale; import io.xpipe.app.prefs.SupportedLocale;
import io.xpipe.app.util.DynamicOptionsBuilder;
import io.xpipe.app.util.OptionsBuilder; import io.xpipe.app.util.OptionsBuilder;
import io.xpipe.app.util.Translatable; import io.xpipe.app.util.Translatable;
import io.xpipe.core.util.ModuleHelper; import io.xpipe.core.util.ModuleHelper;
@ -149,8 +148,7 @@ public class AppI18n {
|| caller.equals(FancyTooltipAugment.class) || caller.equals(FancyTooltipAugment.class)
|| caller.equals(PrefsChoiceValue.class) || caller.equals(PrefsChoiceValue.class)
|| caller.equals(Translatable.class) || caller.equals(Translatable.class)
|| caller.equals(OptionsBuilder.class) || caller.equals(OptionsBuilder.class)) {
|| caller.equals(DynamicOptionsBuilder.class)) {
continue; continue;
} }
var split = caller.getModule().getName().split("\\."); var split = caller.getModule().getName().split("\\.");

View file

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

View file

@ -14,7 +14,6 @@ import javafx.animation.KeyFrame;
import javafx.animation.KeyValue; import javafx.animation.KeyValue;
import javafx.animation.Timeline; import javafx.animation.Timeline;
import javafx.application.Application; import javafx.application.Application;
import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
@ -84,12 +83,6 @@ public class AppTheme {
AppPrefs.get().theme.addListener((c, o, n) -> { AppPrefs.get().theme.addListener((c, o, n) -> {
changeTheme(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) { 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.FeatureProvider;
import io.xpipe.app.util.FileBridge; import io.xpipe.app.util.FileBridge;
import io.xpipe.app.util.LockedSecretValue; import io.xpipe.app.util.LockedSecretValue;
import io.xpipe.core.impl.LocalStore; import io.xpipe.core.store.LocalStore;
import io.xpipe.core.util.JacksonMapper; import io.xpipe.core.util.JacksonMapper;
public class BaseMode extends OperationMode { 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.launcher.LauncherCommand;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.app.util.XPipeSession; 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.FailableRunnable;
import io.xpipe.core.util.XPipeDaemonMode; import io.xpipe.core.util.XPipeDaemonMode;
import io.xpipe.core.util.XPipeInstallation; import io.xpipe.core.util.XPipeInstallation;

View file

@ -1,7 +1,5 @@
package io.xpipe.app.exchange; 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.DataStorage;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.beacon.BeaconHandler; import io.xpipe.beacon.BeaconHandler;
@ -9,49 +7,11 @@ import io.xpipe.beacon.ClientException;
import io.xpipe.beacon.RequestMessage; import io.xpipe.beacon.RequestMessage;
import io.xpipe.beacon.ResponseMessage; import io.xpipe.beacon.ResponseMessage;
import io.xpipe.beacon.exchange.MessageExchange; import io.xpipe.beacon.exchange.MessageExchange;
import io.xpipe.core.dialog.Dialog; import io.xpipe.core.store.DataStoreId;
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 lombok.NonNull; import lombok.NonNull;
import java.util.Optional;
public interface MessageExchangeImpl<RQ extends RequestMessage, RS extends ResponseMessage> extends MessageExchange { 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 { default DataStoreEntry getStoreEntryByName(@NonNull String name, boolean acceptDisabled) throws ClientException {
var store = DataStorage.get().getStoreEntryIfPresent(name); var store = DataStorage.get().getStoreEntryIfPresent(name);
if (store.isEmpty()) { if (store.isEmpty()) {
@ -73,7 +33,7 @@ public interface MessageExchangeImpl<RQ extends RequestMessage, RS extends Respo
throw new ClientException( throw new ClientException(
String.format("Store %s is disabled", store.get().getName())); String.format("Store %s is disabled", store.get().getName()));
} }
if (!store.get().getState().isUsable() && !acceptUnusable) { if (!store.get().getValidity().isUsable() && !acceptUnusable) {
throw new ClientException( throw new ClientException(
String.format("Store %s is not completely configured", store.get().getName())); 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 @Override
public Response handleRequest(BeaconHandler handler, Request msg) throws Exception { public Response handleRequest(BeaconHandler handler, Request msg) throws Exception {
var store = getStoreEntryByName(msg.getName(), true); var store = getStoreEntryByName(msg.getName(), true);
var information = store.getInformation(); var summary = "";
var summary = store.getProvider().toSummaryString(store.getStore(), 100);
var dialog = store.getProvider().dialogForStore(store.getStore().asNeeded()); var dialog = store.getProvider().dialogForStore(store.getStore().asNeeded());
var config = new DialogMapper(dialog).handle(); var config = new DialogMapper(dialog).handle();
return Response.builder() return Response.builder()
.information(information)
.summary(summary) .summary(summary)
.internalStore(store.getStore()) .internalStore(store.getStore())
.provider(store.getProvider().getId()) .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.app.update.XPipeInstanceHelper;
import io.xpipe.beacon.BeaconHandler; import io.xpipe.beacon.BeaconHandler;
import io.xpipe.beacon.exchange.cli.InstanceExchange; import io.xpipe.beacon.exchange.cli.InstanceExchange;
import io.xpipe.core.impl.LocalStore; import io.xpipe.core.store.LocalStore;
public class InstanceExchangeImpl extends InstanceExchange public class InstanceExchangeImpl extends InstanceExchange
implements MessageExchangeImpl<InstanceExchange.Request, InstanceExchange.Response> { implements MessageExchangeImpl<InstanceExchange.Request, InstanceExchange.Response> {

View file

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

View file

@ -11,7 +11,7 @@ public class RenameStoreExchangeImpl extends RenameStoreExchange
@Override @Override
public Response handleRequest(BeaconHandler handler, Request msg) { public Response handleRequest(BeaconHandler handler, Request msg) {
var s = DataStorage.get().getStoreEntry(msg.getStoreName(), true); var s = DataStorage.get().getStoreEntry(msg.getStoreName(), true);
DataStorage.get().renameStoreEntry(s, msg.getNewName()); s.setName(msg.getNewName());
return Response.builder().build(); 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.exchange.MessageExchangeImpl;
import io.xpipe.app.ext.DataStoreProvider; import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.DataStoreProviders; import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.issue.ExceptionConverter;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.beacon.BeaconHandler; import io.xpipe.beacon.BeaconHandler;
import io.xpipe.beacon.ClientException; import io.xpipe.beacon.ClientException;
@ -51,7 +50,7 @@ public class StoreAddExchangeImpl extends StoreAddExchange
return; return;
} }
DataStorage.get().addStoreEntry(name.getValue(), store); DataStorage.get().addStoreIfNotPresent(name.getValue(), store);
}); });
return StoreAddExchange.Response.builder().config(config).build(); return StoreAddExchange.Response.builder().config(config).build();
@ -64,12 +63,6 @@ public class StoreAddExchangeImpl extends StoreAddExchange
return "Store is null"; return "Store is null";
} }
try {
store.validate();
} catch (Exception ex) {
return ExceptionConverter.convertMessage(ex);
}
return null; return null;
}) })
.map((String msg) -> { .map((String msg) -> {
@ -95,7 +88,6 @@ public class StoreAddExchangeImpl extends StoreAddExchange
DataStore s = creator.getResult(); DataStore s = creator.getResult();
String d = ""; String d = "";
try { try {
d = provider.queryInformationString(s, 50);
} catch (Exception ignored) { } catch (Exception ignored) {
} }
if (d != null) { if (d != null) {

View file

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

View file

@ -1,7 +1,6 @@
package io.xpipe.app.ext; package io.xpipe.app.ext;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.store.DataStore; import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.ModuleLayerLoader; import io.xpipe.core.util.ModuleLayerLoader;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
@ -91,10 +90,6 @@ public interface ActionProvider {
return null; return null;
} }
default DataSourceCallSite<?> getDataSourceCallSite() {
return null;
}
interface DefaultDataStoreCallSite<T extends DataStore> { interface DefaultDataStoreCallSite<T extends DataStore> {
Action createAction(T store); Action createAction(T store);
@ -142,27 +137,4 @@ public interface ActionProvider {
return ActiveType.ONLY_SHOW_IF_ENABLED; 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; package io.xpipe.app.ext;
import io.xpipe.app.comp.base.MarkdownComp; import io.xpipe.app.comp.base.MarkdownComp;
import io.xpipe.app.comp.base.SystemStateComp; import io.xpipe.app.comp.storage.store.StoreEntryComp;
import io.xpipe.app.comp.storage.store.*; 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.AppI18n;
import io.xpipe.app.core.AppImages; import io.xpipe.app.core.AppImages;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.dialog.Dialog; import io.xpipe.core.dialog.Dialog;
import io.xpipe.core.store.*; import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.JacksonizedValue; import io.xpipe.core.util.JacksonizedValue;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import java.util.List; import java.util.List;
@ -36,8 +39,6 @@ public interface DataStoreProvider {
} }
} }
default void preAdd(DataStore store) {}
default String browserDisplayName(DataStore store) { default String browserDisplayName(DataStore store) {
var e = DataStorage.get().getStoreDisplayName(store); var e = DataStorage.get().getStoreDisplayName(store);
return e.orElse("?"); return e.orElse("?");
@ -64,16 +65,7 @@ public interface DataStoreProvider {
} }
default Comp<?> stateDisplay(StoreEntryWrapper w) { default Comp<?> stateDisplay(StoreEntryWrapper w) {
var state = Bindings.createObjectBinding( return Comp.empty();
() -> {
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);
} }
default Comp<?> createInsightsComp(ObservableValue<DataStore> store) { default Comp<?> createInsightsComp(ObservableValue<DataStore> store) {
@ -97,19 +89,15 @@ public interface DataStoreProvider {
return null; return null;
} }
default DisplayCategory getDisplayCategory() { default CreationCategory getCreationCategory() {
return DisplayCategory.OTHER;
}
default DataStore getLogicalParent(DataStore store) {
return null; return null;
} }
default DataStore getDisplayParent(DataStore store) { default DataStoreEntry getDisplayParent(DataStoreEntry store) {
return getLogicalParent(store); return null;
} }
default GuiDialog guiDialog(Property<DataStore> store) { default GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {
return null; return null;
} }
@ -126,16 +114,12 @@ public interface DataStoreProvider {
return false; return false;
} }
default String queryInformationString(DataStore store, int length) throws Exception { default String summaryString(StoreEntryWrapper wrapper) {
return null; return null;
} }
default String queryInvalidInformationString(DataStore store, int length) { default ObservableValue<String> informationString(StoreEntryWrapper wrapper) {
return "Connection failed"; return new SimpleStringProperty(null);
}
default String toSummaryString(DataStore store, int length) {
return null;
} }
default String i18n(String key) { default String i18n(String key) {
@ -173,10 +157,6 @@ public interface DataStoreProvider {
return null; return null;
} }
default boolean requiresFrequentRefresh() {
return getStoreClasses().stream().anyMatch(aClass -> FixedHierarchyStore.class.isAssignableFrom(aClass));
}
default DataStore defaultStore() { default DataStore defaultStore() {
return null; return null;
} }
@ -189,22 +169,12 @@ public interface DataStoreProvider {
List<Class<?>> getStoreClasses(); List<Class<?>> getStoreClasses();
default boolean canManuallyCreate() { enum CreationCategory {
return true;
}
enum DataCategory {
STREAM,
SHELL,
DATABASE
}
enum DisplayCategory {
HOST, HOST,
DATABASE, DATABASE,
SHELL, SHELL,
COMMAND, COMMAND,
TUNNEL, TUNNEL,
OTHER SCRIPT
} }
} }

View file

@ -19,14 +19,6 @@ public class XPipeServiceProviders {
ProcessControlProvider.init(layer); ProcessControlProvider.init(layer);
TrackEvent.info("Loading extension providers ..."); 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); DataStoreProviders.init(layer);
for (DataStoreProvider p : DataStoreProviders.getAll()) { for (DataStoreProvider p : DataStoreProviders.getAll()) {
TrackEvent.trace("Loaded data store provider " + p.getId()); 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(); 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.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.storage.DataStorage; 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.app.util.DataStoreCategoryChoiceComp;
import io.xpipe.core.store.DataStore; import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.ShellStore; import io.xpipe.core.store.ShellStore;
@ -37,62 +39,59 @@ import java.util.function.Predicate;
public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp { public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
public static <T extends DataStore> DataStoreChoiceComp<T> other( 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); return new DataStoreChoiceComp<>(Mode.OTHER, null, selected, clazz, filter);
} }
public static DataStoreChoiceComp<ShellStore> proxy(Property<ShellStore> selected) { public static DataStoreChoiceComp<ShellStore> proxy(Property<DataStoreEntryRef<ShellStore>> selected) {
return new DataStoreChoiceComp<>(Mode.PROXY, null, selected, ShellStore.class, shellStore -> true); return new DataStoreChoiceComp<>(Mode.PROXY, null, selected, ShellStore.class, null);
} }
public static DataStoreChoiceComp<ShellStore> host(Property<ShellStore> selected) { public static DataStoreChoiceComp<ShellStore> host(Property<DataStoreEntryRef<ShellStore>> selected) {
return new DataStoreChoiceComp<>(Mode.HOST, null, selected, ShellStore.class, shellStore -> true); return new DataStoreChoiceComp<>(Mode.HOST, null, selected, ShellStore.class, null);
} }
public static DataStoreChoiceComp<ShellStore> environment(ShellStore self, Property<ShellStore> selected) { public static DataStoreChoiceComp<ShellStore> environment(
return new DataStoreChoiceComp<>(Mode.ENVIRONMENT, self, selected, ShellStore.class, shellStore -> true); 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) { public static DataStoreChoiceComp<ShellStore> proxy(
return new DataStoreChoiceComp<>(Mode.PROXY, self, selected, ShellStore.class, shellStore -> true); 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) { public static DataStoreChoiceComp<ShellStore> host(
return new DataStoreChoiceComp<>(Mode.HOST, self, selected, ShellStore.class, shellStore -> true); DataStoreEntry self, Property<DataStoreEntryRef<ShellStore>> selected) {
return new DataStoreChoiceComp<>(Mode.HOST, self, selected, ShellStore.class, null);
} }
public enum Mode { public enum Mode {
HOST, HOST,
ENVIRONMENT,
OTHER, OTHER,
PROXY PROXY
} }
private final Mode mode; private final Mode mode;
private final T self; private final DataStoreEntry self;
private final Property<T> selected; private final Property<DataStoreEntryRef<T>> selected;
private final Class<T> storeClass; private final Class<T> storeClass;
private final Predicate<T> applicableCheck; private final Predicate<DataStoreEntryRef<T>> applicableCheck;
private Popover popover; private Popover popover;
private Popover getPopover() { 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<>( var selectedCategory = new SimpleObjectProperty<>(
StoreViewState.get().getActiveCategory().getValue()); StoreViewState.get().getActiveCategory().getValue());
var filterText = new SimpleStringProperty(); var filterText = new SimpleStringProperty();
popover = new Popover(); popover = new Popover();
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> { Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
var e = storeEntryWrapper.getEntry(); var e = storeEntryWrapper.getEntry();
if (e.getStore() == self) { if (e.equals(self)) {
return false;
}
var store = e.getStore();
if (!(mode == Mode.ENVIRONMENT)
&& e.getProvider() != null
&& !e.getProvider().canHaveSubShells()) {
return false; return false;
} }
@ -100,15 +99,18 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
if (e.getStore() == null) { if (e.getStore() == null) {
return false; 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( var section = StoreSectionMiniComp.createList(
StoreSection.createTopLevel( StoreSection.createTopLevel(
StoreViewState.get().getAllEntries(), applicable, filterText, selectedCategory), StoreViewState.get().getAllEntries(), applicable, filterText, selectedCategory),
(s, comp) -> { (s, comp) -> {
comp.apply(struc -> struc.get().setOnAction(event -> { comp.apply(struc -> struc.get().setOnAction(event -> {
selected.setValue( selected.setValue(s.getWrapper().getEntry().ref());
s.getWrapper().getEntry().getStore().asNeeded());
popover.hide(); popover.hide();
event.consume(); event.consume();
})); }));
@ -126,7 +128,7 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
Platform.runLater(() -> { Platform.runLater(() -> {
Platform.runLater(() -> { Platform.runLater(() -> {
Platform.runLater(() -> { Platform.runLater(() -> {
struc.getText().requestFocus(); struc.getText().requestFocus();
}); });
}); });
}); });
@ -134,10 +136,13 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
}); });
var addButton = Comp.of(() -> { var addButton = Comp.of(() -> {
MenuButton m = new MenuButton(null, new FontIcon("mdi2p-plus-box-outline")); MenuButton m = new MenuButton(null, new FontIcon("mdi2p-plus-box-outline"));
StoreCreationMenu.addButtons(m); StoreCreationMenu.addButtons(m);
return 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)) var top = new HorizontalComp(List.of(category, filter.hgrow(), addButton))
.styleClass("top") .styleClass("top")
@ -182,16 +187,18 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
return new Label(name, imgView); return new Label(name, imgView);
} }
private String toName(DataStore store) { private String toName(DataStoreEntry entry) {
if (store == null) { if (entry == null) {
return 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 AppI18n.get("none");
} }
return DataStorage.get().getStoreDisplayName(store).orElse("?"); return entry.getName();
} }
@Override @Override
@ -199,7 +206,7 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
var button = new ButtonComp( var button = new ButtonComp(
Bindings.createStringBinding( Bindings.createStringBinding(
() -> { () -> {
return toName(selected.getValue()); return selected.getValue() != null ? toName(selected.getValue().getEntry()) : null;
}, },
selected), selected),
() -> {}); () -> {});
@ -207,21 +214,22 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
struc.get().setMaxWidth(2000); struc.get().setMaxWidth(2000);
struc.get().setAlignment(Pos.CENTER_LEFT); struc.get().setAlignment(Pos.CENTER_LEFT);
struc.get() struc.get()
.setGraphic(PrettyImageHelper.ofSvg(Bindings.createStringBinding( .setGraphic(PrettyImageHelper.ofSvg(
Bindings.createStringBinding(
() -> { () -> {
if (selected.getValue() == null) { if (selected.getValue() == null) {
return null; return null;
} }
return DataStorage.get() return selected.getValue()
.getStoreEntryIfPresent(selected.getValue()) .get()
.map(entry -> entry.getProvider() .getProvider()
.getDisplayIconFileName(selected.getValue())) .getDisplayIconFileName(selected.getValue()
.orElse(null); .getStore());
}, },
selected), selected),
16, 16,
16) 16)
.createRegion()); .createRegion());
struc.get().setOnAction(event -> { struc.get().setOnAction(event -> {
getPopover().show(struc.get()); getPopover().show(struc.get());

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

View file

@ -16,6 +16,10 @@ public class HorizontalComp extends Comp<CompStructure<HBox>> {
entries = List.copyOf(comps); entries = List.copyOf(comps);
} }
public Comp<CompStructure<HBox>> spacing(double spacing) {
return apply(struc -> struc.get().setSpacing(spacing));
}
@Override @Override
public CompStructure<HBox> createBase() { public CompStructure<HBox> createBase() {
HBox b = new HBox(); 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.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.prefs.AppPrefs; 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.binding.Bindings;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue; 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.core.AppImages;
import io.xpipe.app.fxcomps.Comp; 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.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
public class PrettyImageHelper { public class PrettyImageHelper {
public static Comp<?> ofFixedSquare(String img, int size) { public static Comp<?> ofFixedSquare(String img, int size) {
if (img.endsWith(".svg")) { if (img != null && img.endsWith(".svg")) {
var base = FileNames.getBaseName(img); var base = FileNames.getBaseName(img);
var renderedName = base + "-" + size + ".png"; var renderedName = base + "-" + size + ".png";
if (AppImages.hasNormalImage(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.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.prefs.AppPrefs; 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.binding.Bindings;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue; 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) { public static <T,U> ObservableValue<U> map(ObservableValue<T> observableValue, Function<? super T, ? extends U> mapper) {
return persist(Bindings.createObjectBinding(() -> { return persist(Bindings.createObjectBinding(() -> {
return mapper.apply(observableValue.getValue()); 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) { 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; 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) { public static <T, V> void bindMappedContent(ObservableList<T> l1, ObservableList<V> l2, Function<V, T> map) {
Runnable runnable = () -> { Runnable runnable = () -> {
setContent(l1, l2.stream().map(map).toList()); setContent(l1, l2.stream().map(map).toList());

View file

@ -18,6 +18,7 @@ import java.util.concurrent.CountDownLatch;
public class PlatformThread { public class PlatformThread {
public static Observable sync(Observable o) { public static Observable sync(Observable o) {
Objects.requireNonNull(o);
return new Observable() { return new Observable() {
private final Map<InvalidationListener, InvalidationListener> invListenerMap = new ConcurrentHashMap<>(); private final Map<InvalidationListener, InvalidationListener> invListenerMap = new ConcurrentHashMap<>();
@ -40,6 +41,7 @@ public class PlatformThread {
} }
public static <T> ObservableValue<T> sync(ObservableValue<T> ov) { public static <T> ObservableValue<T> sync(ObservableValue<T> ov) {
Objects.requireNonNull(ov);
ObservableValue<T> obs = new ObservableValue<>() { ObservableValue<T> obs = new ObservableValue<>() {
private final Map<ChangeListener<? super T>, ChangeListener<? super T>> changeListenerMap = 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) { public static <T> ObservableList<T> sync(ObservableList<T> ol) {
Objects.requireNonNull(ol);
ObservableList<T> obs = new ObservableList<>() { ObservableList<T> obs = new ObservableList<>() {
private final Map<ListChangeListener<? super T>, ListChangeListener<? super T>> listChangeListenerMap = 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.comp.base.TitledPaneComp;
import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.AppWindowHelper; import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.util.JfxHelper; import io.xpipe.app.util.JfxHelper;
import io.xpipe.app.util.LicenseException;
import io.xpipe.app.util.PlatformState; import io.xpipe.app.util.PlatformState;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.Property; import javafx.beans.property.Property;
@ -192,6 +194,26 @@ public class ErrorHandlerComp extends SimpleComp {
actionBox.getStyleClass().add("actions"); actionBox.getStyleClass().add("actions");
actionBox.setFillWidth(true); 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(); var custom = event.getCustomActions();
for (var c : custom) { for (var c : custom) {
var ac = createActionComp(c); 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.ext.ActionProvider;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.issue.TrackEvent;
import io.xpipe.core.store.ShellStore; import io.xpipe.app.storage.DataStorage;
import lombok.Getter; import lombok.Getter;
import lombok.Value; import lombok.Value;
@ -116,7 +116,7 @@ public abstract class LauncherInput {
var dir = Files.isDirectory(file) ? file : file.getParent(); var dir = Files.isDirectory(file) ? file : file.getParent();
AppLayoutModel.get().selectBrowser(); AppLayoutModel.get().selectBrowser();
BrowserModel.DEFAULT.openFileSystemAsync(null, ShellStore.createLocal(), dir.toString(), null); BrowserModel.DEFAULT.openFileSystemAsync( DataStorage.get().local().ref(), dir.toString(), null);
} }
@Override @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.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.*; 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.ModuleHelper;
import io.xpipe.core.util.SecretValue; import io.xpipe.core.util.SecretValue;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
@ -198,10 +198,6 @@ public class AppPrefs {
private final BooleanField automaticallyCheckForUpdatesField = private final BooleanField automaticallyCheckForUpdatesField =
BooleanField.ofBooleanType(automaticallyCheckForUpdates).render(() -> new CustomToggleControl()); 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); private final BooleanProperty confirmDeletions = typed(new SimpleBooleanProperty(true), Boolean.class);
// Storage // Storage
@ -309,10 +305,6 @@ public class AppPrefs {
return automaticallyCheckForUpdates; return automaticallyCheckForUpdates;
} }
public ReadOnlyBooleanProperty updateToPrereleases() {
return checkForPrereleases;
}
public ReadOnlyBooleanProperty confirmDeletions() { public ReadOnlyBooleanProperty confirmDeletions() {
return confirmDeletions; return confirmDeletions;
} }
@ -569,16 +561,15 @@ public class AppPrefs {
"appBehaviour", "appBehaviour",
Setting.of("startupBehaviour", startupBehaviourControl, startupBehaviour), Setting.of("startupBehaviour", startupBehaviourControl, startupBehaviour),
Setting.of("closeBehaviour", closeBehaviourControl, closeBehaviour)), Setting.of("closeBehaviour", closeBehaviourControl, closeBehaviour)),
Group.of(
"advanced",
Setting.of("developerMode", developerModeField, internalDeveloperMode)),
Group.of( Group.of(
"updates", "updates",
Setting.of( Setting.of(
"automaticallyUpdate", "automaticallyUpdate",
automaticallyCheckForUpdatesField, automaticallyCheckForUpdatesField,
automaticallyCheckForUpdates), automaticallyCheckForUpdates))),
Setting.of("updateToPrereleases", checkForPrereleasesField, checkForPrereleases)),
Group.of(
"advanced",
Setting.of("developerMode", developerModeField, internalDeveloperMode))),
new VaultCategory(this).create(), new VaultCategory(this).create(),
Category.of( Category.of(
"appearance", "appearance",

View file

@ -2,7 +2,7 @@ package io.xpipe.app.prefs;
import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.issue.ErrorEvent; 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.OsType;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialects; 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.MacOsPermissions;
import io.xpipe.app.util.ScriptHelper; import io.xpipe.app.util.ScriptHelper;
import io.xpipe.app.util.WindowsRegistry; import io.xpipe.app.util.WindowsRegistry;
import io.xpipe.core.impl.FileNames; import io.xpipe.core.store.FileNames;
import io.xpipe.core.impl.LocalStore; import io.xpipe.core.store.LocalStore;
import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;
@ -170,7 +170,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
@Override @Override
public void launch(String name, String file) throws Exception { public void launch(String name, String file) throws Exception {
try (ShellControl pc = LocalStore.getShell()) { 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); var toExecute = executable + " " + toCommand(name, file).build(pc);
// In order to fix this bug which also affects us: // In order to fix this bug which also affects us:
@ -590,7 +590,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
@Override @Override
public void launch(String name, String file) throws Exception { public void launch(String name, String file) throws Exception {
try (ShellControl pc = LocalStore.getShell()) { 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); var toExecute = executable + " " + toCommand(name, file).build(pc);
if (pc.getOsType().equals(OsType.WINDOWS)) { if (pc.getOsType().equals(OsType.WINDOWS)) {

View file

@ -51,7 +51,7 @@ public class JsonStorageHandler implements StorageHandler {
var id = getSaveId(breadcrumb); var id = getSaveId(breadcrumb);
var tree = object instanceof PrefsChoiceValue prefsChoiceValue var tree = object instanceof PrefsChoiceValue prefsChoiceValue
? new TextNode(prefsChoiceValue.getId()) ? new TextNode(prefsChoiceValue.getId())
: (object != null ? JacksonMapper.newMapper().valueToTree(object) : NullNode.getInstance()); : (object != null ? JacksonMapper.getDefault().valueToTree(object) : NullNode.getInstance());
setContent(id, tree); setContent(id, tree);
} }
@ -102,7 +102,7 @@ public class JsonStorageHandler implements StorageHandler {
try { try {
TrackEvent.debug("Loading preferences value for key " + breadcrumb + " from value " + tree); 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) { } catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omit().handle(); ErrorEvent.fromThrowable(ex).omit().handle();
return defaultObject; 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.fxcomps.impl.TextFieldComp;
import io.xpipe.app.util.TerminalHelper; import io.xpipe.app.util.TerminalHelper;
import io.xpipe.app.util.ThreadHelper; 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.CommandControl;
import io.xpipe.core.process.ShellDialects; import io.xpipe.core.process.ShellDialects;
import javafx.beans.property.Property; 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.ErrorEvent;
import io.xpipe.app.issue.UserReportComp; import io.xpipe.app.issue.UserReportComp;
import io.xpipe.app.util.*; import io.xpipe.app.util.*;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.process.OsType; 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 io.xpipe.core.util.XPipeInstallation;
import java.util.List; import java.util.List;
@ -58,7 +58,7 @@ public class TroubleshootComp extends Comp<CompStructure<?>> {
.addComp( .addComp(
new TileButtonComp("launchDebugMode", "launchDebugModeDescription", "mdmz-refresh", e -> { new TileButtonComp("launchDebugMode", "launchDebugModeDescription", "mdmz-refresh", e -> {
OperationMode.executeAfterShutdown(() -> { OperationMode.executeAfterShutdown(() -> {
try (var sc = ShellStore.createLocal() try (var sc = new LocalStore()
.control() .control()
.start()) { .start()) {
var script = FileNames.join( var script = FileNames.join(

View file

@ -1,17 +1,18 @@
package io.xpipe.app.storage; package io.xpipe.app.storage;
import io.xpipe.core.store.StatefulDataStore;
import io.xpipe.core.store.DataStore; import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.DataStoreState;
import io.xpipe.core.util.DataStateProvider; import io.xpipe.core.util.DataStateProvider;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.function.Supplier; import java.util.function.Supplier;
public class DataStateProviderImpl extends DataStateProvider { public class DataStateProviderImpl extends DataStateProvider {
@Override @Override
public void putState(DataStore store, String key, Object value) { public void setState(DataStore store, Object value) {
if (DataStorage.get() == null) { if (DataStorage.get() == null) {
return; return;
} }
@ -21,14 +22,12 @@ public class DataStateProviderImpl extends DataStateProvider {
return; return;
} }
var old = entry.get().getElementState().put(key, value); entry.get().setStorePersistentState(value);
if (!Objects.equals(old, value)) {
entry.get().simpleRefresh();
}
} }
@Override @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) { if (DataStorage.get() == null) {
return def.get(); return def.get();
} }
@ -38,7 +37,43 @@ public class DataStateProviderImpl extends DataStateProvider {
return def.get(); 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); 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.ext.DataStoreProviders;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.FixedHierarchyStore;
import io.xpipe.app.util.ThreadHelper; 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.DataStore;
import io.xpipe.core.store.DataStoreId;
import io.xpipe.core.store.FixedChildStore; 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 io.xpipe.core.util.FailableRunnable;
import javafx.util.Pair; import javafx.util.Pair;
import lombok.Getter; import lombok.Getter;
@ -18,12 +18,18 @@ import lombok.Setter;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream; import java.util.stream.Stream;
public abstract class DataStorage { public abstract class DataStorage {
public static final UUID ALL_CATEGORY_UUID = UUID.fromString("bfb0b51a-e7a3-4ce4-8878-8d4cb5828d6c"); 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 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 PERSIST_PROP = "io.xpipe.storage.persist";
private static final String IMMUTABLE_PROP = "io.xpipe.storage.immutable"; private static final String IMMUTABLE_PROP = "io.xpipe.storage.immutable";
@ -45,11 +51,23 @@ public abstract class DataStorage {
this.storeCategories = new CopyOnWriteArrayList<>(); 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(); return getStoreCategoryIfPresent(DEFAULT_CATEGORY_UUID).orElseThrow();
} }
public DataStoreCategory getAllCategory(){ public DataStoreCategory getAllCategory() {
return getStoreCategoryIfPresent(ALL_CATEGORY_UUID).orElseThrow(); return getStoreCategoryIfPresent(ALL_CATEGORY_UUID).orElseThrow();
} }
@ -69,8 +87,6 @@ public abstract class DataStorage {
INSTANCE = shouldPersist() ? new StandardStorage() : new ImpersistentStorage(); INSTANCE = shouldPersist() ? new StandardStorage() : new ImpersistentStorage();
INSTANCE.load(); INSTANCE.load();
INSTANCE.storeEntries.forEach(entry -> entry.simpleRefresh());
DataStoreProviders.getAll().forEach(dataStoreProvider -> { DataStoreProviders.getAll().forEach(dataStoreProvider -> {
try { try {
dataStoreProvider.storageInit(); dataStoreProvider.storageInit();
@ -94,10 +110,6 @@ public abstract class DataStorage {
} }
public boolean refreshChildren(DataStoreEntry e) { public boolean refreshChildren(DataStoreEntry e) {
return refreshChildren(e, null);
}
public boolean refreshChildren(DataStoreEntry e, DataStore newValue) {
if (!(e.getStore() instanceof FixedHierarchyStore)) { if (!(e.getStore() instanceof FixedHierarchyStore)) {
return false; return false;
} }
@ -105,7 +117,7 @@ public abstract class DataStorage {
e.setInRefresh(true); e.setInRefresh(true);
Map<String, FixedChildStore> newChildren; Map<String, FixedChildStore> newChildren;
try { try {
newChildren = ((FixedHierarchyStore) (newValue != null ? newValue : e.getStore())).listChildren(); newChildren = ((FixedHierarchyStore) (e.getStore())).listChildren(e);
e.setInRefresh(false); e.setInRefresh(false);
} catch (Exception ex) { } catch (Exception ex) {
e.setInRefresh(false); e.setInRefresh(false);
@ -113,112 +125,134 @@ public abstract class DataStorage {
return false; return false;
} }
synchronized (this) { var oldChildren = getStoreChildren(e, false);
var oldChildren = getStoreChildren(e, false, false); var toRemove = oldChildren.stream()
var toRemove = oldChildren.stream() .filter(entry -> newChildren.entrySet().stream()
.filter(entry -> newChildren.entrySet().stream() .noneMatch(
.noneMatch(nc -> nc -> nc.getValue().getFixedId() == ((FixedChildStore) entry.getStore()).getFixedId()))
nc.getValue().getFixedId() == ((FixedChildStore) entry.getStore()).getFixedId())) .toList();
.toList(); var toAdd = newChildren.entrySet().stream()
var toAdd = newChildren.entrySet().stream() .filter(entry -> oldChildren.stream()
.filter(entry -> oldChildren.stream() .noneMatch(oc -> ((FixedChildStore) oc.getStore()).getFixedId()
.noneMatch(oc -> ((FixedChildStore) oc.getStore()).getFixedId() == entry.getValue().getFixedId()))
== entry.getValue().getFixedId())) .toList();
.toList(); var toUpdate = oldChildren.stream()
var toUpdate = oldChildren.stream() .map(entry -> {
.map(entry -> { FixedChildStore found = newChildren.values().stream()
FixedChildStore found = newChildren.values().stream() .filter(nc -> nc.getFixedId() == ((FixedChildStore) entry.getStore()).getFixedId())
.filter(nc -> nc.getFixedId() == ((FixedChildStore) entry.getStore()).getFixedId()) .findFirst()
.findFirst() .orElse(null);
.orElse(null); return new Pair<>(entry, found);
return new Pair<>(entry, found); })
}) .filter(en -> en.getValue() != null)
.filter(en -> en.getValue() != null) .toList();
.toList();
if (newValue != null) { if (newChildren.size() > 0) {
e.setStoreInternal(newValue, false); e.setExpanded(true);
}
deleteWithChildren(toRemove.toArray(DataStoreEntry[]::new));
addStoreEntries(toAdd.stream()
.map(stringDataStoreEntry -> DataStoreEntry.createNew(
UUID.randomUUID(),
e.getCategoryUuid(),
stringDataStoreEntry.getKey(),
stringDataStoreEntry.getValue()))
.toArray(DataStoreEntry[]::new));
toUpdate.forEach(entry -> {
propagateUpdate(
() -> {
entry.getKey().setStoreInternal(entry.getValue(), false);
},
entry.getKey());
});
saveAsync();
return !newChildren.isEmpty();
} }
deleteWithChildren(toRemove.toArray(DataStoreEntry[]::new));
addStoreEntriesIfNotPresent(toAdd.stream()
.map(stringDataStoreEntry -> DataStoreEntry.createNew(
UUID.randomUUID(),
e.getCategoryUuid(),
stringDataStoreEntry.getKey(),
stringDataStoreEntry.getValue()))
.toArray(DataStoreEntry[]::new));
toUpdate.forEach(entry -> {
propagateUpdate(
() -> {
entry.getKey().setStoreInternal(entry.getValue(), false);
},
entry.getKey());
});
saveAsync();
return !newChildren.isEmpty();
} }
public void deleteWithChildren(DataStoreEntry... entries) { public void deleteWithChildren(DataStoreEntry... entries) {
var toDelete = Arrays.stream(entries) var toDelete = Arrays.stream(entries)
.flatMap(entry -> { .flatMap(entry -> {
// Reverse to delete deepest children first // Reverse to delete deepest children first
var ordered = getStoreChildren(entry, false, true); var ordered = getStoreChildren(entry, true);
Collections.reverse(ordered); Collections.reverse(ordered);
ordered.add(entry); ordered.add(entry);
return ordered.stream(); return ordered.stream();
}) })
.toList(); .toList();
synchronized (this) { toDelete.forEach(entry -> entry.finalizeEntry());
toDelete.forEach(entry -> entry.finalizeEntry()); this.storeEntries.removeAll(toDelete);
this.storeEntries.removeAll(toDelete); this.listeners.forEach(l -> l.onStoreRemove(toDelete.toArray(DataStoreEntry[]::new)));
this.listeners.forEach(l -> l.onStoreRemove(toDelete.toArray(DataStoreEntry[]::new))); refreshValidities(false);
}
saveAsync(); saveAsync();
} }
public void deleteChildren(DataStoreEntry e, boolean deep) { public void deleteChildren(DataStoreEntry e, boolean deep) {
// Reverse to delete deepest children first // Reverse to delete deepest children first
var ordered = getStoreChildren(e, true, deep); var ordered = getStoreChildren(e, deep);
Collections.reverse(ordered); Collections.reverse(ordered);
synchronized (this) { ordered.forEach(entry -> entry.finalizeEntry());
ordered.forEach(entry -> entry.finalizeEntry()); this.storeEntries.removeAll(ordered);
this.storeEntries.removeAll(ordered); this.listeners.forEach(l -> l.onStoreRemove(ordered.toArray(DataStoreEntry[]::new)));
this.listeners.forEach(l -> l.onStoreRemove(ordered.toArray(DataStoreEntry[]::new))); refreshValidities(false);
}
saveAsync(); saveAsync();
} }
public Optional<DataStoreEntry> getParent(DataStoreEntry entry, boolean display) { public Optional<DataStoreEntry> getDisplayParent(DataStoreEntry entry) {
if (entry.getState() == DataStoreEntry.State.LOAD_FAILED) { if (entry.getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {
return Optional.empty(); return Optional.empty();
} }
try { try {
var provider = entry.getProvider(); var provider = entry.getProvider();
var parent = return Optional.ofNullable(provider.getDisplayParent(entry)).filter(dataStoreEntry -> storeEntries.contains(dataStoreEntry));
display ? provider.getDisplayParent(entry.getStore()) : provider.getLogicalParent(entry.getStore());
return parent != null ? getStoreEntryIfPresent(parent) : Optional.empty();
} catch (Exception ex) { } catch (Exception ex) {
return Optional.empty(); return Optional.empty();
} }
} }
public synchronized List<DataStoreEntry> getStoreChildren(DataStoreEntry entry, boolean display, boolean deep) { public Optional<DataStoreEntry> findEntry(DataStore store) {
if (entry.getState() == DataStoreEntry.State.LOAD_FAILED) { 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(); 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 -> { .filter(other -> {
if (other.getState() == DataStoreEntry.State.LOAD_FAILED) { if (other.getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {
return false; return false;
} }
var parent = getParent(other, display); var parent = getDisplayParent(other);
return parent.isPresent() return parent.isPresent()
&& entry.getStore() && entry.getStore()
.getClass() .getClass()
@ -229,7 +263,7 @@ public abstract class DataStorage {
if (deep) { if (deep) {
for (DataStoreEntry dataStoreEntry : new ArrayList<>(children)) { 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"); return dir.resolve("categories");
} }
public synchronized List<DataStore> getUsableStores() { public List<DataStore> getUsableStores() {
return new ArrayList<>(getStoreEntries().stream() return new ArrayList<>(getStoreEntries().stream()
.filter(entry -> entry.getState().isUsable()) .filter(entry -> entry.getValidity().isUsable())
.map(DataStoreEntry::getStore) .map(DataStoreEntry::getStore)
.toList()); .toList());
} }
public synchronized void renameStoreEntry(DataStoreEntry entry, String name) { public DataStoreEntry getStoreEntry(@NonNull String name, boolean acceptDisabled) {
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) {
var entry = storeEntries.stream() var entry = storeEntries.stream()
.filter(n -> n.getName().equalsIgnoreCase(name)) .filter(n -> n.getName().equalsIgnoreCase(name))
.findFirst() .findFirst()
@ -286,15 +312,15 @@ public abstract class DataStorage {
public DataStoreId getId(DataStoreEntry entry) { public DataStoreId getId(DataStoreEntry entry) {
var names = new ArrayList<String>(); var names = new ArrayList<String>();
names.add(entry.getName()); names.add(entry.getName().replaceAll(":", "_"));
DataStoreEntry current = entry; DataStoreEntry current = entry;
while ((current = getParent(current, false).orElse(null)) != null) { while ((current = getDisplayParent(current).orElse(null)) != null) {
if (new LocalStore().equals(current.getStore())) { if (new LocalStore().equals(current.getStore())) {
break; break;
} }
names.add(0, current.getName()); names.add(0, current.getName().replaceAll(":", "_"));
} }
return DataStoreId.create(names.toArray(String[]::new)); return DataStoreId.create(names.toArray(String[]::new));
@ -313,7 +339,7 @@ public abstract class DataStorage {
var current = getStoreEntryIfPresent(id.getNames().get(0)); var current = getStoreEntryIfPresent(id.getNames().get(0));
if (current.isPresent()) { if (current.isPresent()) {
for (int i = 1; i < id.getNames().size(); i++) { 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; int finalI = i;
current = children.stream() current = children.stream()
.filter(dataStoreEntry -> dataStoreEntry .filter(dataStoreEntry -> dataStoreEntry
@ -344,6 +370,7 @@ public abstract class DataStorage {
public abstract boolean supportsSharing(); public abstract boolean supportsSharing();
public Optional<DataStoreCategory> getStoreCategoryIfPresent(UUID uuid) { public Optional<DataStoreCategory> getStoreCategoryIfPresent(UUID uuid) {
if (uuid == null) { if (uuid == null) {
return Optional.empty(); return Optional.empty();
@ -362,40 +389,30 @@ public abstract class DataStorage {
.findFirst(); .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) { public void updateEntry(DataStoreEntry entry, DataStoreEntry newEntry) {
var oldParent = DataStorage.get().getParent(entry, false); var oldParent = DataStorage.get().getDisplayParent(entry);
var newParent = DataStorage.get().getParent(newEntry, false); var newParent = DataStorage.get().getDisplayParent(newEntry);
var diffParent = Objects.equals(oldParent, newParent);
propagateUpdate( propagateUpdate(
() -> { () -> {
newEntry.finalizeEntry(); newEntry.finalizeEntry();
var children = getStoreChildren(entry, false, true); var children = getStoreChildren(entry, true);
if (!Objects.equals(oldParent, newParent)) { if (!diffParent) {
var toRemove = Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new); var toRemove = Stream.concat(Stream.of(entry), children.stream())
.toArray(DataStoreEntry[]::new);
listeners.forEach(storageListener -> storageListener.onStoreRemove(toRemove)); listeners.forEach(storageListener -> storageListener.onStoreRemove(toRemove));
} }
entry.applyChanges(newEntry); entry.applyChanges(newEntry);
entry.initializeEntry(); entry.initializeEntry();
if (!Objects.equals(oldParent, newParent)) { if (!diffParent) {
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)); listeners.forEach(storageListener -> storageListener.onStoreAdd(toAdd));
refreshValidities(true);
} }
}, },
entry); entry);
@ -404,104 +421,118 @@ public abstract class DataStorage {
public void updateCategory(DataStoreEntry entry, DataStoreCategory newCategory) { public void updateCategory(DataStoreEntry entry, DataStoreCategory newCategory) {
propagateUpdate( propagateUpdate(
() -> { () -> {
var children = getStoreChildren(entry, false, true); var children = getStoreChildren(entry, true);
var toRemove = Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new); var toRemove =
Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new);
listeners.forEach(storageListener -> storageListener.onStoreRemove(toRemove)); listeners.forEach(storageListener -> storageListener.onStoreRemove(toRemove));
entry.setCategoryUuid(newCategory.getUuid()); entry.setCategoryUuid(newCategory.getUuid());
children.forEach(child -> child.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)); listeners.forEach(storageListener -> storageListener.onStoreAdd(toAdd));
}, },
entry); 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 { <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(); runnable.run();
children.forEach(entry -> { 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) { public void addStoreCategory(@NonNull DataStoreCategory cat) {
synchronized (this) { cat.setDirectory(getCategoriesDir().resolve(cat.getUuid().toString()));
cat.setDirectory(getCategoriesDir().resolve(cat.getUuid().toString())); this.storeCategories.add(cat);
this.storeCategories.add(cat);
}
saveAsync(); saveAsync();
this.listeners.forEach(l -> l.onCategoryAdd(cat)); this.listeners.forEach(l -> l.onCategoryAdd(cat));
} }
public void addStoreEntry(@NonNull DataStoreEntry e) { public DataStoreEntry addStoreEntryIfNotPresent(@NonNull DataStoreEntry e) {
e.getProvider().preAdd(e.getStore()); if (storeEntries.contains(e)) {
synchronized (this) { return e;
e.setDirectory(getStoresDir().resolve(e.getUuid().toString()));
this.storeEntries.add(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(); saveAsync();
this.listeners.forEach(l -> l.onStoreAdd(e)); this.listeners.forEach(l -> l.onStoreAdd(e));
e.initializeEntry(); e.initializeEntry();
refreshValidities(true);
return e;
} }
public void addStoreEntries(@NonNull DataStoreEntry... es) { public DataStoreEntry getOrCreateNewEntry(String name, DataStore store) {
synchronized (this) { var found = findEntry(store);
for (DataStoreEntry e : es) {
e.getProvider().preAdd(e.getStore());
e.setDirectory(getStoresDir().resolve(e.getUuid().toString()));
this.storeEntries.add(e);
}
this.listeners.forEach(l -> l.onStoreAdd(es));
for (DataStoreEntry e : es) {
e.initializeEntry();
}
}
saveAsync();
}
public DataStoreEntry addStoreEntryIfNotPresent(@NonNull String name, DataStore store) {
var found = getStoreEntryIfPresent(store);
if (found.isPresent()) { if (found.isPresent()) {
return found.get(); return found.get();
} }
var e = DataStoreEntry.createNew(UUID.randomUUID(), selectedCategory.getUuid(), name, store); return DataStoreEntry.createNew(UUID.randomUUID(), selectedCategory.getUuid(), name, store);
addStoreEntry(e);
return e;
} }
public DataStoreEntry addStoreEntry(@NonNull String name, DataStore store) { public void addStoreEntriesIfNotPresent(@NonNull DataStoreEntry... es) {
for (DataStoreEntry e : es) {
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);
}
this.listeners.forEach(l -> l.onStoreAdd(es));
for (DataStoreEntry e : es) {
e.initializeEntry();
}
refreshValidities(true);
saveAsync();
}
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); var e = DataStoreEntry.createNew(UUID.randomUUID(), selectedCategory.getUuid(), name, store);
addStoreEntry(e); addStoreEntryIfNotPresent(e);
return e; return e;
} }
@ -510,31 +541,27 @@ public abstract class DataStorage {
return Optional.empty(); return Optional.empty();
} }
return DataStorage.get().getStoreEntries().stream() return findEntry(store).map(dataStoreEntry -> dataStoreEntry.getName());
.filter(entry -> !entry.isDisabled() && entry.getStore().equals(store))
.findFirst()
.map(entry -> entry.getName());
} }
public String getStoreBrowserDisplayName(DataStore store) { public String getStoreBrowserDisplayName(DataStoreEntry store) {
if (store == null) { if (store == null) {
return "?"; return "?";
} }
return getStoreEntryIfPresent(store).map(entry -> entry.getProvider().browserDisplayName(store)).orElse("?"); return store.getProvider().browserDisplayName(store.getStore());
} }
public void deleteStoreEntry(@NonNull DataStoreEntry store) { public void deleteStoreEntry(@NonNull DataStoreEntry store) {
propagateUpdate( propagateUpdate(
() -> { () -> {
store.finalizeEntry(); store.finalizeEntry();
synchronized (this) { this.storeEntries.remove(store);
this.storeEntries.remove(store);
}
}, },
store); store);
saveAsync();
this.listeners.forEach(l -> l.onStoreRemove(store)); this.listeners.forEach(l -> l.onStoreRemove(store));
refreshValidities(false);
saveAsync();
} }
public void deleteStoreCategory(@NonNull DataStoreCategory cat) { public void deleteStoreCategory(@NonNull DataStoreCategory cat) {
@ -566,10 +593,18 @@ public abstract class DataStorage {
public abstract void save(); 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(); 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() { public List<DataStoreEntry> getStoreEntries() {
return new ArrayList<>(storeEntries); return new ArrayList<>(storeEntries);
} }

View file

@ -1,36 +1,14 @@
package io.xpipe.app.storage; package io.xpipe.app.storage;
import com.fasterxml.jackson.databind.JsonNode; 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.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.store.DataStore; import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.JacksonMapper; 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 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) { public static DataStore storeFromNode(JsonNode node) {
node = replaceReferenceIds(node); var mapper = JacksonMapper.getDefault();
var mapper = JacksonMapper.newMapper();
try { try {
return mapper.treeToValue(node, DataStore.class); return mapper.treeToValue(node, DataStore.class);
} catch (Throwable e) { } catch (Throwable e) {
@ -38,74 +16,4 @@ public class DataStorageParser {
return null; 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