Action
launcher core
This is an update on the music launcher I posted, adding:
- Conditional display: Items can be shown or hidden based on conditions like time of day, calendar events, or draft queries
- Shortcuts integration: Use shortcuts:Shortcut Name syntax to trigger iOS Shortcuts e.g. [link title](shortcuts:Shortcut Name)— no need for manual URL encoding!
- Drafts actions: Use drafts:Action Name syntax to run Drafts actions directly e.g. [link title](drafts:Action Name)
- Edit button: Click the ≡ button (top right) to open the source draft for editing
- Slight tweaks for legibility
You can use this version in the same way you might have used the earlier URL launcher, but it does have a bit more code for conditional checks (drafts queries and calendars).
The action renders an interface based on a list of URLs in a source draft, organised by Markdown headings. The first heading in the list is unfolded; entries under other headings are folded by default. Headings with no links underneath them aren’t rendered. The interface parses #tags and offers the ability to filter the visible list by those tags, with a search bar for filtering by item titles.
I’m using this version to make menus on the fly, particularly a menu for some of my most used actions that don’t fit in my action bar, some of which only need to be available under certain conditions.
## Start-up
+ [Morning Breathwork](shortcuts:Morning Breathwork) CONDITIONAL: noEventWithText("@(breathwork)")
+ [Initialise daily note](drafts:Today's journal) CONDITIONAL: noDailyNote()
## Main Menu
+ [Today's Journal](drafts:Today's journal)
+ [Music](drafts:music launcher)
+ [Shopping List](drafts://open?uuid=A8FFD648-A510-4CC2-90C8-297D624AA340)
+ [Log bodywork](drafts:log bodywork)
Configuration:
Update this line and point it to the source draft for your launcher:
const LAUNCHER_DRAFT_TITLE = "::launcher";
To check specific calendars for conditional events (leave empty array to check all):
const CALENDARS_TO_CHECK = ["Work", "Personal"];
Font families can be customised if you don’t have Hoefler or Roboto Mono installed.
Conditional syntax:
Add CONDITIONAL: functionName() at the end of any line to control visibility:
e.g.
+ [Morning Breathwork](shortcuts:Breathwork) CONDITIONAL: noEventWithText("@(breathwork)")
+ [Initialise daily note](drafts:Daily Note) CONDITIONAL: noDailyNote()
+ [Morning Routine](shortcuts:Morning) CONDITIONAL: isTimeRange(5, 12)
+ [Weekly Review](shortcuts:Review) CONDITIONAL: isDayOfWeek(0)
Available conditional functions:
∙ noDailyNote() - Show only if no draft tagged “daily note” exists from today
∙ noEventWithText(“text”) - Show only if no calendar event today contains the text
∙ hasEventWithText(“text”) - Show only if a calendar event today contains the text
∙ isTimeRange(startHour, endHour) - Show only during specified hours (24-hour format)
∙ isDayOfWeek(dayNum) - Show only on specific day (0=Sunday, 6=Saturday)
∙ isWeekday() - Show only Monday-Friday
∙ isWeekend() - Show only Saturday-Sunday
∙ hasTaggedDraft(“tag”) - Show only if drafts with specified tag exist
Interface:
Full screen interface. Click the × in the top-right corner to cancel.
Steps
-
script
// ======================================== // URL LAUNCHER SCRIPT with CONDITIONAL ITEMS: CORE SCRIPT // ======================================== // ---------- PERFORMANCE TIMING ---------- const perfStart = new Date().getTime(); function logPerf(label) { const elapsed = new Date().getTime() - perfStart; console.log("[" + elapsed + "ms] " + label); } logPerf("Script start"); // ---------- LOGGER ---------- function log(s) { if (DEBUG) console.log(s); } // ---------- ONE-RUN CACHES ---------- const _cache = { draftQueryByTag: {}, // key: "scope|tag" -> drafts[] calendarList: null, // calendars[] eventsByCalKey: {}, // key: cal.identifier||cal.title -> events[] actionURLByName: {}, // key: raw actionName -> resolved URL todayRangeCache: null // Cache the today range }; function todayRange() { if (_cache.todayRangeCache) return _cache.todayRangeCache; const now = new Date(); const start = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const end = new Date(start.getTime() + 86400000); _cache.todayRangeCache = { start, end }; return _cache.todayRangeCache; } function queryDraftsByTag(scope, tag) { const key = scope + "|" + tag; if (_cache.draftQueryByTag[key]) return _cache.draftQueryByTag[key]; const results = Draft.query("", scope, [tag]) || []; _cache.draftQueryByTag[key] = results; return results; } // ---------- CALENDAR HELPERS (CACHED + LAZY) ---------- function getCalendarsToCheck() { if (_cache.calendarList) return _cache.calendarList; if (CALENDARS_TO_CHECK.length === 0) { _cache.calendarList = Calendar.getAllCalendars(); } else { _cache.calendarList = CALENDARS_TO_CHECK .map(function(name) { return Calendar.findOrCreate(name); }) .filter(function(cal) { return cal !== null; }); } return _cache.calendarList; } function getEventsForCalendarToday(cal) { const key = cal.identifier || cal.title || String(cal); if (_cache.eventsByCalKey[key]) return _cache.eventsByCalKey[key]; const r = todayRange(); const events = cal.events(r.start, r.end) || []; _cache.eventsByCalKey[key] = events; return events; } // ---------- CONDITIONAL FUNCTIONS ---------- const CONDITIONS = { // Check if no draft exists matching tag(s) and/or title pattern created today noDraftTodayMatching: function() { const r = todayRange(); const args = Array.prototype.slice.call(arguments); if (args.length === 0) return true; let titlePattern = null; let tags = args; if (args.length > 0 && (args[args.length - 1].includes(" ") || args[args.length - 1].includes("»"))) { titlePattern = args[args.length - 1]; tags = args.slice(0, -1); } log("\n=== noDraftTodayMatching() Debug ==="); log("Tags: " + (tags.length ? tags.join(", ") : "none")); log("Title pattern: " + (titlePattern || "none")); let results; if (tags.length > 0) { results = queryDraftsByTag("inbox", tags[0]); for (let i = 1; i < tags.length; i++) { results = results.filter(function(d) { return d.tags.includes(tags[i]); }); } } else { results = Draft.query("", "inbox", []) || []; } log("Found " + results.length + " drafts matching tags"); if (titlePattern) { results = results.filter(function(d) { return d.title.includes(titlePattern); }); log("After title filter: " + results.length + " drafts"); } const hasToday = results.some(function(d) { const createdDate = new Date(d.createdAt); const isToday = createdDate >= r.start && createdDate < r.end; if (DEBUG && isToday) { log(' Draft: "' + d.title + '"'); log(" Created: " + createdDate); } return isToday; }); log("Has today's matching draft: " + hasToday); log("Should show item: " + (!hasToday)); log("======================\n"); return !hasToday; }, // Check if no calendar event today contains specific text in title noEventWithText: function(searchText) { const calendars = getCalendarsToCheck(); for (let i = 0; i < calendars.length; i++) { const cal = calendars[i]; const events = getEventsForCalendarToday(cal); const hasEvent = events.some(function(e) { return (e.title || "").includes(searchText); }); if (hasEvent) return false; } return true; }, // Check if any calendar event today contains specific text in title hasEventWithText: function(searchText) { const calendars = getCalendarsToCheck(); for (let i = 0; i < calendars.length; i++) { const cal = calendars[i]; const events = getEventsForCalendarToday(cal); const hasEvent = events.some(function(e) { return (e.title || "").includes(searchText); }); if (hasEvent) return true; } return false; }, // Check time-based conditions isTimeRange: function(startHour, endHour) { const now = new Date(); const hour = now.getHours(); return hour >= startHour && hour < endHour; }, // Check if specific tag exists in drafts hasTaggedDraft: function(tag) { const results = queryDraftsByTag("inbox", tag); return results.length > 0; }, // Day of week check (0 = Sunday, 6 = Saturday) isDayOfWeek: function(dayNum) { return new Date().getDay() === dayNum; }, // Check if it's a weekday (Monday-Friday) isWeekday: function() { const day = new Date().getDay(); return day >= 1 && day <= 5; }, // Check if it's a weekend (Saturday-Sunday) isWeekend: function() { const day = new Date().getDay(); return day === 0 || day === 6; } }; // ---------- PARSE CONDITIONAL SYNTAX ---------- function parseConditional(condString) { // Parse syntax like: noDraftTodayMatching("tag") or noEventWithText("@(breathwork)") const match = condString.match(/^(\w+)\((.*?)\)$/); if (!match) return null; const funcName = match[1]; const argsString = match[2]; if (!CONDITIONS[funcName]) { log("Unknown condition function: " + funcName); return null; } // Parse arguments (handle quoted strings) const args = []; if (argsString) { const argMatches = argsString.match(/"([^"]*)"|'([^']*)'|(\d+)/g); if (argMatches) { argMatches.forEach(function(arg) { if (arg.startsWith('"') || arg.startsWith("'")) { args.push(arg.slice(1, -1)); // Remove quotes } else { args.push(parseInt(arg)); // Convert numbers } }); } } return { func: CONDITIONS[funcName], args: args }; } // ---------- URL SCHEME PARSERS ---------- function parseShortcutsURL(name) { return "shortcuts://run-shortcut?name=" + encodeURIComponent(name); } function parseDraftsURL(actionName) { if (_cache.actionURLByName[actionName]) return _cache.actionURLByName[actionName]; log("\n=== Drafts URL Parser ==="); log('Looking for action: "' + actionName + '"'); var action = Action.find(actionName); var url; if (action) { log('✓ Found action: "' + action.name + '"'); log(" UUID: " + action.uuid); // /runAction requires text= and action= expects the ACTION NAME url = "drafts://x-callback-url/runAction?text=%20&action=" + encodeURIComponent(action.name); } else { log("✗ Action not found"); url = "drafts://x-callback-url/runAction?text=%20&action=" + encodeURIComponent(actionName); } _cache.actionURLByName[actionName] = url; return url; } logPerf("Functions defined"); // ---------- LOAD LAUNCHER DATA ---------- // First query by tag to reduce search space logPerf("Starting draft lookup"); const launcherDraft = Draft.query("title:"+LAUNCHER_DRAFT_TITLE, "inbox", [LAUNCHER_TAG])[0]; // Then filter by title within that smaller set //const launcherDraft = taggedDrafts.find(function(d) { // return d.title === LAUNCHER_DRAFT_TITLE; //}) || null; logPerf("Launcher draft loaded"); if (!launcherDraft) { alert("No launcher draft found. Please create a draft titled '" + LAUNCHER_DRAFT_TITLE + "'"); context.fail(); } logPerf("Launcher draft loaded"); // Source draft URL (open the launcher draft) const sourceDraftURL = "drafts://open?uuid=" + encodeURIComponent(launcherDraft.uuid); // ---------- PARSE MARKDOWN ---------- const lines = launcherDraft.content.split("\n"); const sections = []; let currentH1 = null; let currentH2 = null; const allTags = {}; const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/; const tagRegex = / #(\w+)/g; const conditionalRegex = /CONDITIONAL:\s*(.+?)(?:\s*$)/; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith("# ")) { currentH1 = line.substring(2).trim(); currentH2 = null; } else if (line.startsWith("## ")) { currentH2 = line.substring(3).trim(); sections.push({ h1: currentH1, h2: currentH2, items: [] }); } else if (currentH2 && (line.startsWith("+ [") || line.startsWith("- ["))) { const linkMatch = line.match(linkRegex); if (linkMatch) { const label = linkMatch[1]; let url = linkMatch[2]; // Handle shortcuts: pseudo-URLs if (url.startsWith("shortcuts:")) { const shortcutName = url.substring(10).trim(); url = parseShortcutsURL(shortcutName); } // Handle drafts: pseudo-URLs else if ((url.startsWith("drafts:") || url.startsWith("Drafts:")) && !url.startsWith("drafts://") && !url.startsWith("Drafts://")) { const actionName = url.substring(7).trim(); url = parseDraftsURL(actionName); } // Extract tags (but don't include them in conditional check) const tags = []; let tagMatch; const lineBeforeConditional = line.split("CONDITIONAL:")[0]; tagRegex.lastIndex = 0; while ((tagMatch = tagRegex.exec(lineBeforeConditional)) !== null) { const tag = tagMatch[1].toLowerCase(); tags.push(tag); allTags[tag] = (allTags[tag] || 0) + 1; } // Extract and evaluate conditional let shouldShow = true; const conditionalMatch = line.match(conditionalRegex); if (conditionalMatch) { const condString = conditionalMatch[1].trim(); const parsed = parseConditional(condString); if (parsed) { try { shouldShow = parsed.func.apply(null, parsed.args); log("Condition '" + condString + "' for '" + label + "': " + (shouldShow ? "SHOW" : "HIDE")); } catch(e) { log("Error evaluating condition '" + condString + "': " + e.message); shouldShow = true; // Show item on error } } } // Only add item if condition is met if (shouldShow) { sections[sections.length - 1].items.push({ label: label, url: url, tags: tags }); } else { log("Item '" + label + "' hidden by condition"); } } } } logPerf("Markdown parsed - " + sections.length + " sections, " + sections.reduce(function(sum, sec) { return sum + sec.items.length; }, 0) + " items"); // Sort tags by frequency const sortedTags = Object.keys(allTags).sort(function(a, b) { return allTags[b] - allTags[a]; }); logPerf("Tags sorted"); // ---------- HELPERS ---------- function esc(s) { return (s || "") .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/'/g, "'") .replace(/"/g, """); } function isAppURL(url) { return !url.startsWith("http://") && !url.startsWith("https://"); } // ---------- CSS ---------- const css = ` *{box-sizing:border-box;margin:0;padding:0} body{background:#000;color:#fff;font-family:${TITLE_FONT};font-size:16px;line-height:1.6;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} ::selection{background:rgba(255,255,255,0.12)} .page{max-width:700px;margin:0 auto;padding:60px 40px 100px;min-height:100vh;position:relative} @media(max-width:640px){.page{padding:40px 30px 80px}} .close-btn{position:fixed;top:20px;right:20px;width:36px;height:36px;border-radius:50%;background:#1a1a1a;border:1px solid #333;color:#e06a70;font-size:20px;line-height:34px;text-align:center;cursor:pointer;transition:all .2s ease;z-index:1000} .close-btn:hover{background:#e06a70;color:#000;border-color:#e06a70;transform:rotate(90deg)} .source-btn{position:fixed;top:20px;right:64px;width:36px;height:36px;border-radius:50%;background:#1a1a1a;border:1px solid #333;color:#5AAE91;font-size:18px;line-height:34px;text-align:center;text-decoration:none;cursor:pointer;transition:all .2s ease;z-index:1000} .source-btn:hover{background:#5AAE91;color:#000;border-color:#5AAE91;transform:rotate(90deg)} .header{margin-bottom:30px;padding-bottom:20px;border-bottom:1px solid #444} .h1{font-size:18px;font-weight:500;letter-spacing:3px;text-transform:uppercase;margin-bottom:10px;color:#fff} .subtitle{font-size:13px;color:#888;font-style:italic;margin-bottom:20px} .search-container{position:sticky;top:0;background:#000;padding:16px 0 20px;z-index:100;border-bottom:1px solid #222;margin-bottom:20px} .search-bar{width:100%;padding:14px 18px;background:#0a0a0a;border:1px solid #333;border-radius:8px;color:#fff;font-size:15px;font-family:${TITLE_FONT};transition:all .2s ease} .search-bar:focus{outline:none;border-color:#5AAE91;background:#111} .search-bar::placeholder{color:#555} .tag-pills{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:30px} .tag-pill{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:#0a0a0a;border:1px solid #222;border-radius:16px;font-size:12px;color:#888;cursor:pointer;transition:all .2s ease;font-family:${MONO_FONT}} .tag-pill:hover{border-color:#444;color:#aaa} .tag-pill.active{background:#5AAE91;border-color:#5AAE91;color:#000} .tag-count{opacity:0.6;font-size:11px} .section{margin-bottom:30px} .section.hidden{display:none} .section-header{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:16px;padding-bottom:10px;border-bottom:1px solid #222;cursor:pointer;transition:border-color .2s ease} .section-header:hover{border-bottom-color:#444} .section-header.kb-selected{border-bottom-color:#5AAE91;background:rgba(90,174,145,0.1)} .section-title{font-size:15px;font-weight:500;letter-spacing:1px;text-transform:uppercase;color:#aaa} .section-count{font-size:13px;color:#555;font-family:${MONO_FONT}} .section-content{max-height:0;overflow:hidden;transition:max-height .3s ease} .section.open .section-content{max-height:2000px} .h1-label{font-size:11px;letter-spacing:1.2px;text-transform:uppercase;color:#666;margin-bottom:20px;padding-top:10px} .launcher-items{display:flex;flex-direction:column;gap:10px;padding-right:6px} .launcher-item{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:12px 16px;background:#0a0a0a;border:1px solid #1a1a1a;border-radius:6px;cursor:pointer;transition:all .2s ease;text-decoration:none;color:#e0e0e0} .launcher-item:hover{background:#1a1a1a;border-color:#333;transform:translateX(4px)} .launcher-item.kb-selected{background:#1a1a1a;border-color:#5AAE91;transform:translateX(4px)} .launcher-item.hidden{display:none} .launcher-label{font-size:18px;font-weight:400;flex:1 1 auto;min-width:0} .item-tags{display:flex;gap:6px;flex-wrap:wrap} .item-tag{font-size:10px;padding:3px 8px;background:#111;border:1px solid #222;border-radius:10px;color:#666;font-family:${MONO_FONT}} .no-results{text-align:center;padding:40px 20px;color:#666;font-style:italic;display:none} .no-results.visible{display:block} `; // ---------- BUILD HTML ---------- const totalItems = sections.reduce(function(sum, sec) { return sum + sec.items.length; }, 0); let htmlParts = ["<!DOCTYPE html><html><head><meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'><style>" + css + "</style></head><body>\ <div class='page'>\ <div class='close-btn' onclick='Drafts.cancel()'>×</div>\ <a class='source-btn' href='" + esc(sourceDraftURL) + "' title='Open source draft'>≡</a>\ <div class='header'>\ <div class='h1'>Launcher</div>\ <div class='subtitle'>" + totalItems + " items · " + sections.length + " sections</div>\ <div class='search-container'>\ <input type='text' class='search-bar' id='searchBar' placeholder='Type to filter...' autocomplete='off' />\ </div>"]; // Tag pills if (sortedTags.length > 0) { htmlParts.push("<div class='tag-pills' id='tagPills'>"); sortedTags.forEach(function(tag) { const count = allTags[tag]; htmlParts.push("<div class='tag-pill' data-tag='" + esc(tag) + "' onclick='toggleTag(\"" + esc(tag) + "\")'>#" + esc(tag) + "<span class='tag-count'>" + count + "</span></div>"); }); htmlParts.push("</div>"); } htmlParts.push("</div>"); // close header // Group sections by H1 const h1Groups = {}; sections.forEach(function(sec) { if (!h1Groups[sec.h1]) h1Groups[sec.h1] = []; h1Groups[sec.h1].push(sec); }); const h1Keys = Object.keys(h1Groups); let firstSection = true; h1Keys.forEach(function(h1, h) { const secs = h1Groups[h1]; if (h1 && h1 !== "null") { htmlParts.push("<div class='h1-label'>" + esc(h1) + "</div>"); } secs.forEach(function(sec, s) { if (sec.items.length === 0) return; const secId = "sec-" + h + "-" + s; const openClass = firstSection ? " open" : ""; firstSection = false; htmlParts.push("<div class='section" + openClass + "' id='" + secId + "'>\ <div class='section-header' onclick='toggleSection(\"" + secId + "\")' data-section-id='" + secId + "'>\ <div class='section-title'>" + esc(sec.h2) + "</div>\ <div class='section-count'>" + sec.items.length + "</div>\ </div>\ <div class='section-content'>\ <div class='launcher-items'>"); sec.items.forEach(function(item, i) { const itemId = secId + "-item-" + i; const isApp = isAppURL(item.url); const tagsData = item.tags.join(" "); if (isApp) { htmlParts.push("<a href='" + esc(item.url) + "' class='launcher-item' id='" + itemId + "' "); } else { const escapedURL = item.url.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"'); htmlParts.push("<a href='#' class='launcher-item' id='" + itemId + "' onclick=\"event.preventDefault();openWebURL('" + escapedURL + "');return false;\" "); } htmlParts.push("data-label='" + esc(item.label).toLowerCase() + "' data-tags='" + esc(tagsData) + "' data-section='" + secId + "'>\ <div class='launcher-label'>" + esc(item.label) + "</div>"); if (item.tags.length > 0) { htmlParts.push("<div class='item-tags'>"); item.tags.forEach(function(tag) { htmlParts.push("<span class='item-tag'>#" + esc(tag) + "</span>"); }); htmlParts.push("</div>"); } htmlParts.push("</a>"); }); htmlParts.push("</div></div></div>"); }); }); htmlParts.push("<div class='no-results' id='noResults'>No matching items found</div>\ </div>\ <script>\ var activeTags=new Set();\ var selectedIndex=-1;\ var selectableElements=[];\ function updateSelectableElements(){selectableElements=[];var sections=document.querySelectorAll('.section:not(.hidden)');sections.forEach(function(section){var header=section.querySelector('.section-header');if(header)selectableElements.push(header);if(section.classList.contains('open')){var items=section.querySelectorAll('.launcher-item:not(.hidden)');items.forEach(function(item){selectableElements.push(item);});}});}\ function clearSelection(){selectableElements.forEach(function(el){el.classList.remove('kb-selected');});}\ function setSelection(index){clearSelection();if(index>=0&&index<selectableElements.length){selectedIndex=index;var el=selectableElements[selectedIndex];el.classList.add('kb-selected');el.scrollIntoView({block:'nearest',behavior:'smooth'});}}\ function moveSelection(delta){var newIndex=selectedIndex+delta;if(newIndex<0)newIndex=0;if(newIndex>=selectableElements.length)newIndex=selectableElements.length-1;setSelection(newIndex);}\ function activateSelected(){if(selectedIndex>=0&&selectedIndex<selectableElements.length){var el=selectableElements[selectedIndex];if(el.classList.contains('section-header')){var secId=el.getAttribute('data-section-id');if(secId)toggleSection(secId);}else{el.click();}}}\ function toggleSection(id){var allSections=document.querySelectorAll('.section');allSections.forEach(function(s){if(s.id!==id)s.classList.remove('open');});var sec=document.getElementById(id);if(sec){sec.classList.toggle('open');updateSelectableElements();}}\ function openWebURL(url){Drafts.send('launcherURL',JSON.stringify({url:url}));Drafts.continue();}\ function toggleTag(tag){var pill=document.querySelector('.tag-pill[data-tag=\"'+tag+'\"]');if(activeTags.has(tag)){activeTags.delete(tag);pill.classList.remove('active');}else{activeTags.add(tag);pill.classList.add('active');}filterItems();}\ var searchBar=document.getElementById('searchBar');\ var allItems=document.querySelectorAll('.launcher-item');\ var allSections=document.querySelectorAll('.section');\ var noResults=document.getElementById('noResults');\ function filterItems(){var query=searchBar.value.toLowerCase().trim();var hasVisibleItems=false;var sectionVisibility={};allItems.forEach(function(item){var label=item.getAttribute('data-label');var tags=item.getAttribute('data-tags');var sectionId=item.getAttribute('data-section');var matchesSearch=!query||label.indexOf(query)>=0;var matchesTags=activeTags.size===0||Array.from(activeTags).every(function(t){return tags.indexOf(t)>=0;});if(matchesSearch&&matchesTags){item.classList.remove('hidden');sectionVisibility[sectionId]=true;hasVisibleItems=true;}else{item.classList.add('hidden');}});allSections.forEach(function(sec){if(sectionVisibility[sec.id]){sec.classList.remove('hidden');sec.classList.add('open');}else{sec.classList.add('hidden');}});if(hasVisibleItems){noResults.classList.remove('visible');}else{noResults.classList.add('visible');}updateSelectableElements();clearSelection();selectedIndex=-1;}\ searchBar.addEventListener('input',filterItems);\ document.addEventListener('keydown',function(e){if(e.key==='ArrowDown'){e.preventDefault();if(document.activeElement===searchBar){searchBar.blur();}if(selectedIndex===-1){setSelection(0);}else{moveSelection(1);}}else if(e.key==='ArrowUp'){e.preventDefault();if(document.activeElement===searchBar){searchBar.blur();}moveSelection(-1);}else if(e.key==='Enter'){if(document.activeElement!==searchBar){e.preventDefault();activateSelected();}}else if(e.key==='Tab'){if(document.activeElement!==searchBar){e.preventDefault();searchBar.focus();clearSelection();selectedIndex=-1;}}else if(e.key==='Escape'){Drafts.cancel();}});\ updateSelectableElements();\ setTimeout(function(){searchBar.focus();},100);\ </script>\ </body></html>"); const html = htmlParts.join(""); logPerf("HTML built - " + html.length + " chars"); // ---------- SHOW PREVIEW ---------- const prev = HTMLPreview.create(); // prev.prefersFullScreen = device.model.includes("iPhone"); prev.prefersFullScreen = true prev.hideInterface = true; logPerf("About to show preview"); prev.show(html); logPerf("Preview shown"); // ======================================== // PROCESSOR - Handle URL callbacks // ======================================== const msg = context.previewValues["launcherURL"]; if (msg) { try { const data = JSON.parse(msg); if (data.url) { app.openURL(data.url, false); } } catch(e) { alert("Error opening URL: " + e.message); } }
Options
-
After Success Default Notification Error Log Level Info