feat(cards): YouTube oEmbed; UI: inline actions in header, 🌝/🌚 summarize toggle, header collapse hides all

This commit is contained in:
Thomas Cravey 2025-08-17 18:24:05 -05:00
parent 2680abcf1f
commit 29d94c13d5
2 changed files with 35 additions and 3 deletions

View file

@ -270,6 +270,38 @@ func (s *Server) handleLinkCard(w http.ResponseWriter, r *http.Request) {
// fallthrough to generic fetch if oEmbed fails // fallthrough to generic fetch if oEmbed fails
} }
// Special handling for YouTube via oEmbed
if host == "www.youtube.com" || host == "youtube.com" || host == "m.youtube.com" || host == "youtu.be" {
watchURL := raw
if host == "youtu.be" {
// Convert youtu.be/ID to watch?v=ID
id := strings.TrimPrefix(u.Path, "/")
watchURL = "https://www.youtube.com/watch?v=" + id
}
oembed := "https://www.youtube.com/oembed?format=json&url=" + url.QueryEscape(watchURL)
client := &http.Client{Timeout: 8 * time.Second}
reqY, _ := http.NewRequestWithContext(r.Context(), http.MethodGet, oembed, nil)
respY, errY := client.Do(reqY)
if errY == nil && respY.StatusCode >= 200 && respY.StatusCode < 300 {
defer respY.Body.Close()
var o struct{
Title string `json:"title"`
Author string `json:"author_name"`
Thumb string `json:"thumbnail_url"`
HTML string `json:"html"`
}
if err := json.NewDecoder(respY.Body).Decode(&o); err == nil {
card := linkCard{URL: raw, Title: o.Title, Image: o.Thumb, HTML: o.HTML}
s.cardCache[raw] = card
s.cardCacheExp[raw] = time.Now().Add(24 * time.Hour)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(card)
return
}
}
// fallthrough if oEmbed fails
}
// fetch minimal HTML and extract tags using a tolerant HTML parser // fetch minimal HTML and extract tags using a tolerant HTML parser
client := &http.Client{Timeout: 10 * time.Second} client := &http.Client{Timeout: 10 * time.Second}
req, _ := http.NewRequestWithContext(r.Context(), http.MethodGet, raw, nil) req, _ := http.NewRequestWithContext(r.Context(), http.MethodGet, raw, nil)

View file

@ -46,7 +46,7 @@ function processLinks(scope){ const links = scope.querySelectorAll('a[href]:not(
left.appendChild(fav); left.appendChild(title); left.appendChild(fav); left.appendChild(title);
const actions=document.createElement('div'); actions.className='card-actions'; const actions=document.createElement('div'); actions.className='card-actions';
const chevron=document.createElement('button'); chevron.type='button'; chevron.title='Expand/collapse'; chevron.textContent='▾'; chevron.style.padding='0 .4rem'; chevron.style.fontSize='.9rem'; const chevron=document.createElement('button'); chevron.type='button'; chevron.title='Expand/collapse'; chevron.textContent='▾'; chevron.style.padding='0 .4rem'; chevron.style.fontSize='.9rem';
const btn=document.createElement('button'); btn.type='button'; btn.title='Summarize this link'; btn.textContent='Summarize ✨'; btn.style.padding='0 .4rem'; btn.style.fontSize='.9rem'; const btn=document.createElement('button'); btn.type='button'; btn.title='Summarize this link'; btn.textContent='🌝'; btn.style.padding='0 .4rem'; btn.style.fontSize='.9rem';
const spinner=document.createElement('span'); spinner.textContent=''; spinner.style.marginLeft='.5rem'; const spinner=document.createElement('span'); spinner.textContent=''; spinner.style.marginLeft='.5rem';
actions.appendChild(btn); actions.appendChild(chevron); actions.appendChild(spinner); actions.appendChild(btn); actions.appendChild(chevron); actions.appendChild(spinner);
head.appendChild(left); head.appendChild(actions); head.appendChild(left); head.appendChild(actions);
@ -74,10 +74,10 @@ function processLinks(scope){ const links = scope.querySelectorAll('a[href]:not(
btn.onclick = async ()=>{ btn.onclick = async ()=>{
if(sum.style.display!== 'none' && sum.textContent){ // hide existing if(sum.style.display!== 'none' && sum.textContent){ // hide existing
sum.style.display='none'; btn.textContent='Summarize ✨'; pinBottomMulti(); return; sum.style.display='none'; btn.textContent='🌝'; pinBottomMulti(); return;
} }
btn.disabled=true; spinner.textContent='…'; sum.textContent=''; sum.style.display=''; btn.disabled=true; spinner.textContent='…'; sum.textContent=''; sum.style.display='';
try{ const data = await api('/api/linksummary',{query:{url:a.href}}); sum.textContent = (data && data.summary) ? data.summary : '(no summary)'; btn.textContent='Hide summary'; } try{ const data = await api('/api/linksummary',{query:{url:a.href}}); sum.textContent = (data && data.summary) ? data.summary : '(no summary)'; btn.textContent='🌚'; }
catch(e){ sum.textContent = 'error: '+e; } catch(e){ sum.textContent = 'error: '+e; }
spinner.textContent=''; btn.disabled=false; pinBottomMulti(); spinner.textContent=''; btn.disabled=false; pinBottomMulti();
}; };