[object Object]

← back to Linkedin Like Helper

add Feed-pages mode: batch-open curated LinkedIn streams (home + design/wallcovering hashtag feeds) for scroll-and-like-in-place

d974f373aac44790f3065cf3ae92cbc06004ce2e · 2026-06-30 11:49:33 -0700 · Steve

Files touched

Diff

commit d974f373aac44790f3065cf3ae92cbc06004ce2e
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jun 30 11:49:33 2026 -0700

    add Feed-pages mode: batch-open curated LinkedIn streams (home + design/wallcovering hashtag feeds) for scroll-and-like-in-place
---
 index.html | 347 +++++++++++++++++++++++++++++++++++++++++++------------------
 1 file changed, 247 insertions(+), 100 deletions(-)

diff --git a/index.html b/index.html
index 9f8b355..f4f471c 100644
--- a/index.html
+++ b/index.html
@@ -13,21 +13,25 @@
   *{box-sizing:border-box}
   body{margin:0;font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
        background:var(--bg);color:var(--ink);padding:24px}
-  .wrap{max-width:820px;margin:0 auto}
+  .wrap{max-width:860px;margin:0 auto}
   header{display:flex;align-items:center;gap:12px;margin-bottom:8px}
   header .badge{width:34px;height:34px;border-radius:7px;background:var(--li);color:#fff;
        display:grid;place-items:center;font-weight:800;font-size:18px}
   h1{font-size:21px;margin:0;font-weight:700}
-  .sub{color:var(--muted);margin:2px 0 20px;font-size:13.5px}
+  .sub{color:var(--muted);margin:2px 0 18px;font-size:13.5px}
+  .tabs{display:flex;gap:8px;margin-bottom:16px}
+  .tab{padding:8px 16px;border-radius:999px;border:1.5px solid var(--line);background:#fff;cursor:pointer;
+       font-size:13.5px;font-weight:600;color:var(--muted)}
+  .tab.active{background:var(--li);color:#fff;border-color:var(--li)}
   .card{background:var(--card);border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow);
         padding:18px 20px;margin-bottom:16px}
   label{font-weight:600;font-size:13px;display:block;margin-bottom:6px}
