This commit is contained in:
crschnick 2024-01-11 14:55:09 +00:00
parent 974da59fc8
commit 830d62db6c
97 changed files with 538 additions and 232 deletions

View file

@ -115,7 +115,6 @@ Alternatively, you can also use your favorite package manager (if supported):
- [choco](https://community.chocolatey.org/packages/xpipe): `choco install xpipe`
- [AUR package](https://aur.archlinux.org/packages/xpipe): `yay -S xpipe`
- [Homebrew](https://github.com/xpipe-io/homebrew-tap): `brew install --cask xpipe-io/tap/xpipe`
- [nixpkg](https://github.com/xpipe-io/nixpkg): You can install XPipe by following the linked repository instructions
## Open source model

View file

@ -81,7 +81,7 @@ public class BrowserWelcomeComp extends SimpleComp {
var listBox = new ListBoxViewComp<>(list, list, e -> {
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
var graphic = entry.get().getProvider().getDisplayIconFileName(entry.get().getStore());
var view = PrettyImageHelper.ofFixedSize(graphic, 50, 40);
var view = PrettyImageHelper.ofFixedSquare(graphic, 45);
view.padding(new Insets(2, 8, 2, 8));
var content =
JfxHelper.createNamedEntry(DataStorage.get().getStoreDisplayName(entry.get()), e.getPath(), graphic);

View file

@ -53,7 +53,7 @@ public class FileSystemHelper {
}
var shell = model.getFileSystem().getShell();
if (shell.isEmpty() || !shell.get().isRunning()) {
if (shell.isEmpty()) {
return path;
}

View file

@ -131,9 +131,6 @@ public final class OpenFileSystemModel {
return Optional.empty();
}
// Start shell in case we exited
getFileSystem().getShell().orElseThrow().start();
// Fix common issues with paths
var adjustedPath = FileSystemHelper.adjustPath(this, path);
if (!Objects.equals(path, adjustedPath)) {

View file

@ -29,8 +29,6 @@ public interface LeafAction extends BrowserAction {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.execute(model.getBusy(), () -> {
// Start shell in case we exited
model.getFileSystem().getShell().orElseThrow().start();
execute(model, selected);
});
});
@ -66,8 +64,6 @@ public interface LeafAction extends BrowserAction {
mi.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.execute(model.getBusy(), () -> {
// Start shell in case we exited
model.getFileSystem().getShell().orElseThrow().start();
execute(model, selected);
});
});

View file

@ -51,7 +51,7 @@ public class OsLogoComp extends SimpleComp {
var hide = BindingsHelper.map(img, s -> s != null);
return new StackComp(List.of(
new SystemStateComp(state).hide(hide),
PrettyImageHelper.ofRasterized(img, 24, 24).visible(hide)))
PrettyImageHelper.ofSvg(img, 24, 24).visible(hide)))
.createRegion();
}
@ -66,9 +66,8 @@ public class OsLogoComp extends SimpleComp {
if (ICONS.isEmpty()) {
AppResources.with(AppResources.XPIPE_MODULE, "img/os", file -> {
try (var list = Files.list(file)) {
list.filter(path -> path.toString().endsWith(".svg") && !path.toString().endsWith(LINUX_DEFAULT))
.map(path -> FileNames.getFileName(path.toString())).forEach(path -> {
var base = FileNames.getBaseName(path).replace("-dark", "") + "-24.png";
list.filter(path -> !path.toString().endsWith(LINUX_DEFAULT)).map(path -> FileNames.getFileName(path.toString())).forEach(path -> {
var base = FileNames.getBaseName(path).replace("-dark", "") + ".svg";
ICONS.put(FileNames.getBaseName(base).split("-")[0], "os/" + base);
});
}

View file

@ -56,7 +56,7 @@ public class DenseStoreEntryComp extends StoreEntryComp {
}, grid.widthProperty()));
if (showIcon) {
var storeIcon = createIcon(30, 24);
var storeIcon = createIcon(30, 25);
grid.getColumnConstraints().add(new ColumnConstraints(46));
grid.add(storeIcon, 0, 0);
GridPane.setHalignment(storeIcon, HPos.CENTER);

View file

@ -1,16 +1,17 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.CustomComboBoxBuilder;
import io.xpipe.app.util.JfxHelper;
import javafx.beans.property.Property;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.Region;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@ -18,23 +19,27 @@ import lombok.experimental.FieldDefaults;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.Supplier;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@AllArgsConstructor
public class DataStoreProviderChoiceComp extends Comp<CompStructure<ComboBox<DataStoreProvider>>> {
public class DsStoreProviderChoiceComp extends Comp<CompStructure<ComboBox<Node>>> {
Predicate<DataStoreProvider> filter;
Property<DataStoreProvider> provider;
boolean staticDisplay;
private Region createDefaultNode() {
return JfxHelper.createNamedEntry(
AppI18n.get("selectType"), AppI18n.get("selectTypeDescription"), "connection_icon.svg");
}
private List<DataStoreProvider> getProviders() {
return DataStoreProviders.getAll().stream().filter(filter).toList();
}
private Region createGraphic(DataStoreProvider provider) {
if (provider == null) {
return null;
return createDefaultNode();
}
var graphic = provider.getDisplayIconFileName(null);
@ -42,40 +47,20 @@ public class DataStoreProviderChoiceComp extends Comp<CompStructure<ComboBox<Dat
}
@Override
public CompStructure<ComboBox<DataStoreProvider>> createBase() {
Supplier<ListCell<DataStoreProvider>> cellFactory = () -> new ListCell<>() {
@Override
protected void updateItem(DataStoreProvider item, boolean empty) {
super.updateItem(item, empty);
setGraphic(createGraphic(item));
setAccessibleText(item != null ? item.getDisplayName() : null);
setAccessibleHelp(item != null ? item.getDisplayDescription() : null);
}
};
var cb = new ComboBox<DataStoreProvider>();
cb.setCellFactory(param -> {
return cellFactory.get();
});
cb.setButtonCell(cellFactory.get());
public CompStructure<ComboBox<Node>> createBase() {
var comboBox = new CustomComboBoxBuilder<>(provider, this::createGraphic, createDefaultNode(), v -> true);
comboBox.setAccessibleNames(dataStoreProvider -> dataStoreProvider.getDisplayName());
var l = getProviders().stream()
.filter(p -> AppPrefs.get().developerShowHiddenProviders().get() || p.getCreationCategory() != null || staticDisplay)
.toList();
l.forEach(dataStoreProvider -> cb.getItems().add(dataStoreProvider));
if (provider.getValue() == null) {
provider.setValue(l.getFirst());
.filter(p -> AppPrefs.get().developerShowHiddenProviders().get() || p.getCreationCategory() != null || staticDisplay).toList();
l
.forEach(comboBox::add);
if (l.size() == 1) {
provider.setValue(l.get(0));
}
cb.setValue(provider.getValue());
provider.bind(cb.valueProperty());
ComboBox<Node> cb = comboBox.build();
cb.getStyleClass().add("data-source-type");
cb.getStyleClass().add("choice-comp");
cb.setAccessibleText("Choose connection type");
cb.setOnKeyPressed(event -> {
if (!event.getCode().equals(KeyCode.ENTER)) {
return;
}
cb.show();
event.consume();
});
return new SimpleCompStructure<>(cb);
}
}

View file

@ -276,7 +276,7 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
var layout = new BorderPane();
layout.getStyleClass().add("store-creator");
layout.setPadding(new Insets(20));
var providerChoice = new DataStoreProviderChoiceComp(filter, provider, staticDisplay);
var providerChoice = new DsStoreProviderChoiceComp(filter, provider, staticDisplay);
if (staticDisplay) {
providerChoice.apply(struc -> struc.get().setDisable(true));
}

View file

@ -20,7 +20,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
grid.setHgap(7);
grid.setVgap(0);
var storeIcon = createIcon(50, 40);
var storeIcon = createIcon(50, 39);
grid.add(storeIcon, 0, 0, 1, 2);
grid.getColumnConstraints().add(new ColumnConstraints(66));

View file

@ -3,9 +3,7 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.util.ScanAlert;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
@ -14,6 +12,7 @@ import org.kordamp.ikonli.javafx.FontIcon;
public class StoreCreationMenu {
public static void addButtons(MenuButton menu) {
{
var automatically = new MenuItem();
automatically.setGraphic(new FontIcon("mdi2e-eye-plus-outline"));
automatically.textProperty().bind(AppI18n.observable("addAutomatically"));
@ -23,62 +22,74 @@ public class StoreCreationMenu {
});
menu.getItems().add(automatically);
menu.getItems().add(new SeparatorMenuItem());
menu.getItems().add(category("addHost", "mdi2h-home-plus",
DataStoreProvider.CreationCategory.HOST, "ssh"));
menu.getItems().add(category("addShell", "mdi2t-text-box-multiple",
DataStoreProvider.CreationCategory.SHELL, null));
menu.getItems().add(category("addScript", "mdi2s-script-text-outline",
DataStoreProvider.CreationCategory.SCRIPT, "script"));
menu.getItems().add(category("addCommand", "mdi2c-code-greater-than",
DataStoreProvider.CreationCategory.COMMAND, "cmd"));
menu.getItems().add(category("addTunnel", "mdi2v-vector-polyline-plus",
DataStoreProvider.CreationCategory.TUNNEL, null));
menu.getItems().add(category("addDatabase", "mdi2d-database-plus",
DataStoreProvider.CreationCategory.DATABASE, null));
}
private static MenuItem category(String name, String graphic, DataStoreProvider.CreationCategory category, String defaultProvider) {
var sub = DataStoreProviders.getAll().stream().filter(dataStoreProvider -> category.equals(dataStoreProvider.getCreationCategory())).toList();
if (sub.size() < 2) {
var item = new MenuItem();
item.setGraphic(new FontIcon(graphic));
item.textProperty().bind(AppI18n.observable(name));
item.setOnAction(event -> {
GuiDsStoreCreator.showCreation(defaultProvider != null ? DataStoreProviders.byName(defaultProvider).orElseThrow() : null,
v -> category.equals(v.getCreationCategory()));
{
var host = new MenuItem();
host.setGraphic(new FontIcon("mdi2h-home-plus"));
host.textProperty().bind(AppI18n.observable("addHost"));
host.setOnAction(event -> {
GuiDsStoreCreator.showCreation(DataStoreProviders.byName("ssh").orElseThrow(),
v -> DataStoreProvider.CreationCategory.HOST.equals(v.getCreationCategory()));
event.consume();
});
return item;
menu.getItems().add(host);
}
var menu = new Menu();
menu.setGraphic(new FontIcon(graphic));
menu.textProperty().bind(AppI18n.observable(name));
menu.setOnAction(event -> {
if (event.getTarget() != menu) {
return;
}
GuiDsStoreCreator.showCreation(defaultProvider != null ? DataStoreProviders.byName(defaultProvider).orElseThrow() : null,
v -> category.equals(v.getCreationCategory()));
{
var shell = new MenuItem();
shell.setGraphic(new FontIcon("mdi2t-text-box-multiple"));
shell.textProperty().bind(AppI18n.observable("addShell"));
shell.setOnAction(event -> {
GuiDsStoreCreator.showCreation(null,
v -> DataStoreProvider.CreationCategory.SHELL.equals(v.getCreationCategory()));
event.consume();
});
sub.forEach(dataStoreProvider -> {
var item = new MenuItem(dataStoreProvider.getDisplayName());
item.setGraphic(PrettyImageHelper.ofFixedSmallSquare(dataStoreProvider.getDisplayIconFileName(null)).createRegion());
item.setOnAction(event -> {
GuiDsStoreCreator.showCreation(dataStoreProvider,
v -> category.equals(v.getCreationCategory()));
menu.getItems().add(shell);
}
{
var cmd = new MenuItem();
cmd.setGraphic(new FontIcon("mdi2c-code-greater-than"));
cmd.textProperty().bind(AppI18n.observable("addCommand"));
cmd.setOnAction(event -> {
GuiDsStoreCreator.showCreation(DataStoreProviders.byName("cmd").orElseThrow(),
v -> DataStoreProvider.CreationCategory.COMMAND.equals(v.getCreationCategory()));
event.consume();
});
menu.getItems().add(item);
menu.getItems().add(cmd);
}
{
var db = new MenuItem();
db.setGraphic(new FontIcon("mdi2d-database-plus"));
db.textProperty().bind(AppI18n.observable("addDatabase"));
db.setOnAction(event -> {
GuiDsStoreCreator.showCreation(null,
v -> DataStoreProvider.CreationCategory.DATABASE.equals(v.getCreationCategory()));
event.consume();
});
return menu;
menu.getItems().add(db);
}
{
var tunnel = new MenuItem();
tunnel.setGraphic(new FontIcon("mdi2v-vector-polyline-plus"));
tunnel.textProperty().bind(AppI18n.observable("addTunnel"));
tunnel.setOnAction(event -> {
GuiDsStoreCreator.showCreation(null,
v -> DataStoreProvider.CreationCategory.TUNNEL.equals(v.getCreationCategory()));
event.consume();
});
menu.getItems().add(tunnel);
}
{
var script = new MenuItem();
script.setGraphic(new FontIcon("mdi2s-script-text-outline"));
script.textProperty().bind(AppI18n.observable("addScript"));
script.setOnAction(event -> {
GuiDsStoreCreator.showCreation(DataStoreProviders.byName("script").orElseThrow(),
v -> DataStoreProvider.CreationCategory.SCRIPT.equals(v.getCreationCategory()));
event.consume();
});
menu.getItems().add(script);
}
}
}

View file

@ -160,7 +160,7 @@ public abstract class StoreEntryComp extends SimpleComp {
: wrapper.getEntry()
.getProvider()
.getDisplayIconFileName(wrapper.getEntry().getStore());
var imageComp = PrettyImageHelper.ofFixedSize(img, w, h);
var imageComp = PrettyImageHelper.ofFixed(img, w, h);
var storeIcon = imageComp.createRegion();
if (wrapper.getValidity().getValue().isUsable()) {
new FancyTooltipAugment<>(new SimpleStringProperty(

View file

@ -124,10 +124,6 @@ public class AppTheme {
}
PlatformThread.runLaterIfNeeded(() -> {
if (AppMainWindow.getInstance() == null) {
return;
}
var window = AppMainWindow.getInstance().getStage();
var scene = window.getScene();
Pane root = (Pane) scene.getRoot();

View file

@ -269,11 +269,6 @@ public class AppWindowHelper {
changed = true;
}
// This should not happen but on weird Linux systems nothing is impossible
if (w < 0 || h < 0) {
return Optional.empty();
}
return changed ? Optional.of(new Rectangle2D(x, y, w, h)) : Optional.empty();
}

View file

@ -3,6 +3,7 @@ package io.xpipe.app.ext;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.store.DataStore;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.ServiceLoader;
@ -16,6 +17,7 @@ public class DataStoreProviders {
if (ALL == null) {
ALL = ServiceLoader.load(layer, DataStoreProvider.class).stream()
.map(ServiceLoader.Provider::get)
.sorted(Comparator.comparing(DataStoreProvider::getId))
.collect(Collectors.toList());
ALL.removeIf(p -> {
try {

View file

@ -0,0 +1,46 @@
package io.xpipe.app.fxcomps.impl;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.util.CustomComboBoxBuilder;
import io.xpipe.core.charsetter.StreamCharset;
import javafx.beans.property.Property;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
public class CharsetChoiceComp extends SimpleComp {
private final Property<StreamCharset> charset;
public CharsetChoiceComp(Property<StreamCharset> charset) {
this.charset = charset;
}
@Override
protected Region createSimple() {
var builder = new CustomComboBoxBuilder<>(
charset,
streamCharset -> {
return new Label(streamCharset.getCharset().displayName()
+ (streamCharset.hasByteOrderMark() ? " (BOM)" : ""));
},
new Label(AppI18n.get("app.none")),
null);
builder.setAccessibleNames(streamCharset -> streamCharset.getNames().get(0));
builder.addFilter((charset, filter) -> {
return charset.getCharset().displayName().contains(filter);
});
builder.addHeader(AppI18n.get("app.common"));
for (var e : StreamCharset.COMMON) {
builder.add(e);
}
builder.addHeader(AppI18n.get("app.other"));
for (var e : StreamCharset.RARE) {
builder.add(e);
}
var comboBox = builder.build();
comboBox.setVisibleRowCount(16);
return comboBox;
}
}

View file

@ -13,6 +13,7 @@ import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.DataStoreCategoryChoiceComp;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.ShellStore;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
@ -105,7 +106,18 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
selectedCategory).styleClass(Styles.LEFT_PILL);
var filter = new FilterComp(filterText)
.styleClass(Styles.CENTER_PILL)
.hgrow();
.hgrow()
.apply(struc -> {
popover.setOnShowing(event -> {
Platform.runLater(() -> {
Platform.runLater(() -> {
Platform.runLater(() -> {
struc.getText().requestFocus();
});
});
});
});
});
var addButton = Comp.of(() -> {
MenuButton m = new MenuButton(null, new FontIcon("mdi2p-plus-box-outline"));
@ -120,16 +132,7 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
var top = new HorizontalComp(List.of(category, filter.hgrow(), addButton))
.styleClass("top")
.apply(struc -> struc.get().setFillHeight(true))
.apply(struc -> {
// Ugly solution to focus the text field
// Somehow this does not work through the normal on shown listeners
struc.get().getChildren().get(0).focusedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
((StackPane) struc.get().getChildren().get(1)).getChildren().get(1).requestFocus();
}
});
})
.createStructure().get();
.createRegion();
var r = section.vgrow().createRegion();
var content = new VBox(top, r);
content.setFillWidth(true);

View file

@ -8,6 +8,7 @@ import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.FileSystemStore;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.layout.HBox;
@ -17,12 +18,14 @@ import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class ContextualFileReferenceChoiceComp extends SimpleComp {
public class FileReferenceChoiceComp extends SimpleComp {
private final boolean hideFileSystem;
private final Property<DataStoreEntryRef<? extends FileSystemStore>> fileSystem;
private final Property<String> filePath;
public <T extends FileSystemStore> ContextualFileReferenceChoiceComp(ObservableValue<DataStoreEntryRef<T>> fileSystem, Property<String> filePath) {
public <T extends FileSystemStore> FileReferenceChoiceComp(ObservableValue<DataStoreEntryRef<T>> fileSystem, Property<String> filePath) {
this.hideFileSystem = true;
this.fileSystem = new SimpleObjectProperty<>();
SimpleChangeListener.apply(fileSystem, val -> {
this.fileSystem.setValue(val);
@ -30,15 +33,27 @@ public class ContextualFileReferenceChoiceComp extends SimpleComp {
this.filePath = filePath;
}
public FileReferenceChoiceComp(boolean hideFileSystem, Property<DataStoreEntryRef<? extends FileSystemStore>> fileSystem, Property<String> filePath) {
this.hideFileSystem = hideFileSystem;
this.fileSystem = fileSystem != null ? fileSystem : new SimpleObjectProperty<>();
this.filePath = filePath;
}
@Override
protected Region createSimple() {
var fileSystemChoiceComp =
new FileSystemStoreChoiceComp(fileSystem).grow(false, true).styleClass(Styles.LEFT_PILL);
if (hideFileSystem) {
fileSystemChoiceComp.hide(new SimpleBooleanProperty(true));
}
var fileNameComp = new TextFieldComp(filePath)
.apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS))
.styleClass(Styles.LEFT_PILL)
.styleClass(hideFileSystem ? Styles.LEFT_PILL : Styles.CENTER_PILL)
.grow(false, true);
var fileBrowseButton = new ButtonComp(null, new FontIcon("mdi2f-folder-open-outline"), () -> {
StandaloneFileBrowser.openSingleFile(() -> fileSystem.getValue(), fileStore -> {
StandaloneFileBrowser.openSingleFile(() -> hideFileSystem ? fileSystem.getValue() : null, fileStore -> {
if (fileStore == null) {
filePath.setValue(null);
fileSystem.setValue(null);
@ -51,7 +66,7 @@ public class ContextualFileReferenceChoiceComp extends SimpleComp {
.styleClass(Styles.RIGHT_PILL)
.grow(false, true);
var layout = new HorizontalComp(List.of(fileNameComp, fileBrowseButton))
var layout = new HorizontalComp(List.of(fileSystemChoiceComp, fileNameComp, fileBrowseButton))
.apply(struc -> struc.get().setFillHeight(true));
layout.apply(struc -> {

View file

@ -0,0 +1,53 @@
package io.xpipe.app.fxcomps.impl;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.CustomComboBoxBuilder;
import io.xpipe.core.store.FileSystemStore;
import javafx.beans.property.Property;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
public class FileSystemStoreChoiceComp extends SimpleComp {
private final Property<DataStoreEntryRef<? extends FileSystemStore>> selected;
public FileSystemStoreChoiceComp(Property<DataStoreEntryRef<? extends FileSystemStore>> selected) {
this.selected = selected;
}
private static String getName(DataStoreEntryRef<? extends FileSystemStore> store) {
return store.get().getName();
}
private Region createGraphic(DataStoreEntryRef<? extends FileSystemStore> s) {
var provider = s.get().getProvider();
var img = PrettyImageHelper.ofFixedSquare(provider.getDisplayIconFileName(s.getStore()), 16);
return new Label(getName(s), img.createRegion());
}
private Region createDisplayGraphic(DataStoreEntryRef<? extends FileSystemStore> s) {
var provider = s.get().getProvider();
var img = PrettyImageHelper.ofFixedSquare(provider.getDisplayIconFileName(s.getStore()), 16);
return new Label(null, img.createRegion());
}
@Override
protected Region createSimple() {
var comboBox = new CustomComboBoxBuilder<>(selected, this::createGraphic, null, v -> true);
comboBox.setAccessibleNames(FileSystemStoreChoiceComp::getName);
comboBox.setSelectedDisplay(this::createDisplayGraphic);
DataStorage.get().getUsableEntries().stream()
.filter(e -> e.getStore() instanceof FileSystemStore)
.map(DataStoreEntry::<FileSystemStore>ref)
.forEach(comboBox::add);
ComboBox<Node> cb = comboBox.build();
cb.getStyleClass().add("choice-comp");
cb.setMaxWidth(45);
return cb;
}
}

View file

@ -4,7 +4,6 @@ import io.xpipe.app.core.AppImages;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.store.FileNames;
import javafx.beans.binding.Bindings;
@ -79,7 +78,6 @@ public class PrettyImageComp extends SimpleComp {
} else if (AppImages.hasNormalImage(image.getValue().replace("-dark", ""))) {
return AppImages.image(image.getValue().replace("-dark", ""));
} else {
TrackEvent.withWarn("Image file not found").tag("file",image.getValue()).handle();
return null;
}
},

View file

@ -6,49 +6,37 @@ import io.xpipe.core.store.FileNames;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import java.util.Optional;
public class PrettyImageHelper {
public static Optional<Comp<?>> rasterizedIfExists(String img, int width, int height) {
public static Comp<?> ofFixedSquare(String img, int size) {
if (img != null && img.endsWith(".svg")) {
var base = FileNames.getBaseName(img);
var renderedName = base + "-" + height + ".png";
if (AppImages.hasNormalImage(base + "-" + height + ".png")) {
return Optional.of(new PrettyImageComp(new SimpleStringProperty(renderedName), width, height));
}
}
return Optional.empty();
}
public static Comp<?> ofFixedSquare(String img, int size) {
return ofFixedSize(img, size, size);
}
public static Comp<?> ofFixedSize(String img, int w, int h) {
if (img == null) {
return new PrettyImageComp(new SimpleStringProperty(null), w, h);
}
var rasterized = rasterizedIfExists(img, w, h);
if (rasterized.isPresent()) {
return rasterized.get();
var renderedName = base + "-" + size + ".png";
if (AppImages.hasNormalImage(base + "-" + size + ".png")) {
return new PrettyImageComp(new SimpleStringProperty(renderedName), size, size);
} else {
return new PrettySvgComp(new SimpleStringProperty(img), size, size);
}
}
return new PrettyImageComp(new SimpleStringProperty(img), size, size);
}
public static Comp<?> ofFixed(String img, int w, int h) {
if (w == h) {
return ofFixedSquare(img, w);
}
return img.endsWith(".svg") ? new PrettySvgComp(new SimpleStringProperty(img), w, h) : new PrettyImageComp(new SimpleStringProperty(img), w, h);
}
}
public static Comp<?> ofSvg(ObservableValue<String> img, int w, int h) {
return new PrettySvgComp(img, w, h);
}
public static Comp<?> ofRasterized(ObservableValue<String> img, int w, int h) {
return new PrettyImageComp(img, w, h);
}
public static Comp<?> ofFixedSmallSquare(String img) {
return ofFixedSize(img, 16, 16);
return ofFixed(img, 16, 16);
}
}

View file

@ -1,7 +1,6 @@
package io.xpipe.app.fxcomps.impl;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.binding.Bindings;
@ -9,24 +8,11 @@ import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.control.TextArea;
import javafx.scene.layout.AnchorPane;
import lombok.Builder;
import lombok.Value;
import javafx.scene.layout.Region;
import java.util.Objects;
public class TextAreaComp extends Comp<TextAreaComp.Structure> {
@Value
@Builder
public static class Structure implements CompStructure<AnchorPane> {
AnchorPane pane;
TextArea textArea;
@Override
public AnchorPane get() {
return pane;
}
}
public class TextAreaComp extends SimpleComp {
private final Property<String> currentValue;
private final Property<String> lastAppliedValue;
@ -46,7 +32,7 @@ public class TextAreaComp extends Comp<TextAreaComp.Structure> {
}
@Override
public Structure createBase() {
protected Region createSimple() {
var text = new TextArea(currentValue.getValue() != null ? currentValue.getValue() : null);
text.setPrefRowCount(5);
text.textProperty().addListener((c, o, n) -> {
@ -71,12 +57,6 @@ public class TextAreaComp extends Comp<TextAreaComp.Structure> {
}
});
var anchorPane = new AnchorPane(text);
AnchorPane.setBottomAnchor(text, 0.0);
AnchorPane.setTopAnchor(text, 0.0);
AnchorPane.setLeftAnchor(text, 0.0);
AnchorPane.setRightAnchor(text, 0.0);
if (lazy) {
var isEqual = Bindings.createBooleanBinding(
() -> Objects.equals(lastAppliedValue.getValue(), currentValue.getValue()),
@ -85,14 +65,16 @@ public class TextAreaComp extends Comp<TextAreaComp.Structure> {
var button = new IconButtonComp("mdi2c-checkbox-marked-outline")
.hide(isEqual)
.createRegion();
anchorPane.getChildren().add(button);
var anchorPane = new AnchorPane(text, button);
AnchorPane.setBottomAnchor(button, 10.0);
AnchorPane.setRightAnchor(button, 10.0);
text.prefWidthProperty().bind(anchorPane.widthProperty());
text.prefHeightProperty().bind(anchorPane.heightProperty());
return anchorPane;
}
return new Structure(anchorPane, text);
return text;
}
}

View file

@ -304,11 +304,9 @@ public class BindingsHelper {
if (target.size() - 1 == newList.size() && targetSet.containsAll(newSet)) {
var l = new HashSet<>(targetSet);
l.removeAll(newSet);
if (l.size() > 0) {
target.remove(l.iterator().next());
return;
}
}
// Other cases are more difficult
target.setAll(newList);

View file

@ -180,7 +180,7 @@ public class SentryErrorHandler implements ErrorHandler {
s.setTag("terminal", Boolean.toString(ee.isTerminal()));
s.setTag("omitted", Boolean.toString(ee.isOmitted()));
s.setTag("diagnostics", Boolean.toString(ee.isShouldSendDiagnostics()));
s.setTag("logs", Boolean.toString(ee.isShouldSendDiagnostics() && !ee.getAttachments().isEmpty()));
s.setTag("logs", Boolean.toString(!ee.getAttachments().isEmpty()));
var exMessage = ee.getThrowable() != null ? ee.getThrowable().getMessage() : null;
if (ee.getDescription() != null && !ee.getDescription().equals(exMessage) && ee.isShouldSendDiagnostics()) {

View file

@ -9,7 +9,6 @@ import lombok.NonNull;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.Optional;
import java.util.regex.Matcher;
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@ -37,24 +36,6 @@ public class ContextualFileReference {
}
}
public static Optional<ContextualFileReference> parseIfInDataDirectory(String s) {
var cf = of(s);
if (cf.serialize().contains("<DATA>")) {
return Optional.of(cf);
} else {
return Optional.empty();
}
}
public static Optional<String> resolveIfInDataDirectory(ShellControl shellControl, String s) {
if (s.contains("<DATA>")) {
var cf = of(s);
return Optional.of(cf.toFilePath(shellControl));
} else {
return Optional.empty();
}
}
public static ContextualFileReference of(String s) {
if (s == null) {
return null;

View file

@ -0,0 +1,266 @@
package io.xpipe.app.util;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.impl.FilterComp;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.Separator;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.util.*;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Predicate;
public class CustomComboBoxBuilder<T> {
private final Property<T> selected;
private final Function<T, Node> nodeFunction;
private ObservableValue<String> emptyAccessibilityName = AppI18n.observable("none");
private Function<T, String> accessibleNameFunction;
private Function<T, Node> selectedDisplayNodeFunction;
private final Map<Node, T> nodeMap = new HashMap<>();
private final Map<Node, Runnable> actionsMap = new HashMap<>();
private final List<Node> nodes = new ArrayList<>();
private final Set<Node> disabledNodes = new HashSet<>();
private final Node emptyNode;
private final Predicate<T> veto;
private final Property<String> filterString = new SimpleStringProperty();
private final List<T> filterable = new ArrayList<>();
private BiPredicate<T, String> filterPredicate;
private Node filterNode;
private Function<T, Node> unknownNode;
public CustomComboBoxBuilder(
Property<T> selected, Function<T, Node> nodeFunction, Node emptyNode, Predicate<T> veto) {
this.selected = selected;
this.nodeFunction = nodeFunction;
this.selectedDisplayNodeFunction = nodeFunction;
this.emptyNode = emptyNode;
this.veto = veto;
}
public void setSelectedDisplay(Function<T, Node> nodeFunction) {
selectedDisplayNodeFunction = nodeFunction;
}
public void setAccessibleNames(Function<T, String> function) {
accessibleNameFunction = function;
}
public void setEmptyAccessibilityName(ObservableValue<String> n) {
emptyAccessibilityName = n;
}
public void addAction(Node node, Runnable run) {
nodes.add(node);
actionsMap.put(node, run);
}
public void disable(Node node) {
disabledNodes.add(node);
}
public void setUnknownNode(Function<T, Node> node) {
unknownNode = node;
}
public Node add(T val) {
var node = nodeFunction.apply(val);
nodeMap.put(node, val);
nodes.add(node);
if (filterPredicate != null) {
filterable.add(val);
}
return node;
}
public void addSeparator() {
var sep = new Separator(Orientation.HORIZONTAL);
nodes.add(sep);
disabledNodes.add(sep);
}
public void addHeader(String name) {
var spacer = new Region();
spacer.setPrefHeight(10);
var header = new Label(name);
header.setAlignment(Pos.CENTER);
var v = new VBox(spacer, header, new Separator(Orientation.HORIZONTAL));
v.setAccessibleText(name);
v.setAlignment(Pos.CENTER);
nodes.add(v);
disabledNodes.add(v);
}
public void addFilter(BiPredicate<T, String> filterPredicate) {
this.filterPredicate = filterPredicate;
var spacer = new Region();
spacer.setPrefHeight(10);
var header = new FilterComp(filterString).createStructure();
var v = new VBox(header.get());
v.setAlignment(Pos.CENTER);
nodes.add(v);
filterNode = header.getText();
}
public ComboBox<Node> build() {
var cb = new ComboBox<Node>();
cb.getItems().addAll(nodes);
cb.setCellFactory((lv) -> {
return new Cell();
});
cb.setButtonCell(new SelectedCell());
SimpleChangeListener.apply(selected, c -> {
var item = nodeMap.entrySet().stream()
.filter(e -> Objects.equals(c, e.getValue()))
.map(e -> e.getKey())
.findAny()
.orElse(c == null || unknownNode == null ? emptyNode : unknownNode.apply(c));
cb.setValue(item);
});
cb.valueProperty().addListener((c, o, n) -> {
if (nodeMap.containsKey(n)) {
if (veto != null && !veto.test(nodeMap.get(n))) {
return;
}
selected.setValue(nodeMap.get(n));
}
if (actionsMap.containsKey(n)) {
cb.setValue(o);
actionsMap.get(n).run();
}
});
if (filterPredicate != null) {
SimpleChangeListener.apply(filterString, c -> {
var filteredNodes = nodes.stream()
.filter(e -> e.equals(cb.getValue())
|| !(nodeMap.get(e) != null
&& (filterable.contains(nodeMap.get(e))
&& filterString.getValue() != null
&& !filterPredicate.test(nodeMap.get(e), c))))
.toList();
cb.setItems(FXCollections.observableList(filteredNodes));
});
filterNode.sceneProperty().addListener((c, o, n) -> {
if (n != null) {
n.getWindow().focusedProperty().addListener((c2, o2, n2) -> {
Platform.runLater(() -> {
filterNode.requestFocus();
});
});
}
Platform.runLater(() -> {
filterNode.requestFocus();
});
});
}
if (emptyNode != null) {
emptyNode.setAccessibleText(emptyAccessibilityName.getValue());
}
if (accessibleNameFunction != null) {
nodes.forEach(node -> node.setAccessibleText(accessibleNameFunction.apply(nodeMap.get(node))));
}
return cb;
}
private class SelectedCell extends ListCell<Node> {
@Override
protected void updateItem(Node item, boolean empty) {
super.updateItem(item, empty);
accessibleTextProperty().unbind();
if (empty || item.equals(emptyNode)) {
if (emptyAccessibilityName != null) {
accessibleTextProperty().bind(emptyAccessibilityName);
} else {
setAccessibleText(null);
}
}
if (empty) {
return;
}
if (item.equals(emptyNode)) {
setGraphic(item);
return;
}
// Case for dynamically created unknown nodes
if (!nodeMap.containsKey(item)) {
setGraphic(item);
// Don't expect the accessible name function to properly map this item
setAccessibleText(null);
return;
}
var val = nodeMap.get(item);
var newNode = selectedDisplayNodeFunction.apply(val);
setGraphic(newNode);
setAccessibleText(newNode.getAccessibleText());
}
}
private class Cell extends ListCell<Node> {
public Cell() {
addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
if (!nodeMap.containsKey(getItem())) {
event.consume();
}
});
addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.ENTER && !nodeMap.containsKey(getItem())) {
event.consume();
}
});
}
@Override
protected void updateItem(Node item, boolean empty) {
setGraphic(item);
if (getItem() == item) {
return;
}
super.updateItem(item, empty);
if (item == null) {
return;
}
setGraphic(item);
if (disabledNodes.contains(item)) {
this.setDisable(true);
this.setFocusTraversable(false);
// this.setPadding(Insets.EMPTY);
} else {
this.setDisable(false);
this.setFocusTraversable(true);
setAccessibleText(item.getAccessibleText());
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 872 B

View file

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 708 B

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 750 B

View file

Before

Width:  |  Height:  |  Size: 780 B

After

Width:  |  Height:  |  Size: 780 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,014 B

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 915 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 925 B

View file

Before

Width:  |  Height:  |  Size: 911 B

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 749 B

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 730 B

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

View file

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 941 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 B

View file

Before

Width:  |  Height:  |  Size: 210 B

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 801 B

View file

Before

Width:  |  Height:  |  Size: 985 B

After

Width:  |  Height:  |  Size: 985 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 715 B

View file

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 817 B

After

Width:  |  Height:  |  Size: 817 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 686 B

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 594 B

View file

Before

Width:  |  Height:  |  Size: 696 B

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 683 B

View file

Before

Width:  |  Height:  |  Size: 360 B

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,023 B

View file

Before

Width:  |  Height:  |  Size: 963 B

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 B

View file

Before

Width:  |  Height:  |  Size: 218 B

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 662 B

View file

Before

Width:  |  Height:  |  Size: 287 B

After

Width:  |  Height:  |  Size: 287 B

View file

@ -242,10 +242,6 @@
-fx-background-color: -color-success-subtle;
}
.root.nord .browser .table-row-cell:selected, .root.nord .browser .table-row-cell:hover:selected {
-fx-background-color: -color-success-7;
}
.browser .table-row-cell:folder:drag-over {
-fx-background-color: -color-success-muted;
}

View file

@ -141,14 +141,14 @@ public sealed interface OsType permits OsType.Windows, OsType.Linux, OsType.MacO
try (CommandControl c = pc.command("lsb_release -a").start()) {
var text = c.readStdoutDiscardErr();
if (c.getExitCode() == 0) {
return PropertiesFormatsParser.parse(text, ":").getOrDefault("Description", "Unknown");
return PropertiesFormatsParser.parse(text, ":").getOrDefault("Description", null);
}
}
try (CommandControl c = pc.command("cat /etc/*release").start()) {
var text = c.readStdoutDiscardErr();
if (c.getExitCode() == 0) {
return PropertiesFormatsParser.parse(text, "=").getOrDefault("PRETTY_NAME", "Unknown");
return PropertiesFormatsParser.parse(text, "=").getOrDefault("PRETTY_NAME", null);
}
}

View file

@ -14,7 +14,7 @@ public interface ProcessControl extends AutoCloseable {
ProcessControl withExceptionConverter(ExceptionConverter converter);
void resetData(boolean cache);
void resetData();
String prepareTerminalOpen(TerminalInitScriptConfig config) throws Exception;
@ -33,7 +33,7 @@ public interface ProcessControl extends AutoCloseable {
@Override
void close() throws Exception;
void kill();
void kill() throws Exception;
ProcessControl start() throws Exception;

View file

@ -16,8 +16,6 @@ import java.util.function.Function;
public interface ShellControl extends ProcessControl {
List<UUID> getExitUuids();
Optional<ShellStore> getSourceStore();
ShellControl withSourceStore(ShellStore store);

View file

@ -22,9 +22,8 @@ public class ShellDialects {
public static ShellDialect ZSH;
public static ShellDialect CSH;
public static ShellDialect FISH;
public static ShellDialect UNSUPPORTED;
public static ShellDialect CISCO;
public static ShellDialect RBASH;
public static List<ShellDialect> getStartableDialects() {
return ALL.stream().filter(dialect -> dialect.getOpenCommand() != null).filter(dialect -> dialect != SH_BSD).toList();
@ -51,8 +50,8 @@ public class ShellDialects {
ASH = byId("ash");
SH = byId("sh");
SH_BSD = byId("shBsd");
UNSUPPORTED = byId("unsupported");
CISCO = byId("cisco");
RBASH = byId("rbash");
}
@Override

3
dist/changelogs/1.7.14.md vendored Normal file
View file

@ -0,0 +1,3 @@
This is just a small hotfix update to fix a few breaking issues:
- Fix performance regression in JavaFX by downgrading version temporarily
- Fix .deb installers not being able to resolve some packages on Ubuntu < 22

View file

@ -59,7 +59,7 @@ open module io.xpipe.ext.base {
DeleteStoreChildrenAction,
BrowseStoreAction;
provides DataStoreProvider with
SimpleScriptStoreProvider,
ScriptGroupStoreProvider,
SimpleScriptStoreProvider,
InMemoryStoreProvider;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

@ -1 +1 @@
1.7.13
1.7.14