This is a new version of my Search Mega Action, that adds a text field to the prompt that will either be pre-filled with the draft, the selected text, or be blank. This means the action can be run on a blank draft without creating a new draft. I used bits of script from an action by @nahumck, who also helped clean the code up in the original action. https://actions.getdrafts.com/a/1Jl
These lists of services are a good idea.
One way of making it slightly easier and quicker to edit the list might be to drop:
- the structure of
if ( ... ) { assignment }
conditions, and - the need to name a service in two different places (
actions
list, and repeatedbuttonPressed
conditions);
replacing these with a single list of (name, url)
pairs, like:
// services :: [(String, String)]
const services = [
[
'Amazon',
'http://amazon.com/s?ie=UTF8&index=blended&keywords='
],
[
'AppStore',
'itms-apps://search.itunes.apple.com/WebObjects/' +
'MZSearch.woa/wa/search?media=software&term='
],
[
'DuckDuckGo',
'https://duckduckgo.com/?q='
],
[
'Google',
'https://google.com/search?q='
],
[
'GoogleImages',
'https://google.com/search?tbm=isch&q='
],
[
'GoogleTranslate',
'http://translate.google.com/#auto/en/'
],
[
'IMDB',
'https://www.imdb.com/find?q='
],
[
'iTunes',
'itms:/search?term='
],
[
'RottenTomatoes',
'http://www.rottentomatoes.com/search/?search='
],
[
'Twitter',
'https://twitter.com/search?q='
],
[
'Wikipedia',
'https://en.wikipedia.org/wiki/Special:Search?search='
],
[
'WolframAlpha',
'http://wolframalpha.com/input/?i='
],
[
'YouTube',
'http://www.youtube.com/results?search_query='
]
];
A list of pairs like this can be automatically converted to a JS Map
object.
const serviceMap = new Map(services);
which gives us short-cut methods like .keys()
, .values()
, and .get()
So if we use a generic menuChoice
function like:
// AN ITEM CHOSEN FROM A MENU
// menuChoice :: Bool -> String -> String -> [String] -> String
const menuChoice = (withCancelButton, title, msg, items) => {
const p = items.reduce(
(a, item) => (a.addButton(item), a),
Object.assign(
Prompt.create(), {
title: title,
message: msg,
isCancellable: withCancelButton
}
)
);
return items.length > 0 ? (
p.show() ? p.buttonPressed : 'Cancel'
) : 'Empty menu';
};
We can immediately .get()
the url matching a chosen service name:
const maybeURL = serviceMap.get(
menuChoice(
true,
'Search on chosen web service',
'Choose:', [...serviceMap.keys()]
)
)
(If the user cancelled, then the value of maybeURL will be the special value: undefined
)
So for a less ambitious variant of your action, in which we just submit any selected text to the web search service, we can then write:
return maybeURL !== undefined ? (
app.openURL(
maybeURL + encodeURIComponent(
// Selected text (if any)
e.getSelectedText().trim() ||
// otherwise line containing cursor.
e.getTextInRange(
...e.getSelectedLineRange()
)
)
)
) : console.log('User cancelled ...');
i.e. in full, something along the lines of:
(() => {
'use strict';
const main = () => {
const
e = editor,
serviceMap = new Map(services),
maybeURL = serviceMap.get(
menuChoice(
true,
'Search on chosen web service',
'Choose:', [...serviceMap.keys()]
)
);
return maybeURL !== undefined ? (
app.openURL(
maybeURL + encodeURIComponent(
// Selected text (if any)
e.getSelectedText().trim() ||
// otherwise line containing cursor.
e.getTextInRange(
...e.getSelectedLineRange()
)
)
)
) : console.log('User cancelled ...');
};
// services :: [(String, String)]
const services = [
[
'Amazon',
'http://amazon.com/s?ie=UTF8&index=blended&keywords='
],
[
'AppStore',
'itms-apps://search.itunes.apple.com/WebObjects/' +
'MZSearch.woa/wa/search?media=software&term='
],
[
'DuckDuckGo',
'https://duckduckgo.com/?q='
],
[
'Google',
'https://google.com/search?q='
],
[
'GoogleImages',
'https://google.com/search?tbm=isch&q='
],
[
'GoogleTranslate',
'http://translate.google.com/#auto/en/'
],
[
'IMDB',
'https://www.imdb.com/find?q='
],
[
'iTunes',
'itms:/search?term='
],
[
'RottenTomatoes',
'http://www.rottentomatoes.com/search/?search='
],
[
'Twitter',
'https://twitter.com/search?q='
],
[
'Wikipedia',
'https://en.wikipedia.org/wiki/Special:Search?search='
],
[
'WolframAlpha',
'http://wolframalpha.com/input/?i='
],
[
'YouTube',
'http://www.youtube.com/results?search_query='
]
];
// AN ITEM CHOSEN FROM A MENU
// menuChoice :: Bool -> String -> String -> [String] -> String
const menuChoice = (withCancelButton, title, msg, items) => {
const p = items.reduce(
(a, item) => (a.addButton(item), a),
Object.assign(
Prompt.create(), {
title: title,
message: msg,
isCancellable: withCancelButton
}
)
);
return items.length > 0 ? (
p.show() ? p.buttonPressed : 'Cancel'
) : 'Empty menu';
};
// MAIN ---
return main();
})();
Cool, and thanks. I’m really, really new to JavaScript, so I’m still learning.
Here, FWIW, is a variant of your action which aims for a bit of extra flexibility by taking a different approach:
- removing the list of services from the code of the script step,
- placing it in a draft (in markdown link format) and
- giving the script step just the UUID of the draft which contains the list.
(We can get the UUID of a draft through the information icon at top left – the information panel has a Copy Link to Draft control)
The draft contents might look something like this (based on your action, and making just a few small edits to ensure that they are all https://
, rather than the less secure http://
)
[Amazon](https://amazon.com/s?ie=UTF8&index=blended&keywords=)
[App Store](itms-apps://search.itunes.apple.com/WebObjects/MZSearch.woa/wa/search?media=software&term=)
[DuckDuckGo](https://duckduckgo.com/?q=)
[Google](https://google.com/search?q=)
[Google Images](https://google.com/search?tbm=isch&q=)
[Google Translate](https://translate.google.com/#auto/en/)
[IMDB](https://www.imdb.com/find?q=)
[iTunes](itms:/search?term=)
[Rotten Tomatoes](https://www.rottentomatoes.com/search/?search=)
[Twitter](https://twitter.com/search?q=)
[Wikipedia](https://en.wikipedia.org/wiki/Special:Search?search=)
[Wolfram Alpha](https://www.wolframalpha.com/input/?i=)
[YouTube](https://www.youtube.com/results?search_query=)
JS source, which can be amended to contain a UUID from a draft on the user’s device:
http://actions.getdrafts.com/a/1Jq
(() => {
'use strict';
/* Edit to provide UUID of a draft containing MD links with
named search expressions up to the '=' sign.
e.g.
[DuckDuckGo](https://duckduckgo.com/?q=)
*/
// SERVICES EITHER FROM MARKDOWN LINKS IN THIS DRAFT:
const uuidLinksDraft =
'9A15F571-0871-4DF7-A1F3-2E5464F197A3';
// OR IF DRAFT NOT FOUND BY THAT UUID, THEN FROM THESE DEFAULTS:
const fallbackDefaults = `
[Amazon](https://amazon.com/s?ie=UTF8&index=blended&keywords=)
[App Store](itms-apps://search.itunes.apple.com/WebObjects/MZSearch.woa/wa/search?media=software&term=)
[DuckDuckGo](https://duckduckgo.com/?q=)
[Google](https://google.com/search?q=)
[Google Images](https://google.com/search?tbm=isch&q=)
[Google Translate](https://translate.google.com/#auto/en/)
[IMDB](https://www.imdb.com/find?q=)
[iTunes](itms:/search?term=)
[Rotten Tomatoes](https://www.rottentomatoes.com/search/?search=)
[Twitter](https://twitter.com/search?q=)
[Wikipedia](https://en.wikipedia.org/wiki/Special:Search?search=)
[Wolfram Alpha](https://www.wolframalpha.com/input/?i=)
[YouTube](https://www.youtube.com/results?search_query=)`;
const main = () => {
const
d = Draft.find(uuidLinksDraft),
lrResult = bindLR(
bindLR(
// LIST OF SERVICES EITHER FROM DRAFT
// WITH GIVEN UUID OR FROM DEFAULTS ABOVE
Right(
d !== undefined ? [
'List found in draft:\n' +
uuidLinksDraft,
d.content
] : [
'No draft found by uuid:\n' +
uuidLinksDraft +
'\n(using default service list)',
fallbackDefaults
]
),
([sourceName, strLinks]) => Right([
sourceName,
new Map(
regexMatches(
/\[(.*)\]\((.*)\)/g,
strLinks
)
.map(x => [x[1], x[2]])
)
])
),
([sourceName, serviceMap]) => {
const e = editor;
return bindLR(
textAndMenuChoiceLR(
true, 'Search with chosen service',
sourceName,
'Term:',
// SELECTION OR CURRENT LINE
e.getSelectedText().trim() ||
e.getTextInRange(
...e.getSelectedLineRange()
), [...serviceMap.keys()]
),
dct => {
const maybeURL = serviceMap
.get(dct.choice);
return maybeURL !== undefined ? (
Right(
app.openURL(
maybeURL +
encodeURIComponent(
dct.text
)
)
)
) : Left(
'Service not found: ' +
dct.choice
);
}
);
}
);
const strLeft = lrResult.Left;
return strLeft && strLeft.includes('uuid') ? (
alert(strLeft)
) : console.log(lrResult.Left || lrResult.Right)
};
// A CONFIRMED TEXT, AND AN ITEM CHOSEN FROM A MENU
// textAndMenuChoiceLR :: Bool -> String -> String ->
// String -> String -> [String] ->
// Either String { text: String, choice :: String }
const textAndMenuChoiceLR = (
withCancelButton, title, msg,
strLabel, strText, items) => {
const p = items.reduce(
(a, item) => (a.addButton(item), a),
Object.assign(
Prompt.create(), {
title: title,
message: msg,
isCancellable: withCancelButton
}
)
);
return (
p.addTextField('txt', strLabel, strText),
items.length > 0 ? (
p.show() ? Right({
text: p.fieldValues.txt,
choice: p.buttonPressed
}) : Left('Cancel')
) : Left('Empty menu')
);
};
// GENERIC FUNCTIONS ---
// Left :: a -> Either a b
const Left = x => ({
type: 'Either',
Left: x
});
// Right :: b -> Either a b
const Right = x => ({
type: 'Either',
Right: x
});
// bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
const bindLR = (m, mf) =>
m.Right !== undefined ? (
mf(m.Right)
) : m;
// regexMatches :: String -> String -> [[String]]
const regexMatches = (strRgx, strHay) => {
const rgx = new RegExp(strRgx, 'g');
let m = rgx.exec(strHay),
xs = [];
while (m)(xs.push(m), m = rgx.exec(strHay));
return xs;
};
// MAIN ---
return main();
})();
PS to ease the burden of selecting some text, we can fall back, if no selection is found, to the text of the line which contains the cursor:
const e = editor;
// ...
app.openURL(
maybeURL + encodeURIComponent(
// Selected text (if any)
e.getSelectedText().trim() ||
// otherwise line containing cursor.
e.getTextInRange(
...e.getSelectedLineRange()
)
)
)
I had written something similar that reads titles & urls from a csv. It occurred to me that this approach would allow a degree of customisation of workflows for non-subscribers.
Can anyone suggest a hack to make this open mobile Safari, rather than use the in-app browser? I realize there’s an option to switch to Safari once the in-app browser opens, but I’d like to cut out the extra step.
This is only an issue re: iOS. In Drafts-for-desktop, the action does open Safari.
Change the second parameter of openURL to false.
See the scripting docs here - https://scripting.getdrafts.com/classes/app#openurl
Thanks, I never imagined it would be so easy!
I’m going to keep tweaking this script as a way to teach myself…
Below is the same script as jmreekes’, but with lots more searches added. Like the original script, it launches an in-app browser in mobile (not Safari). I’ve listed the sites for convenience above the script:
Amazon
Amazon Orders
AppStore
BIu-Ray.com
DuckDuckGo
eBay Completed
eBay
Facebook
Forvo Pronunciation
Google
Google Images
GoogleTranslate
IMDB
iTunes
JustWatch.com
Macupdate
Movielens
MRQE
Netflix
Pinboard
RottenTomatoes
Translate English -> Portuguese
Translate English -> Spanish
Translate Portuguese -> English
Translate Spanish -> English
Twitter
Twitter Advanced Search (Rick Wilson)
Urban Dictionary
Wikipedia
WolframAlpha
Yelp (zip code 10001)
YouTube
//Mega Multi Web Search
/* Action created by Jimmy Reekes, based on actions created by Tim Nahumck */
var content = draft.processTemplate("[[selection]]")
var p = Prompt.create();
p.title = "Select Search Action";
p.addTextField("search", "Search", content);
var actions = ["Amazon","Amazon Orders","AppStore","BIu-Ray.com","DuckDuckGo","eBay Completed","eBay","Facebook","Forvo Pronunciation","Google","GoogleImages","GoogleTranslate","IMDB","iTunes","Jim Leff's Slog","JustWatch.com","Macupdate","Movielens","MRQE","Netflix","Pinboard","RottenTomatoes","Sepinwall Rolling Stone","Translate English -> Portuguese","Translate English -> Spanish","Translate Portuguese -> English","Translate Spanish -> English","Twitter","Twitter Advanced Search (Rick Wilson)","Twitter Must-Read","Urban Dictionary","Wikipedia","WolframAlpha","Yelp (10001)","Yelp (10570)","YouTube"];
for (action of actions) {
p.addButton(action);
}
var con = p.show();
//Actions based on button presses
if (con) {
var input = p.fieldValues["search"];
var output = encodeURIComponent(input);
var state = "true";
if (p.buttonPressed == "Amazon") {
var url = "http://amazon.com/s?ie=UTF8&index=blended&keywords="+output;
}
if (p.buttonPressed == "Amazon Orders") {
var url = "https://www.amazon.com/gp/your-account/order-history/ref=ppx_yo_dt_b_search?opt=ab&search="+output;
}
if (p.buttonPressed == "AppStore") {
var url = "itms-apps://search.itunes.apple.com/WebObjects/MZSearch.woa/wa/search?media=software&term="+output;
var state = "";
}
if (p.buttonPressed == "BIu-Ray.com") {
var url = "https://www.blu-ray.com/search/?quicksearch=1&quicksearch_country=US&quicksearch_keyword="+output;
}
if (p.buttonPressed == "DuckDuckGo"){
var url = "https://duckduckgo.com/?q="+output;
}
if (p.buttonPressed == "eBay") {
var url = "https://www.ebay.com/sch/i.html?_nkw="+output;
}
if (p.buttonPressed == "eBay Completed") {
var url = "https://www.ebay.com/sch/i.html?_from=R40&_sacat=0&rt=nc&LH_Complete=1&_nkw="+output;
}
if (p.buttonPressed == "Facebook") {
var url = "https://www.facebook.com/search/top/?q="+output;
}
if (p.buttonPressed == "Forvo Pronunciation") {
var url = "https://forvo.com/search/"+output;
}
if (p.buttonPressed == "Google") {
var url = "https://google.com/search?q="+output;
}
if (p.buttonPressed == "GoogleImages") {
var url = "https://google.com/search?tbm=isch&q="+output;
}
if (p.buttonPressed == "GoogleTranslate") {
var url = "http://translate.google.com/#auto/en/"+output;
}
if (p.buttonPressed == "IMDB") {
var url = "https://www.imdb.com/find?q="+output;
}
if (p.buttonPressed == "iTunes") {
var url = "itms:/search?term="+output;
var state = "false";
}
if (p.buttonPressed == "Jim Leff's Slog") {
var url = "https://jimleff.blogspot.com/search?q="+output;
}
if (p.buttonPressed == "JustWatch.com") {
var url = "https://www.justwatch.com/us/search?q="+output;
}
if (p.buttonPressed == "Macupdate") {
var url = "https://www.macupdate.com/find/mac/context%3D"+output;
}
if (p.buttonPressed == "Movielens") {
var url = "https://twitter.com/search?%20list%3A834895775314415616&src=typed_query&f=live&q="+output;
}
if (p.buttonPressed == "MRQE") {
var url = "https://www.mrqe.com/search?utf8=✓&q="+output;
}
if (p.buttonPressed == "Netflix") {
var url = "https://www.netflix.com/search?q="+output;
}
if (p.buttonPressed == "Pinboard") {
var url = "https://pinboard.in/search/u:Jimbo?query="+output;
}
if (p.buttonPressed == "RottenTomatoes") {
var url = "http://www.rottentomatoes.com/search/?search="+output;
}
if (p.buttonPressed == "Sepinwall Rolling Stone") {
var url = "https://www.rollingstone.com/results/#?q=sepinwall%20"+output;
}
if (p.buttonPressed == "Translate English -> Portuguese") {
var url = "https://translate.google.com/#view=home&op=translate&sl=en&tl=pt&text="+output;
}
if (p.buttonPressed == "Translate English -> Spanish") {
var url = "https://translate.google.com/#view=home&op=translate&sl=en&tl=es&text="+output;
}
if (p.buttonPressed == "Translate Portuguese -> English") {
var url = "https://translate.google.com/#view=home&op=translate&sl=pt&tl=en&text="+output;
}
if (p.buttonPressed == "Translate Spanish -> English") {
var url = "https://translate.google.com/#view=home&op=translate&sl=es&tl=en&text="+output;
}
if (p.buttonPressed == "Twitter") {
var url = "https://twitter.com/search?q="+output;
var state = "";
}
if (p.buttonPressed == "Twitter Advanced Search - Rick Wilson") {
var url =
"https://twitter.com/search?q="+output;
var state = "";
}
if (p.buttonPressed == "Twitter Must-Read") {
var url = "https://twitter.com/search?q=list%3A834895775314415616%20"+output;
}
if (p.buttonPressed == "Urban Dictionary") {
var url = "https://www.urbandictionary.com/define.php?term="+output;
}
if (p.buttonPressed == "Wikipedia") {
var url = "https://en.wikipedia.org/wiki/Special:Search?search="+output;
}
if (p.buttonPressed == "WolframAlpha") {
var url = "http://wolframalpha.com/input/?i="+output;
}
if (p.buttonPressed == "Yelp (10570)") {
var url = "https://www.yelp.com/search?find_loc=10570&find_desc="+output;
}
if (p.buttonPressed == "Yelp (10001)") {
var url = "https://www.yelp.com/search?find_loc=10001&find_desc="+output;
}
if (p.buttonPressed == "YouTube") {
var url = "http://www.youtube.com/results?search_query="+output;
var state = ""
}
var result = app.openURL(url, state);
}
else {
context.cancel();
}
One problem with my script, just above. One search actually wants two parameters: text string, plus Twitter account to search via their advanced search. It’s currently configured to one arbitrary Twitter account. Can anyone come up with a way to input both parameters for only this particular search?
Here’s how I have it presently:
if (p.buttonPressed == "Twitter Advanced Search - Rick Wilson") {
var url =
"https://twitter.com/search?q="+output;
var state = "";