-  textarea{width:100%;min-height:140px;border:1px solid var(--line);border-radius:8px;padding:11px 12px;
+  textarea{width:100%;min-height:120px;border:1px solid var(--line);border-radius:8px;padding:11px 12px;
         font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;resize:vertical;color:var(--ink)}
   .row{display:flex;flex-wrap:wrap;gap:14px;align-items:flex-end}
   .grp{flex:1 1 200px}
-  select,input[type=number]{width:100%;padding:9px 11px;border:1px solid var(--line);border-radius:8px;
-        font-size:14px;background:#fff;color:var(--ink)}
+  select,input[type=number],input[type=text]{width:100%;padding:9px 11px;border:1px solid var(--line);
+        border-radius:8px;font-size:14px;background:#fff;color:var(--ink)}
   .btns{display:flex;gap:10px;flex-wrap:wrap;margin-top:4px}
   button{border:0;border-radius:999px;padding:10px 18px;font-size:14px;font-weight:600;cursor:pointer}
   .primary{background:var(--li);color:#fff}
@@ -40,7 +44,7 @@
   .stat b{color:var(--ink)}
   .bar{height:8px;background:#e6eaee;border-radius:999px;overflow:hidden;margin:10px 0 2px}
   .bar>i{display:block;height:100%;background:var(--done);width:0;transition:width .3s}
-  ul.list{list-style:none;margin:8px 0 0;padding:0;max-height:340px;overflow:auto;border-top:1px solid var(--line)}
+  ul.list{list-style:none;margin:8px 0 0;padding:0;max-height:300px;overflow:auto;border-top:1px solid var(--line)}
   ul.list li{display:flex;align-items:center;gap:10px;padding:9px 4px;border-bottom:1px solid #eef1f4;font-size:13.5px}
   ul.list li.done{opacity:.5}
   ul.list li.done .url{text-decoration:line-through}
@@ -51,8 +55,16 @@
         background:#eafaf0;border-radius:999px;padding:2px 8px}
   .mini{flex:0 0 auto;background:#fff;border:1px solid var(--line);border-radius:6px;padding:4px 9px;
         font-size:12px;font-weight:600;cursor:pointer;color:var(--ink)}
+  .sources{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px 16px;margin-top:6px}
+  .src{display:flex;align-items:center;gap:9px;font-size:13.5px;padding:5px 2px}
+  .src input{width:auto}
+  .src .tag{color:var(--muted);font-size:11.5px}
+  .selrow{display:flex;gap:10px;margin:6px 0 2px}
+  .selrow button{padding:5px 12px;font-size:12px}
   .note{font-size:12.5px;color:var(--muted);background:#fffbe9;border:1px solid #f1e3a8;border-radius:8px;
         padding:10px 12px;margin-top:14px}
+  .pane{display:none}
+  .pane.active{display:block}
   .tos{font-size:12px;color:var(--muted);margin-top:18px;text-align:center}
 </style>
 </head>
@@ -62,55 +74,115 @@
     <div class="badge">in</div>
     <div>
       <h1>LinkedIn Like Helper</h1>
-      <div class="sub">Opens posts in batches so <b>you</b> like them by hand. No automation — fully within LinkedIn's terms.</div>
+      <div class="sub">Opens streams &amp; posts in batches so <b>you</b> like them by hand. No automation — fully within LinkedIn's terms.</div>
     </div>
   </header>
 
-  <div class="card">
-    <label for="urls">Post / profile links — one per line</label>
-    <textarea id="urls" placeholder="https://www.linkedin.com/feed/update/urn:li:activity:7300000000000000000/&#10;https://www.linkedin.com/posts/somebody_xyz-activity-7300000000000000000-abcd/&#10;…paste as many as you like"></textarea>
-    <div class="btns" style="margin-top:12px">
-      <button class="ghost" onclick="loadList()">Load list ↻</button>
-      <button class="danger" onclick="resetProgress()">Reset progress</button>
-    </div>
+  <div class="tabs">
+    <div class="tab active" data-mode="feeds" onclick="setMode('feeds')">📜 Feed pages — scroll &amp; like in place</div>
+    <div class="tab" data-mode="links" onclick="setMode('links')">🔗 Post links</div>
   </div>
 
-  <div class="card">
-    <div class="row">
-      <div class="grp">
-        <label for="size">Group size</label>
-        <select id="size" onchange="syncCustom()">
-          <option value="1">1 at a time</option>
-          <option value="3">3</option>
-          <option value="5" selected>5</option>
-          <option value="10">10</option>
-          <option value="25">25</option>
-          <option value="all">All remaining</option>
-          <option value="custom">Custom…</option>
-        </select>
+  <!-- ============ FEED-PAGES MODE ============ -->
+  <div class="pane active" id="pane-feeds">
+    <div class="card">
+      <label>Pick the streams to open — each is a fresh river of likable content</label>
+      <div class="sources" id="sources"></div>
+      <div class="selrow">
+        <button class="ghost" onclick="selectAll(true)">Select all</button>
+        <button class="ghost" onclick="selectAll(false)">Clear</button>
       </div>
-      <div class="grp" id="customWrap" style="display:none">
-        <label for="custom">Custom count</label>
-        <input type="number" id="custom" min="1" value="8">
+
+      <label style="margin-top:14px">Add your own — hashtags (<code>#raffia</code>) or full URLs, one per line</label>
+      <textarea id="customFeeds" placeholder="#grasscloth&#10;#luxuryinteriors&#10;https://www.linkedin.com/in/some-connection/recent-activity/all/" oninput="persistFeeds()"></textarea>
+    </div>
+
+    <div class="card">
+      <div class="row">
+        <div class="grp">
+          <label for="fsize">Group size — how many tabs to open per click</label>
+          <select id="fsize" onchange="syncCustom('f')">
+            <option value="1">1 at a time</option>
+            <option value="3" selected>3</option>
+            <option value="5">5</option>
+            <option value="10">10</option>
+            <option value="all">All selected</option>
+            <option value="custom">Custom…</option>
+          </select>
+        </div>
+        <div class="grp" id="fcustomWrap" style="display:none">
+          <label for="fcustom">Custom count</label>
+          <input type="number" id="fcustom" min="1" value="4">
+        </div>
+        <div class="grp" style="flex:0 0 auto">
+          <button class="primary" onclick="openNextFeeds()">▶ Open next group</button>
+        </div>
+        <div class="grp" style="flex:0 0 auto">
+          <button class="danger" onclick="resetFeeds()">Start over</button>
+        </div>
       </div>
-      <div class="grp" style="flex:0 0 auto">
-        <button class="primary" onclick="openNext()">▶ Open next group</button>
+
+      <div class="stat">
+        <span>Selected: <b id="fTotal">0</b></span>
+        <span>Opened: <b id="fDone">0</b></span>
+        <span>Left: <b id="fLeft">0</b></span>
+      </div>
+      <div class="bar"><i id="fBar"></i></div>
+
+      <div class="note">
+        <b>First time:</b> Chrome asks to <i>“allow multiple pop-ups”</i> — click Allow once.
+        Each tab is a real LinkedIn stream — scroll it and 👍 the posts you like, then close it.
+        Opened tabs auto-mark; <b>Start over</b> re-arms the whole set.
       </div>
     </div>
+  </div>
 
-    <div class="stat">
-      <span>Total: <b id="cTotal">0</b></span>
-      <span>Done: <b id="cDone">0</b></span>
-      <span>Remaining: <b id="cLeft">0</b></span>
+  <!-- ============ POST-LINKS MODE ============ -->
+  <div class="pane" id="pane-links">
+    <div class="card">
+      <label for="urls">Post / profile links — one per line  <span style="font-weight:400;color:var(--muted)">(••• → Copy link to post)</span></label>
+      <textarea id="urls" placeholder="https://www.linkedin.com/feed/update/urn:li:activity:7300000000000000000/&#10;https://www.linkedin.com/posts/somebody_xyz-activity-7300000000000000000-abcd/"></textarea>
+      <div class="btns" style="margin-top:12px">
+        <button class="ghost" onclick="loadList()">Load list ↻</button>
+        <button class="danger" onclick="resetProgress()">Reset progress</button>
+      </div>
     </div>
-    <div class="bar"><i id="barFill"></i></div>
 
-    <ul class="list" id="list"></ul>
+    <div class="card">
+      <div class="row">
+        <div class="grp">
+          <label for="size">Group size</label>
+          <select id="size" onchange="syncCustom('l')">
+            <option value="1">1 at a time</option>
+            <option value="3">3</option>
+            <option value="5" selected>5</option>
+            <option value="10">10</option>
+            <option value="25">25</option>
+            <option value="all">All remaining</option>
+            <option value="custom">Custom…</option>
+          </select>
+        </div>
+        <div class="grp" id="lcustomWrap" style="display:none">
+          <label for="lcustom">Custom count</label>
+          <input type="number" id="lcustom" min="1" value="8">
+        </div>
+        <div class="grp" style="flex:0 0 auto">
+          <button class="primary" onclick="openNext()">▶ Open next group</button>
+        </div>
+      </div>
 
-    <div class="note">
-      <b>First time:</b> Chrome will ask to <i>“allow multiple pop-ups”</i> — click Allow once.
-      Each opened tab is a real LinkedIn page; click the 👍 Like yourself, then close the tab.
-      A batch auto-marks as done when you open it (or use “done” / “Reset progress” to adjust).
+      <div class="stat">
+        <span>Total: <b id="cTotal">0</b></span>
+        <span>Done: <b id="cDone">0</b></span>
+        <span>Remaining: <b id="cLeft">0</b></span>
+      </div>
+      <div class="bar"><i id="barFill"></i></div>
+      <ul class="list" id="list"></ul>
+
+      <div class="note">
+        <b>First time:</b> allow multiple pop-ups once. Each opened tab is a real post — click 👍 yourself, then close it.
+        Opening a post auto-marks it done; use a row's <i>undo</i> if you skipped the like.
+      </div>
     </div>
   </div>
 
@@ -118,73 +190,148 @@
 </div>
 
 <script>
-const KEY = 'lk_like_helper_v1';
-let items = [];   // {url, done}
-
-function load(){ try{ return JSON.parse(localStorage.getItem(KEY)) || {} }catch(e){ return {} } }
-function save(state){ localStorage.setItem(KEY, JSON.stringify(state)); }
-
-function loadList(){
-  const lines = document.getElementById('urls').value
-    .split(/\n+/).map(s=>s.trim()).filter(Boolean);
-  // de-dupe, preserve order
-  const seen = new Set(); const urls = [];
-  for(const u of lines){ if(!seen.has(u)){ seen.add(u); urls.push(u); } }
-  const prev = load(); const doneMap = prev.done || {};
-  items = urls.map(u => ({url:u, done: !!doneMap[u]}));
-  persist(); render();
+/* ---------- shared ---------- */
+function openTabs(urls){
+  urls.forEach((u,i)=> setTimeout(()=>window.open(u,'_blank','noopener'), i*150));
 }
-
-function persist(){
-  const doneMap = {}; items.forEach(i=>{ if(i.done) doneMap[i.url]=1; });
-  save({ raw: document.getElementById('urls').value, done: doneMap });
+function syncCustom(which){
+  const sel = document.getElementById(which==='f'?'fsize':'size').value;
+  document.getElementById(which==='f'?'fcustomWrap':'lcustomWrap').style.display = sel==='custom' ? '' : 'none';
 }
-
-function syncCustom(){
-  document.getElementById('customWrap').style.display =
-    document.getElementById('size').value==='custom' ? '' : 'none';
-}
-
-function groupSize(){
-  const v = document.getElementById('size').value;
+function groupSizeFrom(selId, customId){
+  const v = document.getElementById(selId).value;
   if(v==='all') return Infinity;
-  if(v==='custom') return Math.max(1, parseInt(document.getElementById('custom').value)||1);
+  if(v==='custom') return Math.max(1, parseInt(document.getElementById(customId).value)||1);
   return parseInt(v);
 }
 
-function openNext(){
-  if(!items.length){ loadList(); }
-  const pending = items.filter(i=>!i.done);
-  if(!pending.length){ alert('All done 🎉  Use “Reset progress” to go again.'); return; }
-  const n = Math.min(groupSize(), pending.length);
-  const batch = pending.slice(0, n);
-  batch.forEach((it, idx) => {
-    // tiny stagger so the browser doesn't drop simultaneous opens
-    setTimeout(()=>window.open(it.url, '_blank', 'noopener'), idx*120);
-    it.done = true;
-  });
-  persist(); render();
+/* ---------- mode switch ---------- */
+function setMode(m){
+  document.querySelectorAll('.tab').forEach(t=>t.classList.toggle('active', t.dataset.mode===m));
+  document.getElementById('pane-feeds').classList.toggle('active', m==='feeds');
+  document.getElementById('pane-links').classList.toggle('active', m==='links');
+  localStorage.setItem('lk_mode', m);
 }
 
-function toggleDone(url){
-  const it = items.find(i=>i.url===url); if(it){ it.done=!it.done; persist(); render(); }
-}
+/* ======================================================== */
+/*  FEED-PAGES MODE                                         */
+/* ======================================================== */
+const HASH = h => 'https://www.linkedin.com/feed/hashtag/?keywords='+encodeURIComponent(h);
+const PRESETS = [
+  {label:'🏠 My Home Feed',      url:'https://www.linkedin.com/feed/',            tag:'feed',  on:true},
+  {label:'🔔 Notifications',     url:'https://www.linkedin.com/notifications/',   tag:'feed',  on:false},
+  {label:'👥 My Network',        url:'https://www.linkedin.com/mynetwork/',       tag:'feed',  on:false},
+  {label:'#interiordesign',      url:HASH('interiordesign'),     tag:'hashtag', on:true},
+  {label:'#interiordesigner',    url:HASH('interiordesigner'),   tag:'hashtag', on:true},
+  {label:'#wallcoverings',       url:HASH('wallcoverings'),      tag:'hashtag', on:true},
+  {label:'#wallpaper',           url:HASH('wallpaper'),          tag:'hashtag', on:true},
+  {label:'#commercialdesign',    url:HASH('commercialdesign'),   tag:'hashtag', on:false},
+  {label:'#hospitalitydesign',   url:HASH('hospitalitydesign'),  tag:'hashtag', on:false},
+  {label:'#luxuryinteriors',     url:HASH('luxuryinteriors'),    tag:'hashtag', on:false},
+  {label:'#designtrends',        url:HASH('designtrends'),       tag:'hashtag', on:false},
+  {label:'#architecture',        url:HASH('architecture'),       tag:'hashtag', on:false},
+];
+let fOpened = new Set();   // urls opened this session
 
-function resetProgress(){
-  items.forEach(i=>i.done=false); persist(); render();
+function loadFeedPrefs(){
+  try{ return JSON.parse(localStorage.getItem('lk_feed_prefs'))||{} }catch(e){ return {} }
+}
+function persistFeeds(){
+  const checked = {};
+  document.querySelectorAll('#sources input[type=checkbox]').forEach(c=>checked[c.value]=c.checked);
+  localStorage.setItem('lk_feed_prefs', JSON.stringify({
+    checked, custom: document.getElementById('customFeeds').value
+  }));
+}
+function customFeedUrls(){
+  return document.getElementById('customFeeds').value.split(/\n+/).map(s=>s.trim()).filter(Boolean)
+    .map(s => s.startsWith('#') ? HASH(s.slice(1)) : (s.startsWith('http') ? s : HASH(s)));
+}
+function selectedFeeds(){
+  const urls = [];
+  document.querySelectorAll('#sources input[type=checkbox]').forEach(c=>{ if(c.checked) urls.push(c.value); });
+  customFeedUrls().forEach(u=>{ if(!urls.includes(u)) urls.push(u); });
+  return urls;
+}
+function renderSources(){
+  const prefs = loadFeedPrefs();
+  const host = document.getElementById('sources'); host.innerHTML='';
+  PRESETS.forEach(p=>{
+    const isOn = prefs.checked && (p.url in prefs.checked) ? prefs.checked[p.url] : p.on;
+    const div = document.createElement('label'); div.className='src';
+    div.innerHTML = '<input type="checkbox" value="'+p.url+'" '+(isOn?'checked':'')+'>'+
+                    '<span>'+p.label+'</span>';
+    div.querySelector('input').onchange = ()=>{ persistFeeds(); renderFeedStats(); };
+    host.appendChild(div);
+  });
+  if(prefs.custom) document.getElementById('customFeeds').value = prefs.custom;
+}
+function selectAll(on){
+  document.querySelectorAll('#sources input[type=checkbox]').forEach(c=>c.checked=on);
+  persistFeeds(); renderFeedStats();
 }
+function renderFeedStats(){
+  const sel = selectedFeeds();
+  const opened = sel.filter(u=>fOpened.has(u)).length;
+  document.getElementById('fTotal').textContent = sel.length;
+  document.getElementById('fDone').textContent  = opened;
+  document.getElementById('fLeft').textContent  = sel.length-opened;
+  document.getElementById('fBar').style.width = sel.length? (opened/sel.length*100)+'%' : '0';
+}
+function openNextFeeds(){
+  const sel = selectedFeeds();
+  if(!sel.length){ alert('Pick at least one stream first.'); return; }
+  const pending = sel.filter(u=>!fOpened.has(u));
+  if(!pending.length){ alert('Opened them all 🎉  Hit “Start over” to re-arm.'); return; }
+  const n = Math.min(groupSizeFrom('fsize','fcustom'), pending.length);
+  const batch = pending.slice(0,n);
+  openTabs(batch);
+  batch.forEach(u=>fOpened.add(u));
+  renderFeedStats();
+}
+function resetFeeds(){ fOpened.clear(); renderFeedStats(); }
 
-function render(){
-  const total = items.length, done = items.filter(i=>i.done).length;
-  document.getElementById('cTotal').textContent = total;
-  document.getElementById('cDone').textContent  = done;
-  document.getElementById('cLeft').textContent  = total-done;
-  document.getElementById('barFill').style.width = total? (done/total*100)+'%' : '0';
-  const ul = document.getElementById('list'); ul.innerHTML='';
+/* ======================================================== */
+/*  POST-LINKS MODE                                         */
+/* ======================================================== */
+const KEY='lk_like_helper_v1';
+let items=[];
+function loadLS(){ try{ return JSON.parse(localStorage.getItem(KEY))||{} }catch(e){ return {} } }
+function saveLS(s){ localStorage.setItem(KEY, JSON.stringify(s)); }
+function loadList(){
+  const lines = document.getElementById('urls').value.split(/\n+/).map(s=>s.trim()).filter(Boolean);
+  const seen=new Set(), urls=[];
+  for(const u of lines){ if(!seen.has(u)){ seen.add(u); urls.push(u); } }
+  const doneMap=(loadLS().done)||{};
+  items = urls.map(u=>({url:u, done:!!doneMap[u]}));
+  persistLinks(); renderLinks();
+}
+function persistLinks(){
+  const doneMap={}; items.forEach(i=>{ if(i.done) doneMap[i.url]=1; });
+  saveLS({ raw: document.getElementById('urls').value, done: doneMap });
+}
+function openNext(){
+  if(!items.length){ loadList(); }
+  const pending=items.filter(i=>!i.done);
+  if(!pending.length){ alert('All done 🎉  Use “Reset progress” to go again.'); return; }
+  const n=Math.min(groupSizeFrom('size','lcustom'), pending.length);
+  const batch=pending.slice(0,n);
+  openTabs(batch.map(i=>i.url));
+  batch.forEach(i=>i.done=true);
+  persistLinks(); renderLinks();
+}
+function toggleDone(url){ const it=items.find(i=>i.url===url); if(it){ it.done=!it.done; persistLinks(); renderLinks(); } }
+function resetProgress(){ items.forEach(i=>i.done=false); persistLinks(); renderLinks(); }
+function renderLinks(){
+  const total=items.length, done=items.filter(i=>i.done).length;
+  document.getElementById('cTotal').textContent=total;
+  document.getElementById('cDone').textContent=done;
+  document.getElementById('cLeft').textContent=total-done;
+  document.getElementById('barFill').style.width = total? (done/total*100)+'%':'0';
+  const ul=document.getElementById('list'); ul.innerHTML='';
   items.forEach((it,i)=>{
     const li=document.createElement('li'); if(it.done) li.className='done';
-    li.innerHTML =
-      '<span class="num">'+(i+1)+'</span>'+
+    li.innerHTML='<span class="num">'+(i+1)+'</span>'+
       '<span class="url"><a href="'+it.url+'" target="_blank" rel="noopener">'+it.url+'</a></span>'+
       (it.done?'<span class="pill">done</span>':'')+
       '<button class="mini" data-u="'+encodeURIComponent(it.url)+'">'+(it.done?'undo':'done')+'</button>';
@@ -193,11 +340,11 @@ function render(){
   ul.querySelectorAll('.mini').forEach(b=>b.onclick=()=>toggleDone(decodeURIComponent(b.dataset.u)));
 }
 
-// restore on open
+/* ---------- init ---------- */
 (function init(){
-  const prev = load();
-  if(prev.raw){ document.getElementById('urls').value = prev.raw; loadList(); }
-  syncCustom();
+  renderSources(); renderFeedStats(); syncCustom('f'); syncCustom('l');
+  const prev=loadLS(); if(prev.raw){ document.getElementById('urls').value=prev.raw; loadList(); }
+  setMode(localStorage.getItem('lk_mode')||'feeds');
 })();
 </script>
 </body>

← b5303bb initial scaffold: LinkedIn manual like helper (batch-open, g  ·  back to Linkedin Like Helper  ·  chore: refactor (loadKey helper, named TAB_STAGGER_MS), lint 56aaf4f →