Small fixes and polishing

This commit is contained in:
crschnick 2023-10-18 04:17:34 +00:00
parent 7995d95b8d
commit 06d9c777fc
17 changed files with 228 additions and 166 deletions

View file

@ -80,6 +80,15 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
vbox.getChildren().add(b.createRegion());
}
{
var b = new IconButtonComp("mdi2c-comment-processing-outline", () -> Hyperlinks.open(Hyperlinks.ROADMAP))
.apply(new FancyTooltipAugment<>("roadmap"));
b.apply(struc -> {
AppFont.setSize(struc.get(), 2);
});
vbox.getChildren().add(b.createRegion());
}
{
var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableAlert.showIfNeeded())
.apply(new FancyTooltipAugment<>("updateAvailableTooltip"));

View file

@ -380,7 +380,9 @@ public abstract class StoreEntryComp extends SimpleComp {
}
var del = new MenuItem(AppI18n.get("remove"), new FontIcon("mdal-delete_outline"));
del.disableProperty().bind(wrapper.getDeletable().not());
del.disableProperty().bind(Bindings.createBooleanBinding(() -> {
return !wrapper.getDeletable().get() && !AppPrefs.get().developerDisableGuiRestrictions().get();
}, wrapper.getDeletable(), AppPrefs.get().developerDisableGuiRestrictions()));
del.setOnAction(event -> wrapper.delete());
contextMenu.getItems().add(del);

View file

@ -195,6 +195,11 @@ public class StoreEntryWrapper {
}
public void executeDefaultAction() throws Exception {
if (getEntry().getValidity() == DataStoreEntry.Validity.INCOMPLETE) {
editDialog();
return;
}
var found = getDefaultActionProvider().getValue();
entry.updateLastUsed();
if (found != null) {

View file

@ -6,6 +6,7 @@ import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.MarkdownHelper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
@ -110,9 +111,7 @@ public class AppGreetings {
temp,
MarkdownHelper.toHtml(Files.readString(file), UnaryOperator.identity()));
});
App.getApp()
.getHostServices()
.showDocument(temp.toUri().toString());
Hyperlinks.open(temp.toUri().toString());
} catch (IOException e) {
ErrorEvent.fromThrowable(e).handle();
}

View file

