Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'Send to Kindle' feature #9995

Merged
merged 8 commits into from
Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve
- We added the ability to search for a DOI directly from 'Web Search'. [#9674](https://github.com/JabRef/jabref/issues/9674)
- We added a cleanup activity that identifies a URL in the `note` field and moves it to the `url` field. [koppor#216](https://github.com/koppor/jabref/issues/216)
- We enabled the user to change the name of a field in a custom entry type by double-clicking on it. [#9840](https://github.com/JabRef/jabref/issues/9840)

- We integrated two mail actions ("As Email" and "To Kindle") under a new "Send" option in the right-click & Tools menus. The Kindle option creates an email targeted to the user's Kindle email, which can be set in preferences under "External programs" [#6186](https://github.com/JabRef/jabref/issues/6186)

### Changed

Expand Down
15 changes: 14 additions & 1 deletion src/main/java/org/jabref/gui/JabRefFrame.java
Original file line number Diff line number Diff line change
Expand Up @@ -904,7 +904,7 @@ private MenuBar createMenu() {

new SeparatorMenuItem(),

factory.createMenuItem(StandardActions.SEND_AS_EMAIL, new SendAsEMailAction(dialogService, this.prefs, stateManager)),
createSendSubMenu(factory, dialogService, stateManager, prefs),
pushToApplicationMenuItem,

new SeparatorMenuItem(),
Expand Down Expand Up @@ -1322,6 +1322,19 @@ private void copyRootNode(LibraryTab destinationLibraryTab) {
.setGroups(currentLibraryGroupRoot);
}

private Menu createSendSubMenu(ActionFactory factory,
DialogService dialogService,
StateManager stateManager,
PreferencesService preferencesService) {
Menu sendMenu = factory.createMenu(StandardActions.SEND);
sendMenu.getItems().addAll(
factory.createMenuItem(StandardActions.SEND_AS_EMAIL, new SendAsStandardEmailAction(dialogService, preferencesService, stateManager)),
factory.createMenuItem(StandardActions.SEND_TO_KINDLE, new SendAsKindleEmailAction(dialogService, preferencesService, stateManager))
);

return sendMenu;
}

/**
* The action concerned with closing the window.
*/
Expand Down
64 changes: 30 additions & 34 deletions src/main/java/org/jabref/gui/SendAsEMailAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,17 @@

import java.awt.Desktop;
import java.io.IOException;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

import org.jabref.architecture.AllowedToUseAwt;
import org.jabref.gui.actions.ActionHelper;
import org.jabref.gui.actions.SimpleCommand;
import org.jabref.gui.desktop.JabRefDesktop;
import org.jabref.gui.util.BackgroundTask;
import org.jabref.logic.bibtex.BibEntryWriter;
import org.jabref.logic.bibtex.FieldWriter;
import org.jabref.logic.exporter.BibWriter;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.OS;
import org.jabref.logic.util.io.FileUtil;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
Expand All @@ -37,7 +32,7 @@
* preferences/external programs
*/
@AllowedToUseAwt("Requires AWT to send an email")
public class SendAsEMailAction extends SimpleCommand {
public abstract class SendAsEMailAction extends SimpleCommand {

private static final Logger LOGGER = LoggerFactory.getLogger(SendAsEMailAction.class);
private final DialogService dialogService;
Expand All @@ -48,8 +43,6 @@ public SendAsEMailAction(DialogService dialogService, PreferencesService prefere
this.dialogService = dialogService;
this.preferencesService = preferencesService;
this.stateManager = stateManager;

this.executable.bind(ActionHelper.needsEntriesSelected(stateManager));
}

@Override
Expand All @@ -73,29 +66,40 @@ private String sendEmail() throws Exception {
return Localization.lang("This operation requires one or more entries to be selected.");
}

StringWriter rawEntries = new StringWriter();
BibWriter bibWriter = new BibWriter(rawEntries, OS.NEWLINE);
BibDatabaseContext databaseContext = stateManager.getActiveDatabase().get();
List<BibEntry> entries = stateManager.getSelectedEntries();
URI uriMailTo = getUriMailTo(entries);

// write the entries via this writer to "rawEntries" (being a StringWriter), which used later to form the email content
BibEntryWriter bibtexEntryWriter = new BibEntryWriter(new FieldWriter(preferencesService.getFieldPreferences()), Globals.entryTypesManager);
Desktop desktop = Desktop.getDesktop();
desktop.mail(uriMailTo);

for (BibEntry entry : entries) {
try {
bibtexEntryWriter.write(entry, bibWriter, databaseContext.getMode());
} catch (IOException e) {
LOGGER.warn("Problem creating BibTeX file for mailing.", e);
}
return String.format("%s: %d", Localization.lang("Entries added to an email"), entries.size());
}

private URI getUriMailTo(List<BibEntry> entries) throws URISyntaxException {
StringBuilder mailTo = new StringBuilder();

mailTo.append(getEmailAddress());
mailTo.append("?Body=").append(getBody());
mailTo.append("&Subject=").append(getSubject());

List<String> attachments = getAttachments(entries);
for (String path : attachments) {
mailTo.append("&Attachment=\"").append(path);
mailTo.append("\"");
}

List<String> attachments = new ArrayList<>();
return new URI("mailto", mailTo.toString(), null);
}

private List<String> getAttachments(List<BibEntry> entries) {
// open folders is needed to indirectly support email programs, which cannot handle
// the unofficial "mailto:attachment" property
boolean openFolders = preferencesService.getExternalApplicationsPreferences().shouldAutoOpenEmailAttachmentsFolder();

BibDatabaseContext databaseContext = stateManager.getActiveDatabase().get();
List<Path> fileList = FileUtil.getListOfLinkedFiles(entries, databaseContext.getFileDirectories(preferencesService.getFilePreferences()));

List<String> attachments = new ArrayList<>();
for (Path path : fileList) {
attachments.add(path.toAbsolutePath().toString());
if (openFolders) {
Expand All @@ -106,20 +110,12 @@ private String sendEmail() throws Exception {
}
}
}
return attachments;
}

String mailTo = "?Body=".concat(rawEntries.toString());
mailTo = mailTo.concat("&Subject=");
mailTo = mailTo.concat(preferencesService.getExternalApplicationsPreferences().getEmailSubject());
for (String path : attachments) {
mailTo = mailTo.concat("&Attachment=\"").concat(path);
mailTo = mailTo.concat("\"");
}

URI uriMailTo = new URI("mailto", mailTo, null);
protected abstract String getEmailAddress();

Desktop desktop = Desktop.getDesktop();
desktop.mail(uriMailTo);
protected abstract String getSubject();

return String.format("%s: %d", Localization.lang("Entries added to an email"), entries.size());
}
protected abstract String getBody();
}
34 changes: 34 additions & 0 deletions src/main/java/org/jabref/gui/SendAsKindleEmailAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.jabref.gui;

import org.jabref.gui.actions.ActionHelper;
import org.jabref.logic.l10n.Localization;
import org.jabref.preferences.PreferencesService;

/**
* Sends attachments for selected entries to the
* configured Kindle email
*/
public class SendAsKindleEmailAction extends SendAsEMailAction {
private final PreferencesService preferencesService;

public SendAsKindleEmailAction(DialogService dialogService, PreferencesService preferencesService, StateManager stateManager) {
super(dialogService, preferencesService, stateManager);
this.preferencesService = preferencesService;
this.executable.bind(ActionHelper.needsEntriesSelected(stateManager).and(ActionHelper.hasLinkedFileForSelectedEntries(stateManager)));
}

@Override
protected String getEmailAddress() {
return preferencesService.getExternalApplicationsPreferences().getKindleEmail();
}

@Override
protected String getSubject() {
return Localization.lang("Send to Kindle");
}

@Override
protected String getBody() {
return Localization.lang("Send to Kindle");
}
}
64 changes: 64 additions & 0 deletions src/main/java/org/jabref/gui/SendAsStandardEmailAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.jabref.gui;

import java.io.IOException;
import java.io.StringWriter;
import java.util.List;

import org.jabref.gui.actions.ActionHelper;
import org.jabref.logic.bibtex.BibEntryWriter;
import org.jabref.logic.bibtex.FieldWriter;
import org.jabref.logic.exporter.BibWriter;
import org.jabref.logic.util.OS;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
import org.jabref.preferences.PreferencesService;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Sends the selected entries to any specifiable email
* by populating the email body
*/
public class SendAsStandardEmailAction extends SendAsEMailAction {
private static final Logger LOGGER = LoggerFactory.getLogger(SendAsStandardEmailAction.class);
private final PreferencesService preferencesService;
private final StateManager stateManager;

public SendAsStandardEmailAction(DialogService dialogService, PreferencesService preferencesService, StateManager stateManager) {
super(dialogService, preferencesService, stateManager);
this.preferencesService = preferencesService;
this.stateManager = stateManager;
this.executable.bind(ActionHelper.needsEntriesSelected(stateManager));
}

@Override
protected String getEmailAddress() {
return "";
}

@Override
protected String getSubject() {
return preferencesService.getExternalApplicationsPreferences().getEmailSubject();
}

@Override
protected String getBody() {
List<BibEntry> entries = stateManager.getSelectedEntries();
BibDatabaseContext databaseContext = stateManager.getActiveDatabase().get();
StringWriter rawEntries = new StringWriter();
BibWriter bibWriter = new BibWriter(rawEntries, OS.NEWLINE);

BibEntryWriter bibtexEntryWriter = new BibEntryWriter(new FieldWriter(preferencesService.getFieldPreferences()), Globals.entryTypesManager);

for (BibEntry entry : entries) {
try {
bibtexEntryWriter.write(entry, bibWriter, databaseContext.getMode());
} catch (IOException e) {
LOGGER.warn("Problem creating BibTeX file for mailing.", e);
}
}

return rawEntries.toString();
}
}
4 changes: 3 additions & 1 deletion src/main/java/org/jabref/gui/actions/StandardActions.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ public enum StandardActions implements Action {
CUT(Localization.lang("Cut"), IconTheme.JabRefIcons.CUT, KeyBinding.CUT),
DELETE(Localization.lang("Delete"), IconTheme.JabRefIcons.DELETE_ENTRY),
DELETE_ENTRY(Localization.lang("Delete entry"), IconTheme.JabRefIcons.DELETE_ENTRY, KeyBinding.DELETE_ENTRY),
SEND_AS_EMAIL(Localization.lang("Send as email"), IconTheme.JabRefIcons.EMAIL),
SEND(Localization.lang("Send"), IconTheme.JabRefIcons.EMAIL),
SEND_AS_EMAIL(Localization.lang("As Email")),
SEND_TO_KINDLE(Localization.lang("To Kindle")),
REBUILD_FULLTEXT_SEARCH_INDEX(Localization.lang("Rebuild fulltext search index"), IconTheme.JabRefIcons.FILE),
OPEN_EXTERNAL_FILE(Localization.lang("Open file"), IconTheme.JabRefIcons.FILE, KeyBinding.OPEN_FILE),
OPEN_URL(Localization.lang("Open URL or DOI"), IconTheme.JabRefIcons.WWW, KeyBinding.OPEN_URL_OR_DOI),
Expand Down
21 changes: 17 additions & 4 deletions src/main/java/org/jabref/gui/maintable/RightClickMenu.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
import org.jabref.gui.ClipBoardManager;
import org.jabref.gui.DialogService;
import org.jabref.gui.LibraryTab;
import org.jabref.gui.SendAsEMailAction;
import org.jabref.gui.SendAsKindleEmailAction;
import org.jabref.gui.SendAsStandardEmailAction;
import org.jabref.gui.StateManager;
import org.jabref.gui.actions.ActionFactory;
import org.jabref.gui.actions.StandardActions;
Expand Down Expand Up @@ -57,9 +58,7 @@ public static ContextMenu create(BibEntryTableViewModel entry,

new SeparatorMenuItem(),

factory.createMenuItem(StandardActions.SEND_AS_EMAIL, new SendAsEMailAction(dialogService, preferencesService, stateManager)),

new SeparatorMenuItem(),
createSendSubMenu(factory, dialogService, stateManager, preferencesService),

SpecialFieldMenuItemFactory.createSpecialFieldMenu(SpecialField.RANKING, factory, libraryTab.frame(), dialogService, preferencesService, undoManager, stateManager),
SpecialFieldMenuItemFactory.getSpecialFieldSingleItem(SpecialField.RELEVANCE, factory, libraryTab.frame(), dialogService, preferencesService, undoManager, stateManager),
Expand Down Expand Up @@ -122,4 +121,18 @@ private static Menu createCopySubMenu(ActionFactory factory,

return copySpecialMenu;
}

private static Menu createSendSubMenu(ActionFactory factory,
DialogService dialogService,
StateManager stateManager,
PreferencesService preferencesService) {
Menu sendMenu = factory.createMenu(StandardActions.SEND);
sendMenu.getItems().addAll(
factory.createMenuItem(StandardActions.SEND_AS_EMAIL, new SendAsStandardEmailAction(dialogService, preferencesService, stateManager)),
factory.createMenuItem(StandardActions.SEND_TO_KINDLE, new SendAsKindleEmailAction(dialogService, preferencesService, stateManager)),
new SeparatorMenuItem()
);

return sendMenu;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
<Label text="%Subject for sending an email with references"/>
<TextField fx:id="eMailReferenceSubject" minWidth="150.0" HBox.hgrow="ALWAYS"/>
</HBox>
<HBox alignment="CENTER_LEFT" spacing="10.0">
<Label text="%Email address for sending to Kindle"/>
<TextField fx:id="kindleEmail" minWidth="150.0" HBox.hgrow="ALWAYS"/>
</HBox>
<CheckBox fx:id="autoOpenAttachedFolders" text="%Automatically open folders of attached files"/>

<Label styleClass="sectionHeader" text="%Push applications"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class ExternalTab extends AbstractPreferenceTabView<ExternalTabViewModel>
@FXML private CheckBox useCustomFileBrowser;
@FXML private TextField customFileBrowserCommand;
@FXML private Button customFileBrowserBrowse;
@FXML private TextField kindleEmail;

private final ControlsFxVisualizer validationVisualizer = new ControlsFxVisualizer();

Expand Down Expand Up @@ -68,6 +69,8 @@ public void initialize() {
customFileBrowserCommand.disableProperty().bind(useCustomFileBrowser.selectedProperty().not());
customFileBrowserBrowse.disableProperty().bind(useCustomFileBrowser.selectedProperty().not());

kindleEmail.textProperty().bindBidirectional(viewModel.kindleEmailProperty());

validationVisualizer.setDecoration(new IconValidationDecorator());
Platform.runLater(() -> {
validationVisualizer.initVisualization(viewModel.terminalCommandValidationStatus(), customTerminalCommand);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public class ExternalTabViewModel implements PreferenceTabViewModel {
private final StringProperty customTerminalCommandProperty = new SimpleStringProperty("");
private final BooleanProperty useCustomFileBrowserProperty = new SimpleBooleanProperty();
private final StringProperty customFileBrowserCommandProperty = new SimpleStringProperty("");
private final StringProperty kindleEmailProperty = new SimpleStringProperty("");

private final Validator terminalCommandValidator;
private final Validator fileBrowserCommandValidator;
Expand Down Expand Up @@ -100,6 +101,7 @@ public void setValues() {
customTerminalCommandProperty.setValue(initialExternalApplicationPreferences.getCustomTerminalCommand());
useCustomFileBrowserProperty.setValue(initialExternalApplicationPreferences.useCustomFileBrowser());
customFileBrowserCommandProperty.setValue(initialExternalApplicationPreferences.getCustomFileBrowserCommand());
kindleEmailProperty.setValue(initialExternalApplicationPreferences.getKindleEmail());
}

public void storeSettings() {
Expand All @@ -111,6 +113,7 @@ public void storeSettings() {
externalPreferences.setCustomTerminalCommand(customTerminalCommandProperty.getValue());
externalPreferences.setUseCustomFileBrowser(useCustomFileBrowserProperty.getValue());
externalPreferences.setCustomFileBrowserCommand(customFileBrowserCommandProperty.getValue());
externalPreferences.setKindleEmail(kindleEmailProperty.getValue());

PushToApplicationPreferences pushPreferences = preferences.getPushToApplicationPreferences();
pushPreferences.setActiveApplicationName(selectedPushToApplicationProperty.getValue().getDisplayName());
Expand Down Expand Up @@ -182,6 +185,10 @@ public StringProperty eMailReferenceSubjectProperty() {
return this.eMailReferenceSubjectProperty;
}

public StringProperty kindleEmailProperty() {
return this.kindleEmailProperty;
}

public BooleanProperty autoOpenAttachedFoldersProperty() {
return this.autoOpenAttachedFoldersProperty;
}
Expand Down
Loading