Action

launcher core

Posted by @jsamlarose, Last update about 1 month ago - Unlisted

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, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/'/g, "&#39;")
        .replace(/"/g, "&quot;");
    }
    
    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
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.