← 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 & 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/ https://www.linkedin.com/posts/somebody_xyz-activity-7300000000000000000-abcd/ …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 & 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 #luxuryinteriors 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/ 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 →