← back to Linkedin Like Helper
index.html
381 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>LinkedIn Like Helper — manual, TOS-safe</title>
<style>
:root{
--bg:#f3f2ef; --card:#fff; --ink:#1d2226; --muted:#56687a;
--li:#0a66c2; --li-dark:#004182; --line:#d7dce1; --done:#057642;
--shadow:0 1px 3px rgba(0,0,0,.08),0 1px 2px rgba(0,0,0,.04);
}
*{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: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 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);font-family:inherit}
.tab.active{background:var(--li);color:#fff;border-color:var(--li)}
.tab:focus-visible{outline:2px solid var(--li);outline-offset:2px}
.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: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],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}
.primary:hover{background:var(--li-dark)}
.ghost{background:#fff;color:var(--li);border:1.5px solid var(--li)}
.ghost:hover{background:#eaf3fb}
.danger{background:#fff;color:#b11b1b;border:1.5px solid #e3c4c4}
.danger:hover{background:#fdeeee}
.stat{display:flex;gap:22px;margin:14px 0 4px;font-size:13.5px;color:var(--muted);flex-wrap:wrap}
.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: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}
.num{width:26px;color:var(--muted);text-align:right;flex:0 0 auto;font-variant-numeric:tabular-nums}
.url{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.url a{color:var(--li);text-decoration:none}
.pill{flex:0 0 auto;font-size:11px;font-weight:700;color:var(--done);border:1px solid #bfe3cd;
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>
<body>
<div class="wrap">
<header>
<div class="badge">in</div>
<div>
<h1>LinkedIn Like Helper</h1>
<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="tabs" role="tablist" aria-label="Mode">
<button type="button" class="tab active" data-mode="feeds" role="tab" aria-selected="true" onclick="setMode('feeds')">📜 Feed pages — scroll & like in place</button>
<button type="button" class="tab" data-mode="links" role="tab" aria-selected="false" onclick="setMode('links')">🔗 Post links</button>
</div>
<!-- ============ 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>
<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();renderFeedStats()"></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="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>
<ul class="list" id="fList"></ul>
<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>
<!-- ============ 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="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="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>
<div class="tos">Manual assist only · your clicks, your account · honors the dw-linkedin “never auto-act” rule</div>
</div>
<script>
/* ---------- shared ---------- */
const TAB_STAGGER_MS = 150;
function openTabs(urls){
urls.forEach((u,i)=> setTimeout(()=>window.open(u,'_blank','noopener'), i*TAB_STAGGER_MS));
}
function loadKey(k){ try{ return JSON.parse(localStorage.getItem(k))||{} }catch(e){ return {} } }
function syncCustom(which){
const sel = document.getElementById(which==='f'?'fsize':'size').value;
document.getElementById(which==='f'?'fcustomWrap':'lcustomWrap').style.display = sel==='custom' ? '' : 'none';
}
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(customId).value)||1);
return parseInt(v);
}
/* ---------- mode switch ---------- */
function setMode(m){
document.querySelectorAll('.tab').forEach(t=>{
const on = t.dataset.mode===m;
t.classList.toggle('active', on);
t.setAttribute('aria-selected', on ? 'true' : 'false');
});
document.getElementById('pane-feeds').classList.toggle('active', m==='feeds');
document.getElementById('pane-links').classList.toggle('active', m==='links');
localStorage.setItem('lk_mode', m);
}
/* ======================================================== */
/* 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},
];
const FOPEN_KEY = 'lk_feed_opened';
function loadFeedOpened(){ const v = loadKey(FOPEN_KEY); return new Set(Array.isArray(v.urls) ? v.urls : []); }
function saveFeedOpened(){ localStorage.setItem(FOPEN_KEY, JSON.stringify({ urls:[...fOpened] })); }
let fOpened = loadFeedOpened(); // urls opened, persisted across reloads
function loadFeedPrefs(){ return loadKey('lk_feed_prefs'); }
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 feedLabel(url){
const p = PRESETS.find(pr=>pr.url===url);
if(p) return p.label;
const m = url.match(/[?&]keywords=([^&]+)/);
if(m) return '#'+decodeURIComponent(m[1]);
return url;
}
function reopenFeed(url){ openTabs([url]); fOpened.add(url); saveFeedOpened(); 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';
const ul=document.getElementById('fList'); ul.innerHTML='';
sel.forEach((u,i)=>{
const done=fOpened.has(u);
const li=document.createElement('li'); if(done) li.className='done';
li.innerHTML='<span class="num">'+(i+1)+'</span>'+
'<span class="url"><a href="'+u+'" target="_blank" rel="noopener">'+feedLabel(u)+'</a></span>'+
(done?'<span class="pill">opened</span>':'')+
'<button class="mini" data-u="'+encodeURIComponent(u)+'">re-open</button>';
ul.appendChild(li);
});
ul.querySelectorAll('.mini').forEach(b=>b.onclick=()=>reopenFeed(decodeURIComponent(b.dataset.u)));
}
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));
saveFeedOpened();
renderFeedStats();
}
function resetFeeds(){ fOpened.clear(); saveFeedOpened(); renderFeedStats(); }
/* ======================================================== */
/* POST-LINKS MODE */
/* ======================================================== */
const KEY='lk_like_helper_v1';
let items=[];
function loadLS(){ return loadKey(KEY); }
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>'+
'<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>';
ul.appendChild(li);
});
ul.querySelectorAll('.mini').forEach(b=>b.onclick=()=>toggleDone(decodeURIComponent(b.dataset.u)));
}
/* ---------- init ---------- */
(function init(){
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>
</html>