diff --git a/CHANGELOG.md b/CHANGELOG.md index 52485dda5ce..5308b02de37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We distribute arm64 images for Linux. [#10842](https://github.com/JabRef/jabref/issues/10842) - We added the field `monthfiled` to the default list of fields to resolve BibTeX-Strings for [#13375](https://github.com/JabRef/jabref/issues/13375) - We added a new ID based fetcher for [EuropePMC](https://europepmc.org/). [#13389](https://github.com/JabRef/jabref/pull/13389) +- We added an initial [cite as you write](https://retorque.re/zotero-better-bibtex/citing/cayw/) endpoint. [#13187](https://github.com/JabRef/jabref/issues/13187) ### Changed diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 131266ce455..3c4092324a2 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -91,6 +91,8 @@ Application=Application Application\ to\ push\ entries\ to=Application to push entries to +%0\ |\ Cite\ As\ You\ Write=%0 | Cite As You Write + Apply=Apply Assign\ the\ original\ group's\ entries\ to\ this\ group?=Assign the original group's entries to this group? diff --git a/jabsrv-cli/src/main/java/org/jabref/http/server/cli/ServerCli.java b/jabsrv-cli/src/main/java/org/jabref/http/server/cli/ServerCli.java index 2188eb9379a..e73ee273955 100644 --- a/jabsrv-cli/src/main/java/org/jabref/http/server/cli/ServerCli.java +++ b/jabsrv-cli/src/main/java/org/jabref/http/server/cli/ServerCli.java @@ -29,7 +29,7 @@ public class ServerCli implements Callable { private String host = "localhost"; @CommandLine.Option(names = {"-p", "--port"}, description = "the port") - private Integer port = 6050; + private Integer port = 23119; /** * Starts an http server serving the last files opened in JabRef
diff --git a/jabsrv/src/main/java/module-info.java b/jabsrv/src/main/java/module-info.java index 518a466ec12..65af0874cd8 100644 --- a/jabsrv/src/main/java/module-info.java +++ b/jabsrv/src/main/java/module-info.java @@ -4,6 +4,8 @@ exports org.jabref.http.dto to com.google.gson, org.glassfish.hk2.locator; opens org.jabref.http.server to org.glassfish.hk2.utilities, org.glassfish.hk2.locator; + exports org.jabref.http.server.cayw; + opens org.jabref.http.server.cayw to org.glassfish.hk2.locator, org.glassfish.hk2.utilities; // For ServiceLocatorUtilities.createAndPopulateServiceLocator() requires org.glassfish.hk2.locator; diff --git a/jabsrv/src/main/java/org/jabref/http/server/Server.java b/jabsrv/src/main/java/org/jabref/http/server/Server.java index ab3364bb98d..be7f21a73d0 100644 --- a/jabsrv/src/main/java/org/jabref/http/server/Server.java +++ b/jabsrv/src/main/java/org/jabref/http/server/Server.java @@ -9,6 +9,7 @@ import org.jabref.http.dto.GlobalExceptionMapper; import org.jabref.http.dto.GsonFactory; +import org.jabref.http.server.cayw.CAYWResource; import org.jabref.http.server.services.FilesToServe; import org.jabref.logic.os.OS; @@ -55,6 +56,7 @@ private HttpServer startServer(ServiceLocator serviceLocator, URI uri) { resourceConfig.register(RootResource.class); resourceConfig.register(LibrariesResource.class); resourceConfig.register(LibraryResource.class); + resourceConfig.register(CAYWResource.class); resourceConfig.register(CORSFilter.class); resourceConfig.register(GlobalExceptionMapper.class); diff --git a/jabsrv/src/main/java/org/jabref/http/server/cayw/CAYWResource.java b/jabsrv/src/main/java/org/jabref/http/server/cayw/CAYWResource.java new file mode 100644 index 00000000000..f664b2a8e69 --- /dev/null +++ b/jabsrv/src/main/java/org/jabref/http/server/cayw/CAYWResource.java @@ -0,0 +1,191 @@ +package org.jabref.http.server.cayw; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; + +import javafx.application.Platform; + +import org.jabref.http.server.cayw.gui.CAYWEntry; +import org.jabref.http.server.cayw.gui.SearchDialog; +import org.jabref.logic.importer.fileformat.BibtexImporter; +import org.jabref.logic.preferences.CliPreferences; +import org.jabref.logic.preferences.JabRefCliPreferences; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.util.DummyFileUpdateMonitor; + +import com.google.gson.Gson; +import jakarta.inject.Inject; +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Path("better-bibtex/cayw") +public class CAYWResource { + public static final Logger LOGGER = LoggerFactory.getLogger(CAYWResource.class); + private static final String CHOCOLATEBIB_PATH = "/Chocolate.bib"; + private static boolean initialized = false; + + @Inject + private CliPreferences preferences; + + @Inject + private Gson gson; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public Response getCitation( + @QueryParam("probe") String probe, + @QueryParam("format") @DefaultValue("latex") String format, + @QueryParam("clipboard") String clipboard, + @QueryParam("minimize") String minimize, + @QueryParam("texstudio") String texstudio, + @QueryParam("selected") String selected, + @QueryParam("select") String select, + @QueryParam("librarypath") String libraryPath + ) throws IOException, ExecutionException, InterruptedException { + if (probe != null && !probe.isEmpty()) { + return Response.ok("ready").build(); + } + + BibDatabaseContext databaseContext = getBibDatabaseContext(libraryPath); + + /* unused until DatabaseSearcher is fixed + PostgreServer postgreServer = new PostgreServer(); + IndexManager.clearOldSearchIndices(); + searcher = new DatabaseSearcher( + databaseContext, + new CurrentThreadTaskExecutor(), + preferences, + postgreServer); + */ + + List> entries = databaseContext.getEntries() + .stream() + .map(this::createCAYWEntry) + .toList(); + + initializeGUI(); + + CompletableFuture> future = new CompletableFuture<>(); + Platform.runLater(() -> { + SearchDialog dialog = new SearchDialog<>(); + // TODO: Using the DatabaseSearcher directly here results in a lot of exceptions being thrown, so we use an alternative for now until we have a nice way of using the DatabaseSearcher class. + // searchDialog.set(new SearchDialog<>(s -> searcher.getMatches(new SearchQuery(s)), entries)); + List results = dialog.show(searchQuery -> + entries.stream() + .filter(bibEntryCAYWEntry -> matches(bibEntryCAYWEntry, searchQuery)) + .map(CAYWEntry::getValue) + .toList(), + entries); + + future.complete(results); + }); + + List citationKeys = future.get().stream() + .map(BibEntry::getCitationKey) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + + if (citationKeys.isEmpty()) { + return Response.noContent().build(); + } + + return Response.ok(gson.toJson(citationKeys)).build(); + } + + private BibDatabaseContext getBibDatabaseContext(String libraryPath) throws IOException { + InputStream libraryStream; + if (libraryPath != null && !libraryPath.isEmpty()) { + java.nio.file.Path path = java.nio.file.Path.of(libraryPath); + if (!Files.exists(path)) { + LOGGER.error("Library path does not exist, using the default chocolate.bib: {}", libraryPath); + libraryStream = getChocolateBibAsStream(); + } else { + libraryStream = Files.newInputStream(path); + } + } else { + // Use the latest opened library as the default library + final List lastOpenedLibraries = new ArrayList<>(JabRefCliPreferences.getInstance().getLastFilesOpenedPreferences().getLastFilesOpened()); + if (lastOpenedLibraries.isEmpty()) { + LOGGER.warn("No library path provided and no last opened libraries found, using the default chocolate.bib."); + libraryStream = getChocolateBibAsStream(); + } else { + java.nio.file.Path lastOpenedLibrary = lastOpenedLibraries.getFirst(); + if (!Files.exists(lastOpenedLibrary)) { + LOGGER.error("Last opened library does not exist, using the default chocolate.bib: {}", lastOpenedLibrary); + libraryStream = getChocolateBibAsStream(); + } else { + libraryStream = Files.newInputStream(lastOpenedLibrary); + } + } + } + + BibtexImporter bibtexImporter = new BibtexImporter(preferences.getImportFormatPreferences(), new DummyFileUpdateMonitor()); + BibDatabaseContext databaseContext; + try (BufferedReader reader = new BufferedReader(new InputStreamReader(libraryStream, StandardCharsets.UTF_8))) { + databaseContext = bibtexImporter.importDatabase(reader).getDatabaseContext(); + } + return databaseContext; + } + + private synchronized void initializeGUI() { + // TODO: Implement a better way to handle the window popup since this is a bit hacky. + if (!initialized) { + CountDownLatch latch = new CountDownLatch(1); + Platform.startup(() -> { + Platform.setImplicitExit(false); + initialized = true; + latch.countDown(); + }); + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("JavaFX initialization interrupted", e); + } + } + } + + /// @return a stream to the `Chocolate.bib` file in the classpath (is null only if the file was moved or there are issues with the classpath) + private @Nullable InputStream getChocolateBibAsStream() { + return BibDatabase.class.getResourceAsStream(CHOCOLATEBIB_PATH); + } + + private CAYWEntry createCAYWEntry(BibEntry entry) { + String label = entry.getCitationKey().orElse(""); + String shortLabel = label; + String description = entry.getField(StandardField.TITLE).orElse(entry.getAuthorTitleYear()); + return new CAYWEntry<>(entry, label, shortLabel, description); + } + + private boolean matches(CAYWEntry entry, String searchText) { + if (searchText == null || searchText.isEmpty()) { + return true; + } + String lowerSearchText = searchText.toLowerCase(); + return entry.getLabel().toLowerCase().contains(lowerSearchText) || + entry.getDescription().toLowerCase().contains(lowerSearchText) || + entry.getShortLabel().toLowerCase().contains(lowerSearchText); + } +} diff --git a/jabsrv/src/main/java/org/jabref/http/server/cayw/gui/CAYWEntry.java b/jabsrv/src/main/java/org/jabref/http/server/cayw/gui/CAYWEntry.java new file mode 100644 index 00000000000..59e3a2b8942 --- /dev/null +++ b/jabsrv/src/main/java/org/jabref/http/server/cayw/gui/CAYWEntry.java @@ -0,0 +1,51 @@ +package org.jabref.http.server.cayw.gui; + +import javafx.event.ActionEvent; +import javafx.event.EventHandler; + +public class CAYWEntry { + + private final T value; + + // Used on the buttons ("chips") + private final String shortLabel; + + // Used in the list + private final String label; + + // Used when hovering and used as bases on the second line + private final String description; + + private EventHandler onClick; + + public CAYWEntry(T value, String label, String shortLabel, String description) { + this.value = value; + this.label = label; + this.shortLabel = shortLabel; + this.description = description; + } + + public T getValue() { + return value; + } + + public String getLabel() { + return label; + } + + public String getShortLabel() { + return shortLabel; + } + + public String getDescription() { + return description; + } + + public EventHandler getOnClick() { + return onClick; + } + + public void setOnClick(EventHandler onClick) { + this.onClick = onClick; + } +} diff --git a/jabsrv/src/main/java/org/jabref/http/server/cayw/gui/SearchDialog.java b/jabsrv/src/main/java/org/jabref/http/server/cayw/gui/SearchDialog.java new file mode 100644 index 00000000000..d4a48d3e06b --- /dev/null +++ b/jabsrv/src/main/java/org/jabref/http/server/cayw/gui/SearchDialog.java @@ -0,0 +1,116 @@ +package org.jabref.http.server.cayw.gui; + +import java.io.InputStream; +import java.util.List; +import java.util.function.Function; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.geometry.Rectangle2D; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.image.Image; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javafx.stage.Screen; +import javafx.stage.Stage; +import javafx.stage.StageStyle; + +import org.jabref.logic.l10n.Localization; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SearchDialog { + + public static final Logger LOGGER = LoggerFactory.getLogger(SearchDialog.class); + + private static final double DIALOG_WIDTH_RATIO = 0.5; + private static final double DIALOG_HEIGHT_RATIO = 0.4; + private static final int PREF_HEIGHT = 150; + + private final ObservableList> selectedItems = FXCollections.observableArrayList(); + + private Stage dialogStage; + + public List show(Function> searchFunction, List> entries) { + FilteredList> searchResults = new FilteredList<>(FXCollections.observableArrayList(entries)); + selectedItems.clear(); + + dialogStage = new Stage(); + dialogStage.initModality(Modality.APPLICATION_MODAL); + dialogStage.initStyle(StageStyle.DECORATED); + dialogStage.setTitle(Localization.lang("%0 | Cite As You Write", "JabRef")); + dialogStage.setResizable(true); + + Rectangle2D screenBounds = Screen.getPrimary().getVisualBounds(); + double dialogWidth = screenBounds.getWidth() * DIALOG_WIDTH_RATIO; + double dialogHeight = screenBounds.getHeight() * DIALOG_HEIGHT_RATIO; + + VBox mainLayout = new VBox(10); + mainLayout.setPadding(new Insets(15)); + mainLayout.setAlignment(Pos.TOP_CENTER); + + SearchField searchField = new SearchField<>(searchResults, searchFunction); + searchField.setMaxWidth(Double.MAX_VALUE); + + SearchResultContainer resultContainer = new SearchResultContainer<>(searchResults, selectedItems); + resultContainer.setPrefHeight(PREF_HEIGHT); + + ScrollPane scrollPane = new ScrollPane(resultContainer); + scrollPane.setFitToWidth(true); + scrollPane.setFitToHeight(true); + VBox.setVgrow(scrollPane, Priority.ALWAYS); + + SelectedItemsContainer selectedContainer = new SelectedItemsContainer<>(selectedItems); + + Button finishButton = new Button(Localization.lang("Cite")); + finishButton.setOnAction(event -> { + dialogStage.close(); + }); + + mainLayout.getChildren().addAll( + searchField, + selectedContainer, + scrollPane, + finishButton + ); + + Scene scene = new Scene(mainLayout, dialogWidth, dialogHeight); + + scene.getStylesheets().add("cayw.css"); + mainLayout.getStyleClass().add("search-dialog"); + scrollPane.getStyleClass().add("scroll-pane"); + + dialogStage.setScene(scene); + + try (InputStream inputStream = getClass().getResourceAsStream("/JabRef-icon-64.png")) { + if (inputStream == null) { + LOGGER.warn("Error loading icon for SearchDialog"); + } else { + Image icon = new Image(inputStream); + dialogStage.getIcons().add(icon); + } + } catch (Exception e) { + LOGGER.warn("Error loading icon for SearchDialog", e); + } + + dialogStage.setX((screenBounds.getWidth() - dialogWidth) / 2); + dialogStage.setY((screenBounds.getHeight() - dialogHeight) / 2); + + dialogStage.showAndWait(); + + return selectedItems.stream().map(CAYWEntry::getValue).toList(); + } + + public void close() { + if (dialogStage != null) { + dialogStage.close(); + } + } +} diff --git a/jabsrv/src/main/java/org/jabref/http/server/cayw/gui/SearchField.java b/jabsrv/src/main/java/org/jabref/http/server/cayw/gui/SearchField.java new file mode 100644 index 00000000000..a442d124e84 --- /dev/null +++ b/jabsrv/src/main/java/org/jabref/http/server/cayw/gui/SearchField.java @@ -0,0 +1,27 @@ +package org.jabref.http.server.cayw.gui; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +import javafx.animation.PauseTransition; +import javafx.collections.transformation.FilteredList; +import javafx.scene.control.TextField; +import javafx.util.Duration; + +public class SearchField extends TextField { + + private static final int DELAY_IN_MS = 100; + + public SearchField(FilteredList> filteredEntries, Function> filter) { + PauseTransition pause = new PauseTransition(Duration.millis(DELAY_IN_MS)); + textProperty().addListener((_, _, newValue) -> { + pause.setOnFinished(event -> { + Set currentEntries = new HashSet<>(filter.apply(newValue)); + filteredEntries.setPredicate(entry -> currentEntries.contains(entry.getValue())); + }); + pause.playFromStart(); + }); + } +} diff --git a/jabsrv/src/main/java/org/jabref/http/server/cayw/gui/SearchResultContainer.java b/jabsrv/src/main/java/org/jabref/http/server/cayw/gui/SearchResultContainer.java new file mode 100644 index 00000000000..4826fa23c07 --- /dev/null +++ b/jabsrv/src/main/java/org/jabref/http/server/cayw/gui/SearchResultContainer.java @@ -0,0 +1,118 @@ +package org.jabref.http.server.cayw.gui; + +import java.util.Arrays; + +import javafx.beans.binding.Bindings; +import javafx.collections.ObservableList; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +import org.jabref.logic.os.OS; +import org.jabref.model.strings.StringUtil; + +public class SearchResultContainer extends ListView> { + + private final static int MAX_LINES = 3; + private final static int ESTIMATED_CHARS_PER_LINE = 80; + private final static int TOOLTIP_WIDTH = 400; + private final static double PREF_WIDTH = 300; + + private ObservableList> selectedEntries = javafx.collections.FXCollections.observableArrayList(); + + public SearchResultContainer(ObservableList> entries, ObservableList> selectedEntries) { + super(entries); + this.selectedEntries = selectedEntries; + setup(); + } + + private void setup() { + this.setCellFactory(listView -> { + SearchResultCell searchResultCell = new SearchResultCell(); + searchResultCell.setOnMouseClicked(event -> { + if (searchResultCell.getItem() == null || selectedEntries.contains(searchResultCell.getItem())) { + return; + } + selectedEntries.add(searchResultCell.getItem()); + }); + return searchResultCell; + }); + + this.setFocusTraversable(false); + + this.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> { + if (getParent() != null) { + return getParent().getLayoutBounds().getWidth(); + } + return PREF_WIDTH; + }, parentProperty())); + } + + private static class SearchResultCell extends ListCell> { + private final VBox content; + private final Text labelText; + private final Text descriptionText; + private final Tooltip tooltip; + + public SearchResultCell() { + this.content = new VBox(5); + this.labelText = new Text(); + this.descriptionText = new Text(); + this.tooltip = new Tooltip(); + + labelText.getStyleClass().add("search-result-label"); + + descriptionText.getStyleClass().add("search-result-description"); + + content.getChildren().addAll(labelText, descriptionText); + + descriptionText.wrappingWidthProperty().bind( + widthProperty().subtract(20) + ); + } + + @Override + protected void updateItem(CAYWEntry item, boolean empty) { + super.updateItem(item, empty); + + if (empty || item == null) { + setGraphic(null); + setTooltip(null); + } else { + labelText.setText(item.getLabel()); + + String fullDescription = item.getDescription(); + String truncatedDescription = truncateToThreeLines(fullDescription); + + descriptionText.setText(truncatedDescription); + + if (!fullDescription.equals(truncatedDescription)) { + tooltip.setText(fullDescription); + tooltip.setWrapText(true); + tooltip.setMaxWidth(TOOLTIP_WIDTH); + setTooltip(tooltip); + } else { + setTooltip(null); + } + + setGraphic(content); + } + } + + private String truncateToThreeLines(String text) { + if (text == null || text.isEmpty()) { + return ""; + } + + String[] lines = text.split(OS.NEWLINE, MAX_LINES + 1); + + if (lines.length <= MAX_LINES) { + return StringUtil.limitStringLength(text, ESTIMATED_CHARS_PER_LINE * MAX_LINES); + } else { + return String.join(OS.NEWLINE, Arrays.copyOf(lines, MAX_LINES)) + "..."; + } + } + } +} diff --git a/jabsrv/src/main/java/org/jabref/http/server/cayw/gui/SelectedItemsContainer.java b/jabsrv/src/main/java/org/jabref/http/server/cayw/gui/SelectedItemsContainer.java new file mode 100644 index 00000000000..ea8f71271aa --- /dev/null +++ b/jabsrv/src/main/java/org/jabref/http/server/cayw/gui/SelectedItemsContainer.java @@ -0,0 +1,90 @@ +package org.jabref.http.server.cayw.gui; + +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; + +public class SelectedItemsContainer extends FlowPane { + + private final ObservableList> items; + + public SelectedItemsContainer(ObservableList> items) { + this.items = items; + setup(); + } + + private void setup() { + this.setHgap(8); + this.setVgap(8); + this.setPadding(new Insets(10)); + + items.forEach(this::addChip); + + items.addListener((ListChangeListener>) change -> { + while (change.next()) { + if (change.wasAdded()) { + change.getAddedSubList().forEach(this::addChip); + } + if (change.wasRemoved()) { + change.getRemoved().forEach(this::removeChip); + } + } + }); + } + + private void addChip(CAYWEntry entry) { + Chip chip = new Chip<>(entry, items); + getChildren().add(chip); + } + + private void removeChip(CAYWEntry entry) { + getChildren().removeIf(node -> { + if (node instanceof SelectedItemsContainer.Chip chip) { + return chip.getEntry().equals(entry); + } + return false; + }); + } + + private static class Chip extends HBox { + private final CAYWEntry entry; + + public Chip(CAYWEntry entry, ObservableList> parentList) { + this.entry = entry; + + this.setAlignment(Pos.CENTER_LEFT); + this.setSpacing(5); + this.setPadding(new Insets(5, 10, 5, 10)); + + this.getStyleClass().add("chip-style"); + + Button removeButton = new Button("×"); + removeButton.getStyleClass().add("chip-remove-button"); + + removeButton.setOnAction(e -> { + e.consume(); + parentList.remove(entry); + }); + + Label label = new Label(entry.getShortLabel()); + + getChildren().addAll(label, removeButton); + + this.setOnMouseClicked(e -> { + if (!e.isConsumed() && entry.getOnClick() != null) { + entry.getOnClick().handle(new ActionEvent(entry, null)); + } + }); + } + + public CAYWEntry getEntry() { + return entry; + } + } +} diff --git a/jabsrv/src/main/resources/JabRef-icon-64.png b/jabsrv/src/main/resources/JabRef-icon-64.png new file mode 100644 index 00000000000..e807e0448d3 Binary files /dev/null and b/jabsrv/src/main/resources/JabRef-icon-64.png differ diff --git a/jabsrv/src/main/resources/cayw.css b/jabsrv/src/main/resources/cayw.css new file mode 100644 index 00000000000..5312a5d9b7a --- /dev/null +++ b/jabsrv/src/main/resources/cayw.css @@ -0,0 +1,44 @@ +.search-dialog { + -fx-background-color: #f5f5f5; +} + +.scroll-pane { + -fx-background-color: transparent; + -fx-border-color: #e0e0e0; +} + +.search-result-label { + -fx-font-weight: bold; + -fx-font-size: 14px; +} + +.search-result-description { + -fx-font-size: 12px; + -fx-fill: #666666; +} + +.chip-remove-button { + -fx-background-color: transparent; + -fx-font-size: 16px; + -fx-font-weight: bold; + -fx-text-fill: #666; + -fx-padding: 0 0 0 5; + -fx-cursor: hand; +} + +.chip-remove-button:hover { + -fx-text-fill: #d32f2f; +} + +.chip-style { + -fx-background-color: #e3f2fd; + -fx-background-radius: 15; + -fx-border-color: #90caf9; + -fx-border-radius: 15; + -fx-border-width: 1; + -fx-cursor: hand; +} + +.chip-style:hover { + -fx-background-color: #bbdefb; +} diff --git a/jabsrv/src/test/cayw.http b/jabsrv/src/test/cayw.http new file mode 100644 index 00000000000..477ccee1ab6 --- /dev/null +++ b/jabsrv/src/test/cayw.http @@ -0,0 +1,7 @@ +#### Serve chocolate.bib + +GET http://localhost:23119/better-bibtex/cayw + +#### Serve a library specified using the URL param + +GET http://localhost:6050/better-bibtex/cayw?librarypath=C%3A%5CUsers%5CDEMO%5CDownloads%5CChocolate.bib