@ -36,6 +36,10 @@ public class AppTheme {
private static final PseudoClass PERFORMANCE = PseudoClass.getPseudoClass("performance");
public static void initThemeHandlers(Window stage) {
if (AppPrefs.get() == null) {
return;
}
SimpleChangeListener.apply(AppPrefs.get().theme, t -> {
Theme.ALL.forEach(theme -> stage.getScene().getRoot().getStyleClass().remove(theme.getCssId()));
if (t == null) {
@ -73,17 +77,20 @@ public class AppTheme {
t.apply();
TrackEvent.debug("Set theme " + t.getId() + " for scene");
detector.registerListener(dark -> {
PlatformThread.runLaterIfNeeded(() -> {
if (dark && !AppPrefs.get().theme.getValue().isDark()) {
AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());
}
// The gnome detector sometimes runs into issues, also it's not that important
if (!OsType.getLocal().equals(OsType.LINUX)) {
detector.registerListener(dark -> {
PlatformThread.runLaterIfNeeded(() -> {
if (dark && !AppPrefs.get().theme.getValue().isDark()) {
AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());
}
if (!dark && AppPrefs.get().theme.getValue().isDark()) {
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
}
if (!dark && AppPrefs.get().theme.getValue().isDark()) {
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
}
});
});
});
}
AppPrefs.get().theme.addListener((c, o, n) -> {
changeTheme(n);

View file

@ -24,6 +24,11 @@ public class Shortcuts {
public static <T extends Region> void addShortcut(T region, KeyCombination comb, Consumer<T> exec) {
var filter = new EventHandler<KeyEvent>() {
public void handle(KeyEvent ke) {
var target = ke.getTarget();
if (!region.isVisible() || !region.isManaged() || region.isDisabled()) {
return;
}
if (comb.match(ke)) {
exec.accept(region);
ke.consume();

View file

@ -10,6 +10,10 @@ import java.util.function.Consumer;
public class GuiErrorHandlerBase {
protected boolean startupGui(Consumer<Throwable> onFail) {
if (PlatformState.getCurrent() == PlatformState.EXITED) {
return false;
}
if (PlatformState.getCurrent() == PlatformState.NOT_INITIALIZED) {
try {
CountDownLatch latch = new CountDownLatch(1);

View file

@ -37,7 +37,9 @@ public abstract class DataStorage {
private static DataStorage INSTANCE;
protected final Path dir;
@Getter
protected final List<DataStoreCategory> storeCategories;
@Getter
protected final Set<DataStoreEntry> storeEntries;
@Getter
@ -159,6 +161,8 @@ public abstract class DataStorage {
listeners.forEach(storageListener -> storageListener.onStoreAdd(toAdd));
refreshValidities(true);
}
saveAsync();
}
public void updateCategory(DataStoreEntry entry, DataStoreCategory newCategory) {
@ -171,6 +175,7 @@ public abstract class DataStorage {
var toAdd = Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new);
listeners.forEach(storageListener -> storageListener.onStoreAdd(toAdd));
saveAsync();
}
public boolean refreshChildren(DataStoreEntry e) {
@ -228,6 +233,15 @@ public abstract class DataStorage {
return !newChildren.isEmpty();
}
public void deleteChildren(DataStoreEntry e) {
var c = getDeepStoreChildren(e);
c.forEach(entry -> entry.finalizeEntry());
this.storeEntries.removeAll(c);
this.listeners.forEach(l -> l.onStoreRemove(c.toArray(DataStoreEntry[]::new)));
refreshValidities(false);
saveAsync();
}
public void deleteWithChildren(DataStoreEntry... entries) {
var toDelete = Arrays.stream(entries)
.flatMap(entry -> {
@ -244,15 +258,130 @@ public abstract class DataStorage {
saveAsync();
}
public void deleteChildren(DataStoreEntry e) {
var c = getDeepStoreChildren(e);
c.forEach(entry -> entry.finalizeEntry());
this.storeEntries.removeAll(c);
this.listeners.forEach(l -> l.onStoreRemove(c.toArray(DataStoreEntry[]::new)));
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) {
cat.setDirectory(getCategoriesDir().resolve(cat.getUuid().toString()));
this.storeCategories.add(cat);
saveAsync();
this.listeners.forEach(l -> l.onCategoryAdd(cat));
}
public DataStoreEntry addStoreEntryIfNotPresent(@NonNull DataStoreEntry e) {
if (storeEntries.contains(e)) {
return e;
}
var byId = getStoreEntryIfPresent(e.getUuid()).orElse(null);
if (byId != null) {
return byId;
}
var syntheticParent = getSyntheticParent(e);
if (syntheticParent.isPresent()) {
addStoreEntryIfNotPresent(syntheticParent.get());
}
var displayParent = syntheticParent.or(() -> getDisplayParent(e));
if (displayParent.isPresent()) {
displayParent.get().setExpanded(true);
}
e.setDirectory(getStoresDir().resolve(e.getUuid().toString()));
this.storeEntries.add(e);
displayParent.ifPresent(p -> {
p.setChildrenCache(null);
});
saveAsync();
this.listeners.forEach(l -> l.onStoreAdd(e));
e.initializeEntry();
refreshValidities(true);
return e;
}
public void addStoreEntriesIfNotPresent(@NonNull DataStoreEntry... es) {
for (DataStoreEntry e : es) {
if (storeEntries.contains(e) || getStoreEntryIfPresent(e.getStore()).isPresent()) {
return;
}
var syntheticParent = getSyntheticParent(e);
if (syntheticParent.isPresent()) {
addStoreEntryIfNotPresent(syntheticParent.get());
}
var displayParent = syntheticParent.or(() -> getDisplayParent(e));
if (displayParent.isPresent()) {
displayParent.get().setExpanded(true);
}
e.setDirectory(getStoresDir().resolve(e.getUuid().toString()));
this.storeEntries.add(e);
displayParent.ifPresent(p -> {
p.setChildrenCache(null);
});
}
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 = getStoreEntryIfPresent(store);
if (f.isPresent()) {
return f.get();
}
var e = DataStoreEntry.createNew(UUID.randomUUID(), selectedCategory.getUuid(), name, store);
addStoreEntryIfNotPresent(e);
return e;
}
public void deleteStoreEntry(@NonNull DataStoreEntry store) {
store.finalizeEntry();
this.storeEntries.remove(store);
getDisplayParent(store).ifPresent(p -> p.setChildrenCache(null));
this.listeners.forEach(l -> l.onStoreRemove(store));
refreshValidities(false);
saveAsync();
}
public void deleteStoreCategory(@NonNull DataStoreCategory cat) {
if (cat.getUuid().equals(DEFAULT_CATEGORY_UUID) || cat.getUuid().equals(ALL_CONNECTIONS_CATEGORY_UUID)) {
return;
}
storeEntries.forEach(entry -> {
if (entry.getCategoryUuid().equals(cat.getUuid())) {
entry.setCategoryUuid(DEFAULT_CATEGORY_UUID);
}
});
storeCategories.remove(cat);
saveAsync();
this.listeners.forEach(l -> l.onCategoryRemove(cat));
}
// Get operations
public boolean isRootEntry(DataStoreEntry entry) {
var noParent = DataStorage.get().getDisplayParent(entry).isEmpty();
var diffParentCategory = DataStorage.get()
@ -312,8 +441,9 @@ public abstract class DataStorage {
public Set<DataStoreEntry> getDeepStoreChildren(DataStoreEntry entry) {
var set = new HashSet<DataStoreEntry>();
getStoreChildren(entry).forEach(entry1 -> {
set.addAll(getDeepStoreChildren(entry1));
getStoreChildren(entry).forEach(c -> {
set.add(c);
set.addAll(getDeepStoreChildren(c));
});
return set;
}
@ -435,112 +565,6 @@ public abstract class DataStorage {
.findFirst();
}
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) {
cat.setDirectory(getCategoriesDir().resolve(cat.getUuid().toString()));
this.storeCategories.add(cat);
saveAsync();
this.listeners.forEach(l -> l.onCategoryAdd(cat));
}
public DataStoreEntry addStoreEntryIfNotPresent(@NonNull DataStoreEntry e) {
if (storeEntries.contains(e)) {
return e;
}
var byId = getStoreEntryIfPresent(e.getUuid()).orElse(null);
if (byId != null) {
return byId;
}
var syntheticParent = getSyntheticParent(e);
if (syntheticParent.isPresent()) {
addStoreEntryIfNotPresent(syntheticParent.get());
}
var displayParent = syntheticParent.or(() -> getDisplayParent(e));
if (displayParent.isPresent()) {
displayParent.get().setExpanded(true);
}
e.setDirectory(getStoresDir().resolve(e.getUuid().toString()));
this.storeEntries.add(e);
displayParent.ifPresent(p -> {
p.setChildrenCache(null);
});
saveAsync();
this.listeners.forEach(l -> l.onStoreAdd(e));
e.initializeEntry();
refreshValidities(true);
return e;
}
public DataStoreEntry getOrCreateNewSyntheticEntry(DataStoreEntry parent, String name, DataStore store) {
var uuid = UuidHelper.generateFromObject(parent.getUuid(), name);
var found = getStoreEntryIfPresent(uuid);
if (found.isPresent()) {
return found.get();
}
return DataStoreEntry.createNew(uuid, parent.getCategoryUuid(), name, store);
}
public void addStoreEntriesIfNotPresent(@NonNull DataStoreEntry... es) {
for (DataStoreEntry e : es) {
if (storeEntries.contains(e) || getStoreEntryIfPresent(e.getStore()).isPresent()) {
return;
}
var syntheticParent = getSyntheticParent(e);
if (syntheticParent.isPresent()) {
addStoreEntryIfNotPresent(syntheticParent.get());
}
var displayParent = syntheticParent.or(() -> getDisplayParent(e));
if (displayParent.isPresent()) {
displayParent.get().setExpanded(true);
}
e.setDirectory(getStoresDir().resolve(e.getUuid().toString()));
this.storeEntries.add(e);
displayParent.ifPresent(p -> {
p.setChildrenCache(null);
});
}
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 = getStoreEntryIfPresent(store);
if (f.isPresent()) {
return f.get();
}
var e = DataStoreEntry.createNew(UUID.randomUUID(), selectedCategory.getUuid(), name, store);
addStoreEntryIfNotPresent(e);
return e;
}
public Optional<String> getStoreDisplayName(DataStore store) {
if (store == null) {
return Optional.empty();
@ -557,35 +581,20 @@ public abstract class DataStorage {
return store.getProvider().browserDisplayName(store.getStore());
}
public void deleteStoreEntry(@NonNull DataStoreEntry store) {
store.finalizeEntry();
this.storeEntries.remove(store);
getDisplayParent(store).ifPresent(p -> p.setChildrenCache(null));
this.listeners.forEach(l -> l.onStoreRemove(store));
refreshValidities(false);
saveAsync();
}
public void deleteStoreCategory(@NonNull DataStoreCategory cat) {
if (cat.getUuid().equals(DEFAULT_CATEGORY_UUID) || cat.getUuid().equals(ALL_CONNECTIONS_CATEGORY_UUID)) {
return;
}
storeEntries.forEach(entry -> {
if (entry.getCategoryUuid().equals(cat.getUuid())) {
entry.setCategoryUuid(DEFAULT_CATEGORY_UUID);
}
});
storeCategories.remove(cat);
saveAsync();
this.listeners.forEach(l -> l.onCategoryRemove(cat));
}
public Optional<DataStoreEntry> getStoreEntryIfPresent(UUID id) {
return storeEntries.stream().filter(e -> e.getUuid().equals(id)).findAny();
}
public DataStoreEntry getOrCreateNewSyntheticEntry(DataStoreEntry parent, String name, DataStore store) {
var uuid = UuidHelper.generateFromObject(parent.getUuid(), name);
var found = getStoreEntryIfPresent(uuid);
if (found.isPresent()) {
return found.get();
}
return DataStoreEntry.createNew(uuid, parent.getCategoryUuid(), name, store);
}
public DataStoreEntry getStoreEntry(UUID id) {
return getStoreEntryIfPresent(id).orElseThrow();
}
@ -594,11 +603,4 @@ public abstract class DataStorage {
return getStoreEntryIfPresent(LOCAL_ID).orElse(null);
}
public Set<DataStoreEntry> getStoreEntries() {
return storeEntries;
}
public List<DataStoreCategory> getStoreCategories() {
return storeCategories;
}
}

View file

@ -10,6 +10,10 @@ public class DataStoreEntryRef<T extends DataStore> {
@NonNull
DataStoreEntry entry;
public DataStoreEntryRef(@NonNull DataStoreEntry entry) {
this.entry = entry;
}
public void checkComplete() throws Exception {
getStore().checkComplete();
}

View file

@ -247,9 +247,7 @@ public class StandardStorage extends DataStorage {
ErrorEvent.fromThrowable(ex).terminal(true).build().handle();
}
{
var hasFixedLocal = storeEntries.stream().anyMatch(dataStoreEntry -> dataStoreEntry.getUuid().equals(LOCAL_ID));
// storeEntries.removeIf(dataStoreEntry -> !dataStoreEntry.getUuid().equals(LOCAL_ID) && dataStoreEntry.getStore() instanceof LocalStore);
if (!hasFixedLocal) {
var e = DataStoreEntry.createNew(
LOCAL_ID, DataStorage.DEFAULT_CATEGORY_UUID, "Local Machine", new LocalStore());
@ -264,7 +262,6 @@ public class StandardStorage extends DataStorage {
if (storeEntries.stream().noneMatch(entry -> entry.getColor() != null)) {
local.setColor(DataStoreColor.BLUE);
}
}
// Refresh to update state
storeEntries.forEach(dataStoreEntry -> dataStoreEntry.refresh());
@ -278,6 +275,12 @@ public class StandardStorage extends DataStorage {
refreshValidities(true);
// Save to apply changes
if (!hasFixedLocal) {
storeEntries.removeIf(dataStoreEntry -> !dataStoreEntry.getUuid().equals(LOCAL_ID) && dataStoreEntry.getStore() instanceof LocalStore);
save();
}
deleteLeftovers();
}

View file

@ -7,6 +7,7 @@ public class Hyperlinks {
public static final String WEBSITE = "https://xpipe.io";
public static final String DOCUMENTATION = "https://docs.xpipe.io";
public static final String GITHUB = "https://github.com/xpipe-io/xpipe";
public static final String ROADMAP = "https://xpipe.kampsite.co/";
public static final String PRIVACY = "https://github.com/xpipe-io/xpipe/blob/master/PRIVACY.md";
public static final String TOS = "https://github.com/xpipe-io/xpipe/blob/master/app/src/main/resources/io/xpipe/app/resources/misc/tos.md";
public static final String SECURITY = "https://github.com/xpipe-io/xpipe/blob/master/SECURITY.md";

View file

@ -44,7 +44,7 @@ project.ext {
productName = isStage ? 'XPipe PTB' : 'XPipe'
kebapProductName = isStage ? 'xpipe-ptb' : 'xpipe'
publisher = 'XPipe UG (haftungsbeschränkt)'
shortDescription = 'The shell connection hub and remote file browser for your entire infrastructure'
shortDescription = 'Your entire server infrastructure at your fingertips'
longDescription = 'XPipe is a new type of shell connection hub and remote file manager that allows you to access your entire sever infrastructure from your local machine. It works on top of your installed command-line programs that you normally use to connect and does not require any setup on your remote systems.'
website = 'https://xpipe.io'
sourceWebsite = 'https://github.com/xpipe-io/xpipe'

View file

@ -34,6 +34,10 @@ public interface ShellDialect {
return other.equals(this);
}
default ShellDialect getDumbReplacementDialect() {
return this;
}
String getCatchAllVariable();
CommandControl queryVersion(ShellControl shellControl);

View file

@ -13,6 +13,7 @@ import lombok.extern.jackson.Jacksonized;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.stream.Collectors;
@SuperBuilder
@Getter
@ -29,9 +30,18 @@ public class SimpleScriptStore extends ScriptStore {
}
private String assemble(ShellControl shellControl, ExecutionType type) {
var targetType = type == ExecutionType.TERMINAL_ONLY ? shellControl.getTargetTerminalShellDialect() : shellControl.getShellDialect();
if ((executionType == type || executionType == ExecutionType.BOTH) && minimumDialect.isCompatibleTo(targetType)) {
var script = ScriptHelper.createExecScript(targetType, shellControl, commands);
var targetType = type == ExecutionType.TERMINAL_ONLY
? shellControl.getTargetTerminalShellDialect()
: shellControl.getShellDialect();
if ((executionType == type || executionType == ExecutionType.BOTH)
&& minimumDialect.isCompatibleTo(targetType)) {
var shebang = commands.startsWith("#");
// Fix new lines and shebang
var fixedCommands = commands.lines()
.skip(shebang ? 1 : 0)
.collect(Collectors.joining(
shellControl.getShellDialect().getNewLine().getNewLineString()));
var script = ScriptHelper.createExecScript(targetType, shellControl, fixedCommands);
return targetType.sourceScriptCommand(shellControl, script);
}

View file

@ -145,7 +145,7 @@ public class SimpleScriptStoreProvider implements DataStoreProvider {
.nonNull()
.name("scriptContents")
.description("scriptContentsDescription")
.longDescription("proc:environmentScript")
.longDescription("base:script")
.addComp(
new IntegratedTextAreaComp(commandProp, false, "commands", Bindings.createStringBinding(() -> {
return dialect.getValue() != null

View file

@ -6,6 +6,8 @@ Only afterward will a separate connection be made in the actual terminal.
The file browser for example entirely uses the dumb background mode to handle its operations, so if you want your script environment to apply to the file browser session, it should run in the dumb mode.
If you want the script to be run when you open the connection in a terminal, then choose the terminal mode.
### Blocking commands
Blocking commands that require user input can freeze the shell process when XPipe starts it up internally first in the background.

View file

@ -0,0 +1,5 @@
## Init script
The contents of the script to run. You can choose to either edit this in place or use the external edit button in the top right corner to launch an external text editor.
You don't have to specify a shebang line for shells that support it, one is added automatically with the appropriate shell type.