diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListFilterComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListFilterComp.java index 9478a9e65..c662c3bc0 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListFilterComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListFilterComp.java @@ -32,7 +32,8 @@ public final class BrowserConnectionListFilterComp extends SimpleRegionBuilder { var category = new DataStoreCategoryChoiceComp( StoreViewState.get().getAllConnectionsCategory(), StoreViewState.get().getActiveCategory(), - this.category) + this.category, + true) .style(Styles.LEFT_PILL) .apply(struc -> { AppFontSizes.base(struc); diff --git a/app/src/main/java/io/xpipe/app/comp/base/ListSelectorComp.java b/app/src/main/java/io/xpipe/app/comp/base/ListSelectorComp.java index 23ac2d8c5..be39bb8ea 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ListSelectorComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ListSelectorComp.java @@ -11,6 +11,7 @@ import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.geometry.Orientation; import javafx.geometry.Pos; +import javafx.scene.Node; import javafx.scene.control.CheckBox; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; @@ -65,6 +66,7 @@ public class ListSelectorComp extends SimpleRegionBuilder { } vbox.getChildren().clear(); cbs.clear(); + var newChildren = new ArrayList(); for (var v : currentVals) { var cb = new CheckBox(null); if (disable.test(v)) { @@ -105,8 +107,9 @@ public class ListSelectorComp extends SimpleRegionBuilder { event.consume(); }); l.opacityProperty().bind(cb.opacityProperty()); - vbox.getChildren().add(l); + newChildren.add(l); } + vbox.getChildren().addAll(newChildren); if (showAllSelector.get()) { var allSelector = new CheckBox(null); diff --git a/app/src/main/java/io/xpipe/app/hub/comp/DataStoreCategoryChoiceComp.java b/app/src/main/java/io/xpipe/app/hub/comp/DataStoreCategoryChoiceComp.java index 380ff3f81..fef12d79f 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/DataStoreCategoryChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/DataStoreCategoryChoiceComp.java @@ -19,17 +19,20 @@ public class DataStoreCategoryChoiceComp extends SimpleRegionBuilder { private final StoreCategoryWrapper root; private final Property external; private final Property value; + private final boolean applyExternalInitially; public DataStoreCategoryChoiceComp( - StoreCategoryWrapper root, Property external, Property value) { + StoreCategoryWrapper root, Property external, Property value, boolean applyExternalInitially) { this.root = root; this.external = external; this.value = value; + this.applyExternalInitially = applyExternalInitially; } @Override protected Region createSimple() { var initialized = new SimpleBooleanProperty(); + var last = value.getValue(); external.subscribe(newValue -> { if (newValue == null) { value.setValue(root); @@ -44,6 +47,9 @@ public class DataStoreCategoryChoiceComp extends SimpleRegionBuilder { } initialized.set(true); }); + if (!applyExternalInitially) { + value.setValue(last); + } var box = new ComboBox<>(StoreViewState.get().getSortedCategories(root).getList()); box.setValue(value.getValue()); box.valueProperty().addListener((observable, oldValue, newValue) -> { diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreChoiceComp.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreChoiceComp.java index a3905321f..47937cd07 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreChoiceComp.java @@ -22,6 +22,8 @@ import javafx.scene.layout.Region; import atlantafx.base.theme.Styles; import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.val; import org.int4.fx.builders.common.AbstractRegionBuilder; import io.xpipe.app.comp.BaseRegionBuilder; import org.kordamp.ikonli.javafx.FontIcon; @@ -33,22 +35,33 @@ public class StoreChoiceComp extends SimpleRegionBuilder { private final ObjectProperty> selected; - private final StoreChoicePopover popover; + private StoreChoicePopover popover; public StoreChoiceComp( DataStoreEntry self, ObjectProperty> selected, Class storeClass, Predicate> applicableCheck, - StoreCategoryWrapper initialCategory) { + StoreCategoryWrapper categoryRoot) { + this(self, selected, storeClass, applicableCheck, categoryRoot, null); + } + + public StoreChoiceComp( + DataStoreEntry self, + ObjectProperty> selected, + Class storeClass, + Predicate> applicableCheck, + StoreCategoryWrapper categoryRoot, + StoreCategoryWrapper explicitCategory) { this.selected = selected; this.popover = new StoreChoicePopover<>( self, selected, storeClass, applicableCheck, - initialCategory, - "selectConnection", + categoryRoot, + explicitCategory, + StoreViewState.get().getAllConnectionsCategory().equals(categoryRoot) ? "selectConnection" : "selectEntry", "noCompatibleConnection"); } @@ -60,13 +73,21 @@ public class StoreChoiceComp extends SimpleRegionBuilder { return DataStorage.get().getStoreEntryDisplayName(entry); } + protected String toGraphic(DataStoreEntry entry) { + if (entry == null) { + return null; + } + + return entry.getEffectiveIconFile(); + } + @Override protected Region createSimple() { var button = new ButtonComp( Bindings.createStringBinding( () -> { var val = selected.getValue(); - return val != null ? toName(val.get()) : null; + return toName(val != null ? val.get() : null); }, selected), () -> {}); @@ -84,12 +105,7 @@ public class StoreChoiceComp extends SimpleRegionBuilder { BaseRegionBuilder graphic = PrettyImageHelper.ofFixedSize( Bindings.createStringBinding( () -> { - var val = selected.getValue(); - if (val == null) { - return null; - } - - return val.get().getEffectiveIconFile(); + return toGraphic(selected.getValue() != null ? selected.getValue().get() : null); }, selected), 16, @@ -119,6 +135,7 @@ public class StoreChoiceComp extends SimpleRegionBuilder { AppFontSizes.xl(dropdownIcon); var pane = new AnchorPane(r, dropdownIcon); + r.prefHeightProperty().bind(pane.heightProperty()); pane.focusedProperty().addListener((observable, oldValue, newValue) -> { if (newValue) { r.requestFocus(); diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreChoicePopover.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreChoicePopover.java index a29141848..d0547414f 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreChoicePopover.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreChoicePopover.java @@ -43,7 +43,8 @@ public class StoreChoicePopover { private final Property> selected; private final Class storeClass; private final Predicate> applicableCheck; - private final StoreCategoryWrapper initialCategory; + private final StoreCategoryWrapper rootCategory; + private final StoreCategoryWrapper explicitCategory; private final String titleKey; private final String noMatchKey; private Consumer consumer; @@ -74,9 +75,9 @@ public class StoreChoicePopover { if (popover == null || applicableCheck != null) { var cur = StoreViewState.get().getActiveCategory().getValue(); var selectedCategory = new SimpleObjectProperty<>( - initialCategory != null - ? (initialCategory.getRoot().equals(cur.getRoot()) ? cur : initialCategory) - : cur); + explicitCategory != null ? explicitCategory : (rootCategory != null + ? (rootCategory.getRoot().equals(cur.getRoot()) ? cur : rootCategory) + : cur)); var filterText = new SimpleStringProperty(); popover = new Popover(); Predicate applicable = storeEntryWrapper -> { @@ -101,7 +102,7 @@ public class StoreChoicePopover { var applicableMatch = StoreViewState.get().getCurrentTopLevelSection().anyMatches(applicable); if (!applicableMatch) { - selectedCategory.set(initialCategory); + selectedCategory.set(rootCategory); } var applicableCount = StoreViewState.get().getAllEntries().getList().stream() @@ -134,9 +135,10 @@ public class StoreChoicePopover { initialExpanded); var category = new DataStoreCategoryChoiceComp( - initialCategory != null ? initialCategory.getRoot() : null, + rootCategory != null ? rootCategory.getRoot() : null, StoreViewState.get().getActiveCategory(), - selectedCategory) + selectedCategory, + explicitCategory == null) .style(Styles.LEFT_PILL); var filter = new FilterComp(filterText).style(Styles.CENTER_PILL).hgrow(); diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreComboChoiceComp.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreComboChoiceComp.java index 8e19f0913..60c8ddf06 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreComboChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreComboChoiceComp.java @@ -75,6 +75,7 @@ public class StoreComboChoiceComp extends SimpleRegionBuild storeClass, applicableCheck, initialCategory, + null, "selectConnection", "noCompatibleConnection"); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentitySelectComp.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentitySelectComp.java index 65c39160a..c44c9d6c4 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentitySelectComp.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentitySelectComp.java @@ -302,6 +302,7 @@ public class IdentitySelectComp extends RegionBuilder { IdentityStore.class, null, StoreViewState.get().getAllIdentitiesCategory(), + null, "selectIdentity", "noCompatibleIdentity"); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptCollectionSourceImportDialog.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptCollectionSourceImportDialog.java index 8da759245..b141ff1a4 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptCollectionSourceImportDialog.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptCollectionSourceImportDialog.java @@ -1,25 +1,38 @@ package io.xpipe.ext.base.script; +import io.xpipe.app.comp.RegionBuilder; import io.xpipe.app.comp.base.*; import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.ShellDialectChoiceComp; import io.xpipe.app.ext.ShellDialectIcons; +import io.xpipe.app.hub.comp.StoreChoiceComp; +import io.xpipe.app.hub.comp.StoreCreationDialog; +import io.xpipe.app.hub.comp.StoreViewState; import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.platform.DerivedObservableList; import io.xpipe.app.platform.LabelGraphic; +import io.xpipe.app.platform.OptionsBuilder; import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.ThreadHelper; +import io.xpipe.core.FilePath; +import io.xpipe.ext.base.desktop.DesktopBaseStore; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.*; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import lombok.Getter; import org.kordamp.ikonli.javafx.FontIcon; import java.util.ArrayList; import java.util.List; +import java.util.UUID; public class ScriptCollectionSourceImportDialog { @@ -30,6 +43,7 @@ public class ScriptCollectionSourceImportDialog { private final StringProperty filter = new SimpleStringProperty(); private final BooleanProperty busy = new SimpleBooleanProperty(); private final IntegerProperty count = new SimpleIntegerProperty(); + private final ObjectProperty> targetGroup = new SimpleObjectProperty<>(); public ScriptCollectionSourceImportDialog(ScriptCollectionSource source) { this.source = source; @@ -45,6 +59,7 @@ public class ScriptCollectionSourceImportDialog { var filterField = new FilterComp(filter).hgrow(); // Ugly solution to focus the filter on show filterField.apply(r -> { + HBox.setHgrow(r, Priority.SOMETIMES); r.sceneProperty().subscribe(s -> { if (s != null) { Platform.runLater(() -> { @@ -81,7 +96,35 @@ public class ScriptCollectionSourceImportDialog { var stack = new StackComp(List.of(notFound, selector)); stack.prefWidth(600); - stack.prefHeight(700); + stack.prefHeight(650); + + var storeChoice = new StoreChoiceComp<>( + null, + targetGroup, + ScriptGroupStore.class, + null, + StoreViewState.get().getAllScriptsCategory(), + StoreViewState.get().getAllScriptsCategory()) { + @Override + protected String toName(DataStoreEntry entry) { + if (entry == null) { + return AppI18n.get("selectCategory"); + } else { + return super.toName(entry); + } + } + + @Override + protected String toGraphic(DataStoreEntry entry) { + if (entry == null) { + return "scriptGroup_icon.svg"; + } else { + return super.toGraphic(entry); + } + } + }; + storeChoice.hgrow(); + storeChoice.maxHeight(100); var modal = ModalOverlay.of( Bindings.createStringBinding(() -> { @@ -91,13 +134,42 @@ public class ScriptCollectionSourceImportDialog { null); modal.addButtonBarComp(refresh); modal.addButtonBarComp(filterField); + modal.addButtonBarComp(storeChoice); modal.addButton(ModalButton.ok(() -> { - + ThreadHelper.runAsync(() -> { + finish(); + }); })) - .augment(button -> button.disableProperty().bind(Bindings.isEmpty(selected))); + .augment(button -> button.disableProperty().bind(Bindings.isEmpty(selected).or(targetGroup.isNull()))); modal.show(); } + private void finish() { + var targetCat = DataStorage.get().getStoreCategory(targetGroup.getValue().get()); + StoreViewState.get().selectCategoryIntoViewIfNeeded(StoreViewState.get().getCategoryWrapper(targetCat)); + + var added = new ArrayList(); + for (ScriptCollectionSourceEntry e : selected) { + var name = FilePath.of(e.getName()).getBaseName().toString(); + var textSource = ScriptTextSource.SourceReference.builder().entry(e).build(); + + var alreadyAdded = DataStorage.get().getStoreEntries().stream().anyMatch(entry -> entry.getStore() instanceof ScriptStore ss && + textSource.equals(ss.getTextSource())); + if (alreadyAdded) { + continue; + } + + var store = ScriptStore.builder().textSource(textSource).group(targetGroup.get()).build(); + var entry = DataStoreEntry.createNew(name, store); + DataStorage.get().addStoreEntryIfNotPresent(entry); + added.add(entry); + } + + if (added.size() == 1) { + StoreCreationDialog.showEdit(added.getFirst()); + } + } + private void update() { if (filter.get() == null) { shown.setAll(available); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptCollectionSourceImportHubProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptCollectionSourceImportHubProvider.java index 2774d0eb4..b076ebee8 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptCollectionSourceImportHubProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptCollectionSourceImportHubProvider.java @@ -29,7 +29,7 @@ public class ScriptCollectionSourceImportHubProvider implements HubLeafProvider< @Override public LabelGraphic getIcon(DataStoreEntryRef store) { - return new LabelGraphic.IconGraphic("mdi2f-file-undo-outline"); + return new LabelGraphic.IconGraphic("mdi2i-import"); } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptCollectionSourceStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptCollectionSourceStoreProvider.java index a85ce00b5..19656d170 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptCollectionSourceStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptCollectionSourceStoreProvider.java @@ -91,8 +91,9 @@ public class ScriptCollectionSourceStoreProvider implements DataStoreProvider { return Bindings.createStringBinding(() -> { var s = st.getState(); var summary = st.getSource().toSummary(); + var init = s.getEntries() != null; var format = new StoreStateFormat(List.of(), summary, - s.getEntries() != null ? AppI18n.get("scriptsContained", s.getEntries().size()) : null); + init ? AppI18n.get("scriptsContained", s.getEntries().size()) : null, !init ? AppI18n.get("notInitialized") : null); return format.format(); }, section.getWrapper().getPersistentState(), AppI18n.activeLanguage()); } diff --git a/ext/base/src/main/java/module-info.java b/ext/base/src/main/java/module-info.java index ceb9265fb..5c2864f83 100644 --- a/ext/base/src/main/java/module-info.java +++ b/ext/base/src/main/java/module-info.java @@ -34,7 +34,7 @@ open module io.xpipe.ext.base { requires com.sun.jna; requires javafx.base; requires javafx.graphics; - + provides ActionProvider with IdentityApplyHubLeafProvider, AbstractHostCreationActionProvider, diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-16-dark.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-16-dark.png index 0772f1e34..98c26d421 100644 Binary files a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-16-dark.png and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-16-dark.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-16.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-16.png index 695ea3688..cbb2eb5ea 100644 Binary files a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-16.png and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-16.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-24-dark.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-24-dark.png index 2570db319..bfaab3fbe 100644 Binary files a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-24-dark.png and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-24-dark.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-24.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-24.png index 8dba0a420..04a79e18c 100644 Binary files a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-24.png and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-24.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-40-dark.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-40-dark.png index cb2391be7..0ab9cf747 100644 Binary files a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-40-dark.png and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-40-dark.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-40.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-40.png index 63e0fd800..fbe7044a2 100644 Binary files a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-40.png and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-40.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-80-dark.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-80-dark.png index f345032da..c0bb5ebff 100644 Binary files a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-80-dark.png and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-80-dark.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-80.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-80.png index 89bbde1f9..63fa524d7 100644 Binary files a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-80.png and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-80.png differ diff --git a/img/base/desktopApplication_icon-dark.svg b/img/base/desktopApplication_icon-dark.svg index 7ebae066d..c21970118 100644 --- a/img/base/desktopApplication_icon-dark.svg +++ b/img/base/desktopApplication_icon-dark.svg @@ -22,9 +22,9 @@ inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" - inkscape:zoom="0.9861186" - inkscape:cx="-94.309143" - inkscape:cy="129.29479" + inkscape:zoom="1" + inkscape:cx="104" + inkscape:cy="-51.5" inkscape:window-width="1920" inkscape:window-height="1009" inkscape:window-x="-8" @@ -37,7 +37,7 @@ id="title858">identityidentity + style="fill:#f38626;fill-opacity:1;stroke-width:0.333356" /> diff --git a/img/base/desktopApplication_icon.svg b/img/base/desktopApplication_icon.svg index d150e2bf9..f53571aed 100644 --- a/img/base/desktopApplication_icon.svg +++ b/img/base/desktopApplication_icon.svg @@ -22,9 +22,9 @@ inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" - inkscape:zoom="1.3945823" - inkscape:cx="-15.775333" - inkscape:cy="89.991103" + inkscape:zoom="0.9861186" + inkscape:cx="-11.154845" + inkscape:cy="-62.365723" inkscape:window-width="1920" inkscape:window-height="1009" inkscape:window-x="-8" @@ -37,7 +37,7 @@ id="title858">identityidentity + style="fill:#e1710c;fill-opacity:1;stroke-width:0.333356" + sodipodi:nodetypes="ccsssssccccssssssssssssssss" /> diff --git a/lang/strings/translations_en.properties b/lang/strings/translations_en.properties index 937d1529b..d185cbb11 100644 --- a/lang/strings/translations_en.properties +++ b/lang/strings/translations_en.properties @@ -74,6 +74,7 @@ customCommand=Custom command other=Other setLock=Set lock selectConnection=Select connection +selectEntry=Select entry createLock=Create passphrase changeLock=Change passphrase test=Test @@ -1928,3 +1929,6 @@ importScripts=Import scripts scriptsContained=$NUMBER$ scripts scriptSourceCollectionImportTitle=Import scripts from source ($COUNT$) noScriptsFound=No scripts found +tunnel=Tunnel +notInitialized=Not initialized +selectCategory=Select category ...