Action
music launcher
A launcher for a list of URLs organised under Markdown headings. The first heading in the list is unfolded… entries under other headings are folded by default. The interface also 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 it primarily as a launcher for music— I listen to music a lot while working, drawn from all the different platforms, and I’m using this as a) a way to keep track of things I’ve discovered recently that I don’t want to lose sight of, and b) an interface to surface music by mood regardless of platform. That said, I imagine it might be useful for any organised list of URLs you might need to access regularly?
Also works for URL schemes, so you could use it as an app/file launcher to pull together links to assets in different applications (if those applications support direct links)?
Update this line and point it to the source draft for your launcher:
const LAUNCHER_DRAFT_TITLE = “::music launcher”;
Oh, and you might want to tweak the font families if you don’t have Hoefler or Roboto Mono installed.
Full screen interface. Hit the x in the top righthand corner to cancel.
Steps
-
script
// ======================================== // URL LAUNCHER SCRIPT // Creates a filterable launcher for URLs from markdown links // Supports inline tags for filtering by mood/vibe // ======================================== // ---------- CONFIG ---------- const LAUNCHER_DRAFT_TITLE = "::music launcher"; const TITLE_FONT = "'Hoefler Text','Baskerville','Garamond','Times New Roman',serif"; const MONO_FONT = "'Roboto Mono','SF Mono',ui-monospace,Menlo,Consolas,monospace"; // ---------- LOAD LAUNCHER DATA ---------- const launcherDrafts = Draft.queryByTitle(LAUNCHER_DRAFT_TITLE); const launcherDraft = launcherDrafts?.[0]; if (!launcherDraft) { alert(`No launcher draft found. Please create a draft titled '${LAUNCHER_DRAFT_TITLE}'`); context.fail(); } // ---------- PARSE MARKDOWN ---------- const lines = launcherDraft.content.split("\n"); const sections = []; let currentH1 = null; let currentH2 = null; const allTags = {}; const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/; const tagRegex = /#(\w+)/g; 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]; const url = linkMatch[2]; const tags = []; let tagMatch; tagRegex.lastIndex = 0; while ((tagMatch = tagRegex.exec(line)) !== null) { const tag = tagMatch[1].toLowerCase(); tags.push(tag); allTags[tag] = (allTags[tag] || 0) + 1; } sections[sections.length - 1].items.push({ label, url, tags }); } } } // Sort tags by frequency const sortedTags = Object.keys(allTags).sort((a, b) => allTags[b] - allTags[a]); // ---------- HELPERS ---------- function esc(s) { return (s || "").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)} .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-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} .launcher-item{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:#0a0a0a;border:1px solid #1a1a1a;border-radius:6px;cursor:pointer;transition:all .2s ease;text-decoration:none;color:#f5f5f5} .launcher-item:hover{background:#1a1a1a;border-color:#333;transform:translateX(4px)} .launcher-item.hidden{display:none} .launcher-label{font-size:15px;font-weight:400;flex:1} .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((sum, sec) => 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> <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(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>"); // Group sections by H1 const h1Groups = {}; sections.forEach(sec => { if (!h1Groups[sec.h1]) h1Groups[sec.h1] = []; h1Groups[sec.h1].push(sec); }); const h1Keys = Object.keys(h1Groups); let firstSection = true; h1Keys.forEach((h1, h) => { const secs = h1Groups[h1]; if (h1 && h1 !== "null") { htmlParts.push(`<div class='h1-label'>${esc(h1)}</div>`); } secs.forEach((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}")'> <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((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(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(); function toggleSection(id){var sec=document.getElementById(id);if(sec)sec.classList.toggle('open');} 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');}} searchBar.addEventListener('input',filterItems); setTimeout(function(){searchBar.focus();},100); </script> </body></html>`); const html = htmlParts.join(''); // ---------- SHOW PREVIEW ---------- const prev = HTMLPreview.create(); prev.prefersFullScreen = true; prev.hideInterface = true; prev.show(html); // ======================================== // 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