The best Duolingo account management tool
// ==UserScript==
// @name DuoAM
// @namespace Tampermonkey
// @version 1.2.5
// @description The best Duolingo account management tool
// @author kietxx_173
// @match https://www.duolingo.com/*
// @license MIT
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const STORAGE_KEY = 'duo_am_tokens_v1';
const SETTINGS_KEY = 'duo_am_settings_v1';
const PENDING_LOGIN_KEY = 'duo_am_pending_login_v1';
const LANGS = {
vi: {
tabAccounts:'📋 Tài khoản', tabAdd:'➕ Thêm', tabGuide:'📖 Hướng dẫn',
searchPlaceholder:'Tìm tài khoản…',
emptyNoAccounts:'Chưa có tài khoản nào',
emptyNoAccountsSub:'Sang tab <b style="color:#58CC02">➕ Thêm</b> để bắt đầu',
emptyNotFound:'Không tìm thấy',
labelPlaceholder:'Tên hiển thị (ví dụ: main, alt1…)',
tokenPlaceholder:'JWT Token',
btnSave:'Lưu', btnGetToken:'⚡ Lấy token acc hiện tại', btnGetTokenSuccess:'✓ Đã điền token!',
toastSwitching:'⏳ Đang chuyển tài khoản...', toastNoLabel:'⚠️ Nhập tên hiển thị',
toastNoToken:'⚠️ Nhập JWT Token', toastDuplicateLabel:'⚠️ Tên đã tồn tại',
toastSaved:n=>`✓ Đã lưu: ${n}`, toastDeleted:n=>`Đã xoá: ${n}`,
toastNoTokenFound:'⚠️ Không tìm thấy token — bạn đang login chưa?',
toastCookieError:'⚠️ Lỗi khi đọc cookie', toastCopyFail:'⚠️ Không copy được, hãy copy thủ công',
toastLoginError:'⚠️ Token không hợp lệ hoặc đã hết hạn',
toastTokenDetected:'🔴 Token lỗi — đăng nhập thất bại, vui lòng cập nhật token mới',
toastLoginSuccess:n=>`✓ Đang dùng: ${n}`,
toastBadFormat:'⚠️ Token sai định dạng — phải bắt đầu bằng eyJhb',
badgeCurrent:'Current', btnLogin:'Login', btnDelete:'Xoá', btnEdit:'Sửa',
confirmTitle:'Xác nhận đăng nhập', confirmMsg:name=>`Đăng nhập với tài khoản <strong>${name}</strong>?`,
btnCancel:'Huỷ', btnOk:'Đăng nhập',
editTitle:'Sửa tài khoản', editLabelField:'Tên hiển thị', editTokenField:'JWT Token',
editPreviewField:'Xem trước token đầy đủ',
editNote:'⚠️ Token phải bắt đầu bằng <b>eyJhb</b>. Sau khi lưu cần đăng nhập lại để áp dụng.',
settingTheme:'🎨 Giao diện', themeAuto:'🌗 Tự động', themeDark:'🌙 Tối', themeLight:'☀️ Sáng',
settingTransparent:'🪟 Nền trong suốt', settingTransparentDesc:'Hiệu ứng kính mờ (backdrop blur)',
settingOpacity:'💧 Độ trong suốt', settingLanguage:'🌐 Ngôn ngữ',
btnSettingsSave:'💾 Lưu cài đặt', toastSettingsSaved:'✓ Đã lưu cài đặt',
langVi:'🇻🇳 Tiếng Việt', langEn:'🇬🇧 English',
guideTitle:'📖 Cách lấy JWT Token',
guideSteps:[
{label:'Đăng nhập Duolingo trên trình duyệt',desc:'Vào <b>duolingo.com</b> và đăng nhập bằng tài khoản bạn muốn lưu. Đảm bảo đã vào được trang học (<b>/learn</b>).'},
{label:'Mở DevTools (công cụ nhà phát triển)',desc:'Nhấn <em>F12</em> hoặc <em>Ctrl + Shift + I</em><br>Trên Mac: <em>Cmd + Option + I</em><br>Hoặc chuột phải vào trang → <b>Inspect / Kiểm tra</b>'},
{label:'Chuyển sang tab Console',desc:'Trong DevTools, click vào tab <b>Console</b> ở thanh trên cùng.'},
{label:'Bỏ qua cảnh báo "paste" (nếu có)',desc:'Chrome/Edge có thể hiện cảnh báo bảo mật yêu cầu gõ <b>allow pasting</b> rồi Enter trước khi dán lệnh. Làm theo rồi tiếp tục.'},
{label:'Dán lệnh lấy token vào Console',desc:'Copy lệnh bên dưới, dán vào Console rồi nhấn <em>Enter</em>:',code:true,extra:'Token sẽ hiện ra trong Console — một chuỗi dài bắt đầu bằng <em>eyJ…</em>'},
{label:"Copy token — bỏ dấu nháy đơn ' ở đầu và cuối",desc:`Kết quả trong Console trông như thế này:<br><em style="font-size:10px;word-break:break-all">'eyJhbGciOiJIUzI1NiJ9.eyJ…'</em><br><br>Bạn cần <b>bỏ dấu nháy đơn</b> <em>'</em> ở đầu và cuối chuỗi trước khi dán vào ô Token. Chỉ copy phần chữ ở giữa, ví dụ:<br><em style="font-size:10px;word-break:break-all">eyJhbGciOiJIUzI1NiJ9.eyJ…abc</em>`,warn:`⚠️ Nếu dán cả dấu <b>'</b> vào thì token sẽ không hoạt động và đăng nhập thất bại.`},
{label:'Hoặc dùng nút tự động (nhanh hơn)',desc:'Sang tab <b>➕ Thêm</b>, nhấn nút <em>⚡ Lấy token acc hiện tại</em> — script sẽ tự đọc cookie và điền token vào ô, không cần làm thủ công bước 5–6.'},
{label:'Điền tên và lưu tài khoản',desc:'Sang tab <b>➕ Thêm</b>, nhập <b>Tên hiển thị</b> (vd: <em>main</em>, <em>alt1</em>), dán token vào ô <b>JWT Token</b>, nhấn <em>Lưu</em>.'},
{label:'Muốn lưu nhiều acc? Đăng xuất → đăng nhập acc khác → lặp lại',desc:'Mỗi lần đăng nhập bằng acc khác, token mới sẽ được ghi vào cookie. Lặp lại từ bước 1 để lưu thêm tài khoản.'}
]
},
en: {
tabAccounts:'📋 Accounts', tabAdd:'➕ Add', tabGuide:'📖 Guide',
searchPlaceholder:'Search accounts…',
emptyNoAccounts:'No accounts yet',
emptyNoAccountsSub:'Go to the <b style="color:#58CC02">➕ Add</b> tab to get started',
emptyNotFound:'Nothing found',
labelPlaceholder:'Display name (e.g. main, alt1…)',
tokenPlaceholder:'JWT Token',
btnSave:'Save', btnGetToken:'⚡ Grab current token', btnGetTokenSuccess:'✓ Token filled!',
toastSwitching:'⏳ Switching account…', toastNoLabel:'⚠️ Enter a display name',
toastNoToken:'⚠️ Enter a JWT Token', toastDuplicateLabel:'⚠️ Name already exists',
toastSaved:n=>`✓ Saved: ${n}`, toastDeleted:n=>`Deleted: ${n}`,
toastNoTokenFound:'⚠️ No token found — are you logged in?',
toastCookieError:'⚠️ Error reading cookie', toastCopyFail:'⚠️ Copy failed, please copy manually',
toastLoginError:'⚠️ Token invalid or expired',
toastTokenDetected:'🔴 Token error — login failed, please update your token',
toastLoginSuccess:n=>`✓ Active: ${n}`,
toastBadFormat:'⚠️ Invalid token format — must start with eyJhb',
badgeCurrent:'Current', btnLogin:'Login', btnDelete:'Delete', btnEdit:'Edit',
confirmTitle:'Confirm login', confirmMsg:name=>`Log in with account <strong>${name}</strong>?`,
btnCancel:'Cancel', btnOk:'Log in',
editTitle:'Edit account', editLabelField:'Display name', editTokenField:'JWT Token',
editPreviewField:'Full token preview',
editNote:'⚠️ Token must start with <b>eyJhb</b>. Re-login after saving to apply the new token.',
settingTheme:'🎨 Theme', themeAuto:'🌗 Auto', themeDark:'🌙 Dark', themeLight:'☀️ Light',
settingTransparent:'🪟 Transparent background', settingTransparentDesc:'Frosted glass effect (backdrop blur)',
settingOpacity:'💧 Opacity', settingLanguage:'🌐 Language',
btnSettingsSave:'💾 Save settings', toastSettingsSaved:'✓ Settings saved',
langVi:'🇻🇳 Tiếng Việt', langEn:'🇬🇧 English',
guideTitle:'📖 How to get your JWT Token',
guideSteps:[
{label:'Log in to Duolingo in your browser',desc:'Go to <b>duolingo.com</b> and sign in with the account you want to save. Make sure you reach the learning page (<b>/learn</b>).'},
{label:'Open DevTools',desc:'Press <em>F12</em> or <em>Ctrl + Shift + I</em><br>On Mac: <em>Cmd + Option + I</em><br>Or right-click the page → <b>Inspect</b>'},
{label:'Switch to the Console tab',desc:'In DevTools, click the <b>Console</b> tab at the top.'},
{label:'Dismiss the "paste" warning if it appears',desc:'Chrome/Edge may show a security warning asking you to type <b>allow pasting</b> and press Enter before you can paste commands. Do that first, then continue.'},
{label:'Paste the token command into the Console',desc:'Copy the command below, paste it in the Console, then press <em>Enter</em>:',code:true,extra:'The token will appear in the Console — a long string starting with <em>eyJ…</em>'},
{label:"Copy the token — remove the single quotes ' at both ends",desc:`The Console output will look like this:<br><em style="font-size:10px;word-break:break-all">'eyJhbGciOiJIUzI1NiJ9.eyJ…'</em><br><br>You need to <b>remove the single quotes</b> <em>'</em> at the start and end before pasting the token into the field. Copy only the middle part, e.g.:<br><em style="font-size:10px;word-break:break-all">eyJhbGciOiJIUzI1NiJ9.eyJ…abc</em>`,warn:`⚠️ If you include the <b>'</b> quotes, the token won't work and login will fail.`},
{label:'Or use the auto button (faster)',desc:'Go to the <b>➕ Add</b> tab and click <em>⚡ Grab current token</em> — the script will read the cookie and fill in the token automatically, skipping steps 5–6.'},
{label:'Enter a name and save the account',desc:'In the <b>➕ Add</b> tab, enter a <b>Display name</b> (e.g. <em>main</em>, <em>alt1</em>), paste the token into the <b>JWT Token</b> field, and click <em>Save</em>.'},
{label:'Want to save more accounts? Log out → log in as another → repeat',desc:'Each time you log in as a different account, a new token is written to the cookie. Repeat from step 1 to save more accounts.'}
]
}
};
const t = (key,...args) => { const v=LANGS[settings.lang||'vi'][key]; return typeof v==='function'?v(...args):(v||key); };
const escHtml = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
const sleep = ms => new Promise(r=>setTimeout(r,ms));
const loadAccounts = () => { try{return JSON.parse(GM_getValue(STORAGE_KEY,'[]'));}catch{return[];} };
const saveAccounts = list => GM_setValue(STORAGE_KEY,JSON.stringify(list));
const loadSettings = () => { try{return Object.assign({theme:'dark',themeMode:'auto',transparent:true,opacity:85,lang:'vi'},JSON.parse(GM_getValue(SETTINGS_KEY,'{}')));}catch{return{theme:'dark',themeMode:'auto',transparent:true,opacity:85,lang:'vi'};} };
const saveSettings = s => GM_setValue(SETTINGS_KEY,JSON.stringify(s));
const getCurrentToken = () => { try{return document.cookie.split('; ').find(x=>x.startsWith('jwt_token='))?.split('=')[1]||null;}catch{return null;} };
const isCurrentAccount = acc => {
if (location.pathname.startsWith('/login') || location.search.includes('loginRedirect')) return false;
const tk = getCurrentToken();
return !!(tk && acc.token === tk);
};
// ─── Background account detection ──────────────────────────────────────────
function detectCurrentAccount() {
if (location.pathname.startsWith('/login') || location.search.includes('loginRedirect')) return null;
const tk = getCurrentToken();
if (!tk) return null;
const list = loadAccounts();
return list.find(a => a.token === tk) || null;
}
function updateFab() {
fab.innerHTML = USER_SVG;
fab.style.background = '#58CC02';
fab.style.boxShadow = '0 3px 0 0 #46A302,0 4px 16px rgba(88,204,2,.35)';
fab.style.fontSize = '26px';
fab.style.fontWeight = '';
fab.style.color = '#fff';
}
async function loginByToken(token) {
panel.classList.add('hidden');
showToast(t('toastSwitching'));
document.cookie=`jwt_token=${token}; path=/; domain=.duolingo.com`;
await sleep(400);
const set=getCurrentToken();
if(!set||set!==token){showToast(t('toastLoginError'));panel.classList.remove('hidden');return;}
GM_setValue(PENDING_LOGIN_KEY, '1');
location.replace('https://www.duolingo.com/learn');
}
function makeDraggable(el,handle) {
let x=0,y=0,mx=0,my=0;
handle.style.cursor='move';
handle.onmousedown=e=>{
e.preventDefault(); mx=e.clientX; my=e.clientY;
document.onmouseup=()=>{document.onmouseup=null;document.onmousemove=null;};
document.onmousemove=e=>{
x=mx-e.clientX; y=my-e.clientY; mx=e.clientX; my=e.clientY;
el.style.top=(el.offsetTop-y)+'px'; el.style.left=(el.offsetLeft-x)+'px';
el.style.right='auto'; el.style.bottom='auto';
};
};
}
const buildAvatarEl = (acc,ci) => {
const el=document.createElement('div');
el.className=`dam-avatar dam-avatar-${ci%6}`;
el.textContent=acc.label.slice(0,2).toUpperCase();
return el;
};
GM_addStyle(`
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
#duo-am-panel {
--dam-r:16px; --dam-inp-color:rgba(255,255,255,0.07); --dam-acc-color:rgba(255,255,255,0.04);
--dam-border:rgba(255,255,255,0.09); --dam-text:#E6E6E6; --dam-text-muted:#c0c0c0;
--dam-text-dim:#999; --dam-text-dim2:#777; --dam-accent:#58CC02; --dam-accent-dk:#46A302;
--dam-accent-lt:#61E002; --dam-red:#FF6B6B; --dam-scroll:rgba(255,255,255,0.13);
--dam-ring-gap:#12131a; --dam-bg-solid:rgba(18,19,26,0.97); --dam-bg-glass:rgba(18,19,26,0.72);
}
#duo-am-panel.dam-light {
--dam-inp-color:rgba(0,0,0,0.06); --dam-acc-color:rgba(0,0,0,0.04); --dam-border:rgba(0,0,0,0.09);
--dam-text:#1a1a1a; --dam-text-muted:#444; --dam-text-dim:#666; --dam-text-dim2:#888;
--dam-red:#e53935; --dam-scroll:rgba(0,0,0,0.13); --dam-ring-gap:#f0f1f5;
--dam-bg-solid:rgba(240,241,245,0.97); --dam-bg-glass:rgba(240,241,245,0.72);
}
#duo-am-fab {
position:fixed; bottom:24px; right:24px; z-index:99999;
width:48px; height:48px; border-radius:50%; background:#58CC02; border:none;
box-shadow:0 3px 0 0 #46A302,0 4px 16px rgba(88,204,2,.35);
cursor:grab; display:flex; align-items:center; justify-content:center;
font-size:26px; color:#fff; transition:background .15s,box-shadow .1s;
touch-action:none; user-select:none;
}
#duo-am-fab:hover{background:#61E002;}
#duo-am-fab.dam-fab-dragging{cursor:grabbing;box-shadow:0 6px 24px rgba(88,204,2,.5);}
#duo-am-fab.dam-fab-pressed{transform:translateY(2px);box-shadow:0 1px 0 0 #46A302 !important;}
#duo-am-panel {
position:fixed; top:72px; right:20px; z-index:99999; width:360px;
border-radius:var(--dam-r); border:1px solid var(--dam-border);
box-shadow:0 8px 32px rgba(0,0,0,.35); font-family:'Inter',sans-serif;
font-size:13px; color:var(--dam-text); transition:opacity .18s,transform .18s;
transform-origin:top right; isolation:isolate; overflow:hidden;
}
#duo-am-panel.hidden{opacity:0;transform:scale(0.92) translateY(-6px);pointer-events:none;}
#duo-am-panel::before {
content:''; position:absolute; inset:0; z-index:-1;
background:var(--dam-panel-bg,var(--dam-bg-glass));
backdrop-filter:var(--dam-panel-filter,blur(24px) saturate(180%));
-webkit-backdrop-filter:var(--dam-panel-filter,blur(24px) saturate(180%));
}
.dam-header {
display:flex; align-items:center; gap:8px; padding:12px 14px; user-select:none;
border-bottom:1px solid var(--dam-border); border-radius:var(--dam-r) var(--dam-r) 0 0;
}
.dam-header h2{margin:0;font-size:14px;font-weight:800;color:var(--dam-accent);flex:1;letter-spacing:-.3px;}
.dam-icon-btn {
width:26px; height:26px; border-radius:6px; border:none; cursor:pointer;
background:transparent; color:var(--dam-text-dim); font-size:13px;
display:flex; align-items:center; justify-content:center;
transition:background .15s,color .15s; flex-shrink:0;
}
.dam-icon-btn:hover{background:var(--dam-border);color:var(--dam-text);}
.dam-icon-btn.close:hover{background:rgba(255,107,107,.15);color:var(--dam-red);}
.dam-icon-btn.active{background:var(--dam-accent);color:#fff;}
.dam-tabs{display:flex;border-bottom:1px solid var(--dam-border);}
.dam-tab {
flex:1; padding:9px 0; font-size:11px; font-weight:700; font-family:'Inter',sans-serif;
border:none; background:transparent; color:var(--dam-text-dim); cursor:pointer;
border-bottom:2px solid transparent; transition:color .15s,border-color .15s;
}
.dam-tab.active{color:var(--dam-accent);border-bottom:2px solid var(--dam-accent);}
.dam-tab:hover:not(.active){color:var(--dam-text-muted);}
.dam-pane{display:none;} .dam-pane.active{display:block;}
.dam-add{padding:12px 14px;display:flex;flex-direction:column;gap:0;}
.dam-inp {
flex:1; padding:8px 10px; border:1px solid var(--dam-border); border-radius:8px;
font-size:12px; font-family:'Inter',sans-serif; font-weight:500; outline:none;
background:var(--dam-inp-color); box-sizing:border-box; color:var(--dam-text); transition:border-color .15s;
}
.dam-inp:focus{border-color:var(--dam-accent);}
.dam-inp::placeholder{color:var(--dam-text-dim2);}
.dam-inp-full{width:100%;margin-bottom:8px;}
.dam-token-wrap{position:relative;display:flex;align-items:center;margin-bottom:8px;}
.dam-token-wrap .dam-inp{width:100%;padding-right:62px;}
.dam-btn-clear-token {
position:absolute; right:34px; top:50%; transform:translateY(-50%);
width:22px; height:22px; border-radius:5px; border:none; cursor:pointer;
background:transparent; color:var(--dam-text-dim); font-size:11px; font-weight:700;
display:flex; align-items:center; justify-content:center; transition:background .15s,color .15s; flex-shrink:0; padding:0;
}
.dam-btn-clear-token:hover{background:rgba(255,107,107,.15);color:var(--dam-red);}
.dam-btn-eye {
position:absolute; right:6px; top:50%; transform:translateY(-50%);
width:24px; height:24px; border-radius:5px; border:none; cursor:pointer;
background:transparent; color:var(--dam-text-dim); font-size:13px;
display:flex; align-items:center; justify-content:center; transition:background .15s,color .15s; flex-shrink:0; padding:0;
}
.dam-btn-eye:hover{background:var(--dam-border);color:var(--dam-text);}
.dam-btn-row{display:flex;gap:3px;margin-top:4px;}
.dam-btn-add {
flex:6; height:40px; background:var(--dam-accent); color:#fff; border:none;
border-radius:10px; font-size:13px; font-weight:800; font-family:'Inter',sans-serif; cursor:pointer;
box-shadow:0 3px 0 0 var(--dam-accent-dk); transition:background .15s,transform .1s,box-shadow .1s;
}
.dam-btn-add:hover{background:var(--dam-accent-lt);}
.dam-btn-add:active{transform:translateY(2px);box-shadow:0 1px 0 0 var(--dam-accent-dk);}
.dam-btn-gettoken {
flex:4; height:40px; background:var(--dam-accent); color:#fff; border:none; border-radius:10px;
font-size:13px; font-weight:800; font-family:'Inter',sans-serif; cursor:pointer;
white-space:nowrap; overflow:hidden; box-shadow:0 3px 0 0 var(--dam-accent-dk);
transition:background .15s,transform .1s,box-shadow .1s;
display:flex; align-items:center; justify-content:center; gap:5px;
}
.dam-btn-gettoken:hover{background:var(--dam-accent-lt);}
.dam-btn-gettoken:active{transform:translateY(2px);box-shadow:0 1px 0 0 var(--dam-accent-dk);}
.dam-btn-gettoken.success{background:#46A302;}
.dam-search-wrap{padding:8px 14px;border-bottom:1px solid var(--dam-border);}
.dam-search {
width:100%; padding:7px 10px 7px 30px; box-sizing:border-box;
border:1px solid var(--dam-border); border-radius:8px; font-size:12px;
font-family:'Inter',sans-serif; outline:none; background:var(--dam-inp-color); color:var(--dam-text);
transition:border-color .15s;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E");
background-repeat:no-repeat; background-position:9px center;
}
.dam-search:focus{border-color:var(--dam-accent);}
.dam-search::placeholder{color:var(--dam-text-dim2);}
.dam-list{max-height:260px;overflow-y:auto;}
.dam-list::-webkit-scrollbar{width:3px;}
.dam-list::-webkit-scrollbar-thumb{background:var(--dam-scroll);border-radius:4px;}
.dam-empty{padding:28px 14px;text-align:center;color:var(--dam-text-dim2);font-size:12px;line-height:1.6;}
.dam-empty-icon{font-size:30px;display:block;margin-bottom:8px;}
.dam-acc {
display:flex; align-items:center; gap:8px; padding:9px 14px;
transition:background .1s; border-top:1px solid var(--dam-border);
}
.dam-acc:first-child{border-top:none;}
.dam-acc:hover{background:var(--dam-acc-color);}
.dam-acc:hover .dam-btn-del,
.dam-acc:hover .dam-btn-edit-inline{opacity:1;}
.dam-acc.dam-acc-current{background:rgba(88,204,2,0.06);}
.dam-acc.dam-acc-current:hover{background:rgba(88,204,2,0.10);}
.dam-avatar {
width:36px; height:36px; border-radius:50%; flex-shrink:0;
display:flex; align-items:center; justify-content:center;
font-weight:800; font-size:12px; overflow:hidden;
}
.dam-avatar-0{background:#1C3D00;color:#58CC02;} .dam-avatar-1{background:#00304A;color:#1CB0F6;}
.dam-avatar-2{background:#3A1F52;color:#CE82FF;} .dam-avatar-3{background:#433200;color:#FFC800;}
.dam-avatar-4{background:#4A1515;color:#FF6B6B;} .dam-avatar-5{background:#4A2900;color:#FF9600;}
#duo-am-panel.dam-light .dam-avatar-0{background:#d4f5a8;color:#2a7000;}
#duo-am-panel.dam-light .dam-avatar-1{background:#b3e8ff;color:#005f8a;}
#duo-am-panel.dam-light .dam-avatar-2{background:#e8d0ff;color:#6b00b3;}
#duo-am-panel.dam-light .dam-avatar-3{background:#fff0b3;color:#7a5c00;}
#duo-am-panel.dam-light .dam-avatar-4{background:#ffd0d0;color:#a00000;}
#duo-am-panel.dam-light .dam-avatar-5{background:#ffe0b3;color:#8a4500;}
.dam-avatar-current-ring{box-shadow:0 0 0 2px var(--dam-ring-gap),0 0 0 4px #58CC02;}
.dam-info{flex:1;min-width:0;}
.dam-name{font-weight:700;font-size:13px;color:var(--dam-text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.dam-sub-row{display:flex;align-items:center;gap:4px;margin-top:2px;min-width:0;}
.dam-sub{font-size:10px;color:var(--dam-text-dim2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:140px;}
/* Edit button — inline next to token, visible on hover */
.dam-btn-edit-inline {
width:18px; height:18px; flex-shrink:0; opacity:0;
background:transparent; border:1px solid var(--dam-border); border-radius:4px;
cursor:pointer; color:var(--dam-text-dim); font-size:10px;
display:flex; align-items:center; justify-content:center; transition:all .15s; padding:0;
}
.dam-btn-edit-inline:hover{border-color:var(--dam-accent);color:var(--dam-accent);background:rgba(88,204,2,0.07);}
/* Delete button */
.dam-btn-del {
width:24px; height:24px; flex-shrink:0; opacity:0;
background:transparent; border:1px solid var(--dam-border); border-radius:6px;
cursor:pointer; color:var(--dam-text-dim); font-size:10px;
display:flex; align-items:center; justify-content:center; transition:all .15s;
}
.dam-btn-del:hover{border-color:var(--dam-red);color:var(--dam-red);background:rgba(255,107,107,.1);}
/* Login button */
.dam-btn-login {
padding:0 10px; height:28px; background:var(--dam-accent); color:#fff;
border:none; border-radius:6px; font-size:11px; font-weight:700;
font-family:'Inter',sans-serif; cursor:pointer; flex-shrink:0;
box-shadow:0 2px 0 0 var(--dam-accent-dk); transition:background .15s,transform .1s,box-shadow .1s;
}
.dam-btn-login:hover{background:var(--dam-accent-lt);}
.dam-btn-login:active{transform:translateY(1px);box-shadow:none;}
.dam-badge-current {
display:flex; align-items:center; gap:4px; padding:0 9px; height:28px; flex-shrink:0;
background:rgba(88,204,2,0.12); border:1px solid rgba(88,204,2,0.35); border-radius:6px;
font-size:10px; font-weight:800; font-family:'Inter',sans-serif; color:var(--dam-accent);
letter-spacing:.3px; pointer-events:none; white-space:nowrap;
}
.dam-badge-current::before {
content:''; width:6px; height:6px; border-radius:50%; background:var(--dam-accent);
box-shadow:0 0 5px rgba(88,204,2,.7); animation:dam-pulse 1.8s ease-in-out infinite; flex-shrink:0;
}
@keyframes dam-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.7)}}
.dam-settings{padding:14px;display:flex;flex-direction:column;gap:14px;}
.dam-setting-row{display:flex;flex-direction:column;gap:6px;}
.dam-setting-label{font-size:11px;font-weight:700;color:var(--dam-text-dim);text-transform:uppercase;letter-spacing:.6px;}
.dam-theme-pills,.dam-lang-pills{display:flex;gap:6px;}
.dam-pill,.dam-lang-pill {
flex:1; height:32px; border-radius:8px; border:1px solid var(--dam-border);
background:var(--dam-inp-color); color:var(--dam-text-muted); font-size:11px; font-weight:700;
font-family:'Inter',sans-serif; cursor:pointer; transition:all .15s;
display:flex; align-items:center; justify-content:center; gap:4px;
}
.dam-pill:hover,.dam-lang-pill:hover{border-color:var(--dam-accent);color:var(--dam-accent);}
.dam-pill.active,.dam-lang-pill.active{background:var(--dam-accent);border-color:var(--dam-accent-dk);color:#fff;box-shadow:0 2px 0 0 var(--dam-accent-dk);}
.dam-toggle-row{display:flex;align-items:center;justify-content:space-between;}
.dam-toggle-title{font-size:12px;font-weight:600;color:var(--dam-text);}
.dam-toggle-desc{font-size:10px;color:var(--dam-text-dim2);margin-top:2px;}
.dam-switch{position:relative;width:40px;height:22px;flex-shrink:0;}
.dam-switch input{opacity:0;width:0;height:0;position:absolute;}
.dam-switch-track{position:absolute;inset:0;background:var(--dam-border);border-radius:11px;cursor:pointer;transition:background .2s;}
.dam-switch-track::before{content:'';position:absolute;width:16px;height:16px;border-radius:50%;background:#fff;top:3px;left:3px;transition:transform .2s;box-shadow:0 1px 3px rgba(0,0,0,.3);}
.dam-switch input:checked+.dam-switch-track{background:var(--dam-accent);}
.dam-switch input:checked+.dam-switch-track::before{transform:translateX(18px);}
.dam-slider-row{display:flex;align-items:center;gap:10px;}
.dam-slider{-webkit-appearance:none;appearance:none;flex:1;height:4px;border-radius:4px;background:var(--dam-border);outline:none;cursor:pointer;}
.dam-slider::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;border-radius:50%;background:var(--dam-accent);cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.4);transition:background .15s,transform .1s;}
.dam-slider::-webkit-slider-thumb:hover{background:var(--dam-accent-lt);transform:scale(1.15);}
.dam-slider::-moz-range-thumb{width:16px;height:16px;border-radius:50%;border:none;background:var(--dam-accent);cursor:pointer;}
.dam-slider-val{font-size:11px;font-weight:700;color:var(--dam-accent);width:32px;text-align:right;flex-shrink:0;}
.dam-settings-divider{height:1px;background:var(--dam-border);margin:2px 0;}
.dam-opacity-row-disabled{opacity:0.35;pointer-events:none;}
.dam-settings-footer{padding:12px 14px;border-top:1px solid var(--dam-border);}
.dam-btn-settings-save {
width:100%; height:38px; background:var(--dam-accent); color:#fff; border:none; border-radius:10px;
font-size:13px; font-weight:800; font-family:'Inter',sans-serif; cursor:pointer;
box-shadow:0 3px 0 0 var(--dam-accent-dk); transition:background .15s,transform .1s,box-shadow .1s;
}
.dam-btn-settings-save:hover{background:var(--dam-accent-lt);}
.dam-btn-settings-save:active{transform:translateY(2px);box-shadow:0 1px 0 0 var(--dam-accent-dk);}
.dam-btn-settings-save.saved{background:#46A302;}
.dam-guide{padding:12px 14px 16px;display:flex;flex-direction:column;gap:0;max-height:340px;overflow-y:auto;}
.dam-guide::-webkit-scrollbar{width:3px;}
.dam-guide::-webkit-scrollbar-thumb{background:var(--dam-scroll);border-radius:4px;}
.dam-guide-title{font-size:11px;font-weight:800;color:var(--dam-accent);text-transform:uppercase;letter-spacing:.7px;margin-bottom:10px;}
.dam-step{display:flex;gap:10px;padding:9px 0;border-bottom:1px solid var(--dam-border);}
.dam-step:last-child{border-bottom:none;}
.dam-step-num {
width:20px; height:20px; border-radius:50%; flex-shrink:0;
background:rgba(88,204,2,0.13); border:1px solid rgba(88,204,2,0.3);
color:var(--dam-accent); font-size:10px; font-weight:800;
display:flex; align-items:center; justify-content:center; margin-top:1px;
}
.dam-step-body{flex:1;min-width:0;}
.dam-step-label{font-size:12px;font-weight:700;color:var(--dam-text);line-height:1.4;margin-bottom:4px;}
.dam-step-desc{font-size:11px;color:var(--dam-text-muted);line-height:1.55;}
.dam-step-desc b{color:var(--dam-text);font-weight:700;}
.dam-step-desc em{font-style:normal;color:var(--dam-accent);background:rgba(88,204,2,0.1);border-radius:3px;padding:0 4px;font-size:10.5px;font-weight:600;}
.dam-code-block {
margin-top:6px; position:relative; background:rgba(0,0,0,0.28);
border:1px solid var(--dam-border); border-radius:7px; padding:8px 34px 8px 10px;
font-family:'Consolas','Fira Code',monospace; font-size:10.5px; color:#b5f5a0;
line-height:1.5; word-break:break-all; cursor:pointer; transition:border-color .15s;
}
.dam-code-block:hover{border-color:rgba(88,204,2,0.4);}
.dam-code-block:hover .dam-copy-hint{opacity:1;}
.dam-copy-hint {
position:absolute; top:50%; right:7px; transform:translateY(-50%);
font-size:9px; font-weight:800; color:var(--dam-accent); opacity:0; transition:opacity .15s;
pointer-events:none; text-transform:uppercase; letter-spacing:.4px;
background:rgba(0,0,0,0.4); padding:2px 5px; border-radius:4px;
}
.dam-copy-hint.copied{opacity:1;color:#fff;background:var(--dam-accent);}
.dam-warn-box{margin-top:8px;padding:8px 10px;background:rgba(255,200,0,0.07);border:1px solid rgba(255,200,0,0.2);border-radius:7px;font-size:11px;color:#d4b000;line-height:1.55;}
#duo-am-panel.dam-light .dam-warn-box{color:#8a7000;background:rgba(255,200,0,0.08);}
#duo-am-panel.dam-light .dam-code-block{color:#1a5c00;background:rgba(0,0,0,0.06);}
#duo-am-panel.dam-light .dam-step-desc em{background:rgba(88,204,2,0.15);}
/* Confirm modal */
#duo-am-confirm {
position:fixed; inset:0; z-index:9999999; background:rgba(0,0,0,.65);
backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px);
display:flex; align-items:center; justify-content:center; font-family:'Inter',sans-serif;
}
#duo-am-confirm.hidden{display:none;}
.dam-modal{position:relative;isolation:isolate;border-radius:16px;border:1px solid rgba(255,255,255,0.09);box-shadow:0 16px 48px rgba(0,0,0,.7);width:260px;overflow:hidden;}
.dam-modal::before{content:'';position:absolute;inset:0;z-index:-1;background:rgba(18,19,22,0.92);backdrop-filter:blur(24px) saturate(200%);-webkit-backdrop-filter:blur(24px) saturate(200%);}
.dam-modal-head{padding:16px;background:rgba(0,0,0,0.22);border-bottom:1px solid rgba(255,255,255,0.09);display:flex;align-items:center;gap:10px;}
.dam-modal-head span{font-size:24px;display:flex;align-items:center;color:#58CC02;}
.dam-modal-head h3{margin:0;font-size:14px;font-weight:800;color:#58CC02;}
.dam-modal-body{padding:16px;}
.dam-modal-body p{margin:0 0 16px;font-size:13px;color:#aaa;line-height:1.6;}
.dam-modal-body p strong{color:#E6E6E6;font-weight:700;}
.dam-modal-btns{display:flex;gap:8px;}
.dam-modal-btns button{flex:1;height:36px;border-radius:8px;font-size:13px;font-weight:700;font-family:'Inter',sans-serif;cursor:pointer;border:none;transition:all .15s;}
.dam-btn-cancel{background:rgba(255,255,255,0.07);color:#888;border:1px solid rgba(255,255,255,0.09) !important;}
.dam-btn-cancel:hover{background:rgba(255,255,255,0.12);color:#ccc;}
.dam-btn-ok{background:#58CC02;color:#fff;box-shadow:0 3px 0 0 #46A302;}
.dam-btn-ok:hover{background:#61E002;}
.dam-btn-ok:active{transform:translateY(2px);box-shadow:0 1px 0 0 #46A302;}
#duo-am-confirm.dam-light .dam-modal{border-color:rgba(0,0,0,0.10);box-shadow:0 16px 48px rgba(0,0,0,.18);}
#duo-am-confirm.dam-light .dam-modal::before{background:rgba(240,241,245,0.96);}
#duo-am-confirm.dam-light .dam-modal-head{background:rgba(0,0,0,0.04);border-bottom-color:rgba(0,0,0,0.08);}
#duo-am-confirm.dam-light .dam-modal-head h3{color:#2a7000;}
#duo-am-confirm.dam-light .dam-modal-body p{color:#555;}
#duo-am-confirm.dam-light .dam-modal-body p strong{color:#1a1a1a;}
#duo-am-confirm.dam-light .dam-btn-cancel{background:rgba(0,0,0,0.06) !important;color:#555 !important;border-color:rgba(0,0,0,0.10) !important;}
#duo-am-confirm.dam-light .dam-btn-cancel:hover{background:rgba(0,0,0,0.11) !important;color:#222 !important;}
.dam-confirm-avatar{width:48px;height:48px;border-radius:50%;overflow:hidden;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:16px;margin:0 auto 12px;}
/* Edit modal */
#duo-am-edit {
position:fixed; inset:0; z-index:9999999; background:rgba(0,0,0,.65);
backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px);
display:flex; align-items:center; justify-content:center; font-family:'Inter',sans-serif;
}
#duo-am-edit.hidden{display:none;}
.dam-edit-modal{position:relative;isolation:isolate;border-radius:16px;border:1px solid rgba(255,255,255,0.09);box-shadow:0 16px 48px rgba(0,0,0,.7);width:300px;overflow:hidden;}
.dam-edit-modal::before{content:'';position:absolute;inset:0;z-index:-1;background:rgba(18,19,22,0.94);backdrop-filter:blur(24px) saturate(200%);-webkit-backdrop-filter:blur(24px) saturate(200%);}
.dam-edit-head{padding:12px 14px;background:rgba(0,0,0,.22);border-bottom:1px solid rgba(255,255,255,0.09);display:flex;align-items:center;gap:8px;}
.dam-edit-head h3{margin:0;font-size:14px;font-weight:800;color:#58CC02;flex:1;}
.dam-edit-body{padding:14px;display:flex;flex-direction:column;gap:10px;}
.dam-edit-field-label{font-size:10px;font-weight:700;color:#777;text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px;}
.dam-edit-token-wrap{position:relative;}
.dam-edit-token-wrap .dam-inp{width:100%;padding-right:30px;font-family:'Consolas','Fira Code',monospace;font-size:11px;}
.dam-edit-eye{position:absolute;right:6px;top:50%;transform:translateY(-50%);width:22px;height:22px;border-radius:5px;border:none;cursor:pointer;background:transparent;color:#777;font-size:13px;display:flex;align-items:center;justify-content:center;padding:0;transition:background .15s,color .15s;}
.dam-edit-eye:hover{background:rgba(255,255,255,0.09);color:#E6E6E6;}
.dam-token-preview{background:rgba(0,0,0,.25);border:1px solid rgba(255,255,255,0.09);border-radius:7px;padding:7px 10px;font-family:'Consolas','Fira Code',monospace;font-size:10px;color:#b5f5a0;line-height:1.5;word-break:break-all;max-height:64px;overflow-y:auto;}
.dam-token-preview::-webkit-scrollbar{width:3px;}
.dam-token-preview::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.13);border-radius:4px;}
.dam-edit-note{font-size:10px;color:#666;line-height:1.55;}
.dam-edit-note b{color:#888;font-weight:700;}
.dam-edit-footer{padding:12px 14px;border-top:1px solid rgba(255,255,255,0.09);display:flex;gap:8px;}
.dam-edit-footer button{flex:1;height:36px;border-radius:8px;font-size:13px;font-weight:700;font-family:'Inter',sans-serif;cursor:pointer;border:none;transition:all .15s;}
.dam-btn-edit-cancel{background:rgba(255,255,255,0.07);color:#888;border:1px solid rgba(255,255,255,0.09) !important;}
.dam-btn-edit-cancel:hover{background:rgba(255,255,255,0.12);color:#ccc;}
.dam-btn-edit-save{background:#58CC02;color:#fff;box-shadow:0 3px 0 0 #46A302;}
.dam-btn-edit-save:hover{background:#61E002;}
.dam-btn-edit-save:active{transform:translateY(2px);box-shadow:0 1px 0 0 #46A302;}
#duo-am-edit.dam-light .dam-edit-modal{border-color:rgba(0,0,0,0.10);box-shadow:0 16px 48px rgba(0,0,0,.18);}
#duo-am-edit.dam-light .dam-edit-modal::before{background:rgba(240,241,245,0.96);}
#duo-am-edit.dam-light .dam-edit-head{background:rgba(0,0,0,0.04);border-bottom-color:rgba(0,0,0,0.08);}
#duo-am-edit.dam-light .dam-edit-head h3{color:#2a7000;}
#duo-am-edit.dam-light .dam-edit-field-label{color:#888;}
#duo-am-edit.dam-light .dam-edit-eye{color:#888;}
#duo-am-edit.dam-light .dam-edit-eye:hover{background:rgba(0,0,0,0.07);color:#333;}
#duo-am-edit.dam-light .dam-token-preview{color:#1a5c00;background:rgba(0,0,0,0.06);border-color:rgba(0,0,0,0.09);}
#duo-am-edit.dam-light .dam-token-preview::-webkit-scrollbar-thumb{background:rgba(0,0,0,0.13);}
#duo-am-edit.dam-light .dam-edit-note{color:#888;}
#duo-am-edit.dam-light .dam-edit-note b{color:#666;}
#duo-am-edit.dam-light .dam-edit-footer{border-top-color:rgba(0,0,0,0.08);}
#duo-am-edit.dam-light .dam-btn-edit-cancel{background:rgba(0,0,0,0.06) !important;color:#555 !important;border-color:rgba(0,0,0,0.10) !important;}
#duo-am-edit.dam-light .dam-btn-edit-cancel:hover{background:rgba(0,0,0,0.11) !important;color:#222 !important;}
/* Toast */
#duo-am-toast {
position:fixed; bottom:84px; right:24px; z-index:9999999; isolation:isolate;
border-radius:10px; border:1px solid rgba(255,255,255,0.09); box-shadow:0 4px 16px rgba(0,0,0,.5);
color:#E6E6E6; padding:9px 16px; font-size:12px; font-weight:600; font-family:'Inter',sans-serif;
opacity:0; transform:translateY(10px); transition:opacity .2s,transform .2s;
pointer-events:none; white-space:nowrap;
}
#duo-am-toast::before{content:'';position:absolute;inset:0;z-index:-1;border-radius:10px;background:rgba(18,19,22,0.92);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);}
#duo-am-toast.show{opacity:1;transform:translateY(0);}
#duo-am-toast.dam-light{color:#1a1a1a;border-color:rgba(0,0,0,0.10);box-shadow:0 4px 16px rgba(0,0,0,.12);}
#duo-am-toast.dam-light::before{background:rgba(240,241,245,0.96);}
`);
// ─── SVG Icons ───────────────────────────────────────────────────────────────
const USER_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" style="width:1em;height:1em;display:block"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/></svg>`;
const EYE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px;height:14px;display:block"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>`;
const EYE_OFF = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px;height:14px;display:block"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>`;
// Pencil-line icon (edit) — different from the old ✏️ emoji
const EDIT_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:10px;height:10px;display:block"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`;
// ─── DOM elements ─────────────────────────────────────────────────────────────
const fab = Object.assign(document.createElement('button'), {id:'duo-am-fab',title:'DuoAM',innerHTML:USER_SVG});
const panel = Object.assign(document.createElement('div'), {id:'duo-am-panel'}); panel.classList.add('hidden');
const confirmEl = Object.assign(document.createElement('div'), {id:'duo-am-confirm'}); confirmEl.classList.add('hidden');
const editEl = Object.assign(document.createElement('div'), {id:'duo-am-edit'}); editEl.classList.add('hidden');
const toast = Object.assign(document.createElement('div'), {id:'duo-am-toast'});
document.body.append(fab, panel, confirmEl, editEl, toast);
const TOKEN_SNIPPET = "document.cookie.split('; ').find(x=>x.startsWith('jwt_token='))?.split('=')[1]";
let settings = loadSettings();
function buildUI() {
panel.innerHTML = `
<div class="dam-header" id="dam-drag-handle">
<span style="font-size:17px;display:flex;align-items:center;color:var(--dam-accent)">${USER_SVG}</span>
<h2>DuoAM - v1.4.0</h2>
<button class="dam-icon-btn" id="dam-feedback-btn" title="Feedback">💬</button>
<button class="dam-icon-btn" id="dam-settings-btn" title="Settings">⚙️</button>
<button class="dam-icon-btn close" id="dam-close">✕</button>
</div>
<div class="dam-tabs" id="dam-tabs">
<button class="dam-tab active" data-tab="accounts">${t('tabAccounts')}</button>
<button class="dam-tab" data-tab="add">${t('tabAdd')}</button>
<button class="dam-tab" data-tab="guide">${t('tabGuide')}</button>
</div>
<div class="dam-pane active" id="dam-pane-accounts">
<div class="dam-search-wrap">
<input class="dam-search" id="dam-search" type="text" placeholder="${t('searchPlaceholder')}" autocomplete="off"/>
</div>
<div class="dam-list" id="dam-list"></div>
</div>
<div class="dam-pane" id="dam-pane-add">
<div class="dam-add">
<input class="dam-inp dam-inp-full" type="text" id="dam-inp-label" placeholder="${t('labelPlaceholder')}" autocomplete="off"/>
<div class="dam-token-wrap">
<input class="dam-inp" type="password" id="dam-inp-token" placeholder="${t('tokenPlaceholder')}" autocomplete="off"/>
<button class="dam-btn-clear-token" id="dam-btn-clear-token" title="Xóa token" style="display:none">✕</button>
<button class="dam-btn-eye" id="dam-btn-eye" title="Show/hide token">${EYE_SVG}</button>
</div>
<div class="dam-btn-row">
<button class="dam-btn-add" id="dam-btn-add">💾 ${t('btnSave')}</button>
<button class="dam-btn-gettoken" id="dam-btn-gettoken" title="${t('btnGetToken')}">⚡ ${settings.lang==='en'?'Grab token':'Lấy token'}</button>
</div>
</div>
</div>
<div class="dam-pane" id="dam-pane-guide">
<div class="dam-guide" id="dam-guide-content"></div>
</div>
<div class="dam-pane" id="dam-pane-settings">
<div class="dam-settings">
<div class="dam-setting-row">
<div class="dam-setting-label">${t('settingTheme')}</div>
<div class="dam-theme-pills">
<button class="dam-pill" data-theme="auto">${t('themeAuto')}</button>
<button class="dam-pill" data-theme="dark">${t('themeDark')}</button>
<button class="dam-pill" data-theme="light">${t('themeLight')}</button>
</div>
</div>
<div class="dam-settings-divider"></div>
<div class="dam-setting-row">
<div class="dam-toggle-row">
<div>
<div class="dam-toggle-title">${t('settingTransparent')}</div>
<div class="dam-toggle-desc">${t('settingTransparentDesc')}</div>
</div>
<label class="dam-switch">
<input type="checkbox" id="dam-toggle-transparent"/>
<span class="dam-switch-track"></span>
</label>
</div>
</div>
<div class="dam-setting-row" id="dam-opacity-row">
<div class="dam-setting-label">${t('settingOpacity')}</div>
<div class="dam-slider-row">
<input type="range" class="dam-slider" id="dam-slider-opacity" min="5" max="98" step="1"/>
<span class="dam-slider-val" id="dam-opacity-val"></span>
</div>
</div>
<div class="dam-settings-divider"></div>
<div class="dam-setting-row">
<div class="dam-setting-label">${t('settingLanguage')}</div>
<div class="dam-lang-pills">
<button class="dam-lang-pill" data-lang="vi">${t('langVi')}</button>
<button class="dam-lang-pill" data-lang="en">${t('langEn')}</button>
</div>
</div>
</div>
<div class="dam-settings-footer">
<button class="dam-btn-settings-save" id="dam-btn-settings-save">${t('btnSettingsSave')}</button>
</div>
</div>
`;
confirmEl.innerHTML = `
<div class="dam-modal">
<div class="dam-modal-head"><span>${USER_SVG}</span><h3>${t('confirmTitle')}</h3></div>
<div class="dam-modal-body">
<p id="dam-confirm-msg"></p>
<div class="dam-modal-btns">
<button class="dam-btn-cancel" id="dam-cancel">${t('btnCancel')}</button>
<button class="dam-btn-ok" id="dam-ok">${t('btnOk')}</button>
</div>
</div>
</div>
`;
editEl.innerHTML = `
<div class="dam-edit-modal">
<div class="dam-edit-head">
<h3>✏️ ${t('editTitle')}</h3>
<button class="dam-icon-btn close" id="dam-edit-close" style="color:#777">✕</button>
</div>
<div class="dam-edit-body">
<div>
<div class="dam-edit-field-label">${t('editLabelField')}</div>
<input class="dam-inp dam-inp-full" type="text" id="dam-edit-label" autocomplete="off" style="margin-bottom:0"/>
</div>
<div>
<div class="dam-edit-field-label">${t('editTokenField')}</div>
<div class="dam-edit-token-wrap">
<input class="dam-inp" type="password" id="dam-edit-token" autocomplete="off"/>
<button class="dam-edit-eye" id="dam-edit-eye">${EYE_SVG}</button>
</div>
</div>
<div>
<div class="dam-edit-field-label">${t('editPreviewField')}</div>
<div class="dam-token-preview" id="dam-token-preview">—</div>
</div>
<div class="dam-edit-note">${t('editNote')}</div>
</div>
<div class="dam-edit-footer">
<button class="dam-btn-edit-cancel" id="dam-edit-cancel">${t('btnCancel')}</button>
<button class="dam-btn-edit-save" id="dam-edit-save">💾 ${t('btnSave')}</button>
</div>
</div>
`;
// ─── Wire up events ───────────────────────────────────────────────────────
document.getElementById('dam-close').addEventListener('click', () => panel.classList.add('hidden'));
document.getElementById('dam-settings-btn').addEventListener('click', () => switchTab('settings'));
document.getElementById('dam-feedback-btn').addEventListener('click', () => window.open('https://greasyfork.org/en/scripts/581910-duoam/feedback','_blank'));
document.getElementById('dam-tabs').addEventListener('click', e => {
const tab = e.target.closest('.dam-tab');
if (tab) switchTab(tab.dataset.tab);
});
document.getElementById('dam-search').addEventListener('input', e => render(e.target.value));
document.getElementById('dam-btn-add').addEventListener('click', addAccount);
document.getElementById('dam-btn-gettoken').addEventListener('click', grabCurrentToken);
const tokenInp = document.getElementById('dam-inp-token');
const clearBtn = document.getElementById('dam-btn-clear-token');
const eyeBtn = document.getElementById('dam-btn-eye');
const toggleClearBtn = show => { clearBtn.style.display = show ? 'flex' : 'none'; };
tokenInp.addEventListener('input', () => toggleClearBtn(!!tokenInp.value));
clearBtn.addEventListener('click', () => { tokenInp.value=''; tokenInp.type='password'; eyeBtn.innerHTML=EYE_SVG; toggleClearBtn(false); tokenInp.focus(); });
eyeBtn.addEventListener('click', () => {
const isPass = tokenInp.type==='password';
tokenInp.type = isPass?'text':'password';
eyeBtn.innerHTML = isPass?EYE_OFF:EYE_SVG;
});
document.getElementById('dam-cancel').addEventListener('click', () => confirmEl.classList.add('hidden'));
document.getElementById('dam-ok').addEventListener('click', () => { confirmEl.classList.add('hidden'); if(pendingToken) loginByToken(pendingToken); });
document.getElementById('dam-edit-close').addEventListener('click', closeEdit);
document.getElementById('dam-edit-cancel').addEventListener('click', closeEdit);
document.getElementById('dam-edit-save').addEventListener('click', saveEdit);
document.getElementById('dam-edit-eye').addEventListener('click', () => {
const inp = document.getElementById('dam-edit-token');
const isPass = inp.type==='password';
inp.type = isPass?'text':'password';
document.getElementById('dam-edit-eye').innerHTML = isPass?EYE_OFF:EYE_SVG;
});
document.getElementById('dam-edit-token').addEventListener('input', () => {
const prev = document.getElementById('dam-token-preview');
if (prev) prev.textContent = document.getElementById('dam-edit-token').value || '—';
});
// Settings
document.querySelectorAll('.dam-pill').forEach(p => p.addEventListener('click', () => updateSetting({themeMode:p.dataset.theme,theme:p.dataset.theme})));
document.querySelectorAll('.dam-lang-pill').forEach(p => p.addEventListener('click', () => updateSetting({lang:p.dataset.lang})));
document.getElementById('dam-toggle-transparent').addEventListener('change', e => updateSetting({transparent:e.target.checked}));
document.getElementById('dam-slider-opacity').addEventListener('input', e => {
document.getElementById('dam-opacity-val').textContent = e.target.value + '%';
updateSetting({opacity:+e.target.value});
});
document.getElementById('dam-btn-settings-save').addEventListener('click', () => {
if (!pendingSettings) return;
settings = pendingSettings; pendingSettings = null;
saveSettings(settings);
applyTheme(settings);
buildUI(); applyTheme(settings); render();
const btn = document.getElementById('dam-btn-settings-save');
if (btn){ btn.classList.add('saved'); btn.textContent='✓'; setTimeout(()=>{btn.classList.remove('saved');btn.textContent=t('btnSettingsSave');},1200); }
showToast(t('toastSettingsSaved'));
});
makeDraggable(panel, document.getElementById('dam-drag-handle'));
applyTheme(settings);
syncSettingsUI(settings);
buildGuide();
render();
}
// ─── Tab switch ───────────────────────────────────────────────────────────────
function switchTab(name) {
document.querySelectorAll('.dam-tab').forEach(t => t.classList.toggle('active', t.dataset.tab===name));
document.querySelectorAll('.dam-pane').forEach(p => p.classList.toggle('active', p.id===`dam-pane-${name}`));
document.getElementById('dam-settings-btn')?.classList.toggle('active', name==='settings');
if (name==='accounts') render(document.getElementById('dam-search')?.value||'');
}
// ─── Guide ────────────────────────────────────────────────────────────────────
function buildGuide() {
const el = document.getElementById('dam-guide-content'); if (!el) return;
const steps = t('guideSteps');
el.innerHTML = `<div class="dam-guide-title">${t('guideTitle')}</div>` +
steps.map((s,i) => `
<div class="dam-step">
<div class="dam-step-num">${i+1}</div>
<div class="dam-step-body">
<div class="dam-step-label">${s.label}</div>
<div class="dam-step-desc">${s.desc}</div>
${s.code?`<div class="dam-code-block" id="dam-code-block"><span>${escHtml(TOKEN_SNIPPET)}</span><span class="dam-copy-hint">COPY</span></div>`:''}
${s.extra?`<div class="dam-step-desc" style="margin-top:6px">${s.extra}</div>`:''}
${s.warn?`<div class="dam-warn-box">${s.warn}</div>`:''}
</div>
</div>`).join('');
document.getElementById('dam-code-block')?.addEventListener('click', function() {
navigator.clipboard.writeText(TOKEN_SNIPPET).then(()=>{
const hint=this.querySelector('.dam-copy-hint');
hint.textContent='✓'; hint.classList.add('copied');
setTimeout(()=>{hint.textContent='COPY';hint.classList.remove('copied');},1500);
}).catch(()=>showToast(t('toastCopyFail')));
});
}
// ─── Grab current token ───────────────────────────────────────────────────────
function grabCurrentToken() {
const btn = document.getElementById('dam-btn-gettoken');
let token;
try { token = getCurrentToken(); } catch { showToast(t('toastCookieError')); return; }
if (!token) { showToast(t('toastNoTokenFound')); return; }
document.getElementById('dam-inp-token').value = token;
document.getElementById('dam-inp-token').type = 'password';
document.getElementById('dam-btn-eye').innerHTML = EYE_SVG;
document.getElementById('dam-btn-clear-token').style.display = 'flex';
btn.classList.add('success'); btn.textContent = t('btnGetTokenSuccess');
setTimeout(()=>{ btn.classList.remove('success'); btn.textContent=`⚡ ${settings.lang==='en'?'Grab token':'Lấy token'}`; }, 2000);
}
// ─── Theme ────────────────────────────────────────────────────────────────────
function detectDuoTheme() {
const html = document.documentElement;
const body = document.body;
// 1. Explicit class/attribute markers
const darkMarkers = [
()=>html.classList.contains('dark')||html.classList.contains('night'),
()=>body&&(body.classList.contains('dark')||body.classList.contains('night')),
()=>html.dataset.colorScheme?.includes('dark'),
()=>html.dataset.theme?.includes('dark'),
()=>body&&body.dataset.colorScheme?.includes('dark'),
()=>body&&body.dataset.theme?.includes('dark'),
()=>html.style.colorScheme?.includes('dark'),
()=>getComputedStyle(html).colorScheme?.includes('dark'),
];
const lightMarkers = [
()=>html.classList.contains('light'),
()=>body&&body.classList.contains('light'),
()=>html.dataset.colorScheme?.includes('light'),
()=>html.dataset.theme?.includes('light'),
()=>body&&body.dataset.colorScheme?.includes('light'),
()=>body&&body.dataset.theme?.includes('light'),
()=>html.style.colorScheme?.includes('light'),
()=>getComputedStyle(html).colorScheme?.includes('light'),
];
if (darkMarkers.some(fn=>{try{return fn();}catch{return false;}})) return 'dark';
if (lightMarkers.some(fn=>{try{return fn();}catch{return false;}})) return 'light';
// 2. Fallback: measure actual page background luminance
try {
const bg = getComputedStyle(body||html).backgroundColor;
const m = bg.match(/\d+/g);
if (m && m.length >= 3) {
const lum = 0.299*+m[0] + 0.587*+m[1] + 0.114*+m[2];
if (lum > 10) return lum < 128 ? 'dark' : 'light';
}
} catch {}
// 3. Last resort: OS preference
return window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';
}
function applyTheme(s) {
const eff = s.themeMode==='auto'?detectDuoTheme():s.theme;
panel.classList.toggle('dam-light', eff==='light');
toast.classList.toggle('dam-light', eff==='light');
confirmEl.classList.toggle('dam-light', eff==='light');
editEl.classList.toggle('dam-light', eff==='light');
if (s.transparent) {
const op = Math.round((s.opacity||85)/100*255).toString(16).padStart(2,'0');
panel.style.setProperty('--dam-panel-bg', eff==='light'?`rgba(240,241,245,0.${Math.round((s.opacity||85)*0.72)})`:`rgba(18,19,26,0.${Math.round((s.opacity||85)*0.72)})`);
panel.style.setProperty('--dam-panel-filter', 'blur(24px) saturate(180%)');
} else {
panel.style.setProperty('--dam-panel-bg', eff==='light'?'rgba(240,241,245,0.98)':'rgba(18,19,26,0.98)');
panel.style.setProperty('--dam-panel-filter', 'none');
}
}
function syncSettingsUI(s) {
document.querySelectorAll('.dam-pill').forEach(p=>p.classList.toggle('active',p.dataset.theme===s.themeMode));
document.querySelectorAll('.dam-lang-pill').forEach(p=>p.classList.toggle('active',p.dataset.lang===s.lang));
const tog = document.getElementById('dam-toggle-transparent');
if (tog) tog.checked = !!s.transparent;
const slider = document.getElementById('dam-slider-opacity');
const val = document.getElementById('dam-opacity-val');
if (slider) slider.value = s.opacity||85;
if (val) val.textContent = (s.opacity||85)+'%';
const opRow = document.getElementById('dam-opacity-row');
if (opRow) opRow.classList.toggle('dam-opacity-row-disabled', !s.transparent);
document.getElementById('dam-toggle-transparent')?.addEventListener('change', e=>{
document.getElementById('dam-opacity-row')?.classList.toggle('dam-opacity-row-disabled',!e.target.checked);
});
}
function effectiveTheme() { return settings.themeMode==='auto'?detectDuoTheme():settings.theme; }
const _mq = window.matchMedia('(prefers-color-scheme:dark)');
_mq.addEventListener('change', ()=>{ if(settings.themeMode==='auto') applyTheme(settings); });
new MutationObserver(()=>{ if(settings.themeMode==='auto') applyTheme(settings); })
.observe(document.documentElement,{attributes:true,attributeFilter:['class','data-color-scheme','data-theme','style']});
if(document.body) new MutationObserver(()=>{ if(settings.themeMode==='auto') applyTheme(settings); })
.observe(document.body,{attributes:true,attributeFilter:['class','data-color-scheme','data-theme','style']});
setInterval(()=>{ if(settings.themeMode==='auto') applyTheme(settings); }, 2000);
let pendingSettings = null;
const updateSetting = patch => {
if (!pendingSettings) pendingSettings = Object.assign({},settings);
Object.assign(pendingSettings,patch);
applyThemePreview(pendingSettings);
syncSettingsUI(pendingSettings);
};
function applyThemePreview(s) { const saved=settings; settings=s; applyTheme(s); settings=saved; }
// ─── Avatar palettes ──────────────────────────────────────────────────────────
const AV_BG_DARK = ['#1C3D00','#00304A','#3A1F52','#433200','#4A1515','#4A2900'];
const AV_FG_DARK = ['#58CC02','#1CB0F6','#CE82FF','#FFC800','#FF6B6B','#FF9600'];
const AV_BG_LIGHT = ['#d4f5a8','#b3e8ff','#e8d0ff','#fff0b3','#ffd0d0','#ffe0b3'];
const AV_FG_LIGHT = ['#2a7000','#005f8a','#6b00b3','#7a5c00','#a00000','#8a4500'];
// ─── Render account list ──────────────────────────────────────────────────────
function render(query='') {
const listEl = document.getElementById('dam-list'); if (!listEl) return;
const all = loadAccounts();
const q = query.trim().toLowerCase();
const shown = q ? all.filter(a=>a.label.toLowerCase().includes(q)) : all;
if (!all.length) {
listEl.innerHTML=`<div class="dam-empty"><span class="dam-empty-icon" style="display:flex;justify-content:center;color:var(--dam-text-dim2)">${USER_SVG}</span>${t('emptyNoAccounts')}<br>${t('emptyNoAccountsSub')}</div>`;
return;
}
if (!shown.length) {
listEl.innerHTML=`<div class="dam-empty"><span class="dam-empty-icon">🔍</span>${t('emptyNotFound')}</div>`;
return;
}
listEl.innerHTML='';
shown.forEach(a => {
const realIdx = all.indexOf(a);
const ci = realIdx % 6;
const isCur = isCurrentAccount(a);
const row = document.createElement('div');
row.className = 'dam-acc'+(isCur?' dam-acc-current':'');
const avatarEl = buildAvatarEl(a,ci);
if (isCur) avatarEl.classList.add('dam-avatar-current-ring');
// Info: name + sub row (token snippet + edit button)
const info = document.createElement('div'); info.className='dam-info';
const nameEl = document.createElement('div'); nameEl.className='dam-name'; nameEl.textContent=a.label;
const subRow = document.createElement('div'); subRow.className='dam-sub-row';
const subEl = document.createElement('div'); subEl.className='dam-sub'; subEl.textContent='⚡ '+a.token.slice(0,18)+'…';
// Edit button — next to the token snippet
const btnEdit = document.createElement('button');
btnEdit.className='dam-btn-edit-inline';
btnEdit.title=t('btnEdit');
btnEdit.innerHTML=EDIT_SVG;
btnEdit.addEventListener('click', e=>{e.stopPropagation();openEdit(realIdx);});
subRow.append(subEl, btnEdit);
info.append(nameEl, subRow);
// Delete button
const btnDel = document.createElement('button');
btnDel.className='dam-btn-del';
btnDel.title=t('btnDelete');
btnDel.innerHTML='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:11px;height:11px;display:block"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/></svg>';
btnDel.addEventListener('click', e=>{e.stopPropagation();deleteAcc(realIdx);});
// Login / Current badge — far right
let actionEl;
if (isCur) {
actionEl = document.createElement('div');
actionEl.className='dam-badge-current';
actionEl.textContent=t('badgeCurrent');
} else {
actionEl = document.createElement('button');
actionEl.className='dam-btn-login';
actionEl.textContent=t('btnLogin');
actionEl.addEventListener('click', e=>{e.stopPropagation();openConfirm(realIdx);});
}
// Order: avatar | info (name + token+editBtn) | delete | login
row.append(avatarEl, info, btnDel, actionEl);
listEl.appendChild(row);
});
}
// ─── Account actions ──────────────────────────────────────────────────────────
function addAccount() {
const label = document.getElementById('dam-inp-label').value.trim();
const token = document.getElementById('dam-inp-token').value.trim();
if (!label) return showToast(t('toastNoLabel'));
if (!token) return showToast(t('toastNoToken'));
if (!token.startsWith('eyJhb')) return showToast(t('toastBadFormat'));
const list = loadAccounts();
if (list.find(a=>a.label===label)) return showToast(t('toastDuplicateLabel'));
list.push({label,token});
saveAccounts(list);
document.getElementById('dam-inp-label').value='';
document.getElementById('dam-inp-token').value='';
document.getElementById('dam-inp-token').type='password';
document.getElementById('dam-btn-eye').innerHTML=EYE_SVG;
document.getElementById('dam-btn-clear-token').style.display='none';
showToast(t('toastSaved',label));
switchTab('accounts');
render();
}
function deleteAcc(i) {
const list=loadAccounts();
const name=list[i]?.label||'';
list.splice(i,1);
saveAccounts(list);
render(document.getElementById('dam-search')?.value||'');
showToast(t('toastDeleted',name));
}
// ─── Confirm login modal ──────────────────────────────────────────────────────
let pendingToken=null;
function openConfirm(i) {
const a=loadAccounts()[i]; if (!a) return;
pendingToken=a.token;
const msgEl=document.getElementById('dam-confirm-msg');
const ci=i%6, isLight=effectiveTheme()==='light';
msgEl.innerHTML='';
const avWrap=document.createElement('div');
avWrap.className='dam-confirm-avatar';
avWrap.style.background=(isLight?AV_BG_LIGHT:AV_BG_DARK)[ci];
avWrap.style.color=(isLight?AV_FG_LIGHT:AV_FG_DARK)[ci];
avWrap.textContent=a.label.slice(0,2).toUpperCase();
const txt=document.createElement('span');
txt.innerHTML=t('confirmMsg',escHtml(a.label));
msgEl.append(avWrap,txt);
confirmEl.classList.remove('hidden');
}
// ─── Edit modal ───────────────────────────────────────────────────────────────
let editingIndex=-1;
function openEdit(i) {
const list=loadAccounts(), a=list[i]; if (!a) return;
editingIndex=i;
document.getElementById('dam-edit-label').value=a.label;
const tokenInp=document.getElementById('dam-edit-token');
tokenInp.value=a.token; tokenInp.type='password';
document.getElementById('dam-edit-eye').innerHTML=EYE_SVG;
const prev=document.getElementById('dam-token-preview');
if (prev) prev.textContent=a.token||'—';
editEl.classList.toggle('dam-light',effectiveTheme()==='light');
editEl.classList.remove('hidden');
}
function closeEdit() { editEl.classList.add('hidden'); editingIndex=-1; }
function saveEdit() {
const label=document.getElementById('dam-edit-label').value.trim();
const token=document.getElementById('dam-edit-token').value.trim();
if (!label) return showToast(t('toastNoLabel'));
if (!token) return showToast(t('toastNoToken'));
if (!token.startsWith('eyJhb')) return showToast(t('toastBadFormat'));
const list=loadAccounts();
if (list.findIndex((a,i)=>a.label===label&&i!==editingIndex)!==-1) return showToast(t('toastDuplicateLabel'));
list[editingIndex]={label,token};
saveAccounts(list);
closeEdit();
showToast(t('toastSaved',label));
render(document.getElementById('dam-search')?.value||'');
}
// ─── Draggable FAB ────────────────────────────────────────────────────────────
(function makeFabDraggable() {
let dragging=false,moved=false,startX=0,startY=0,origLeft=0,origTop=0;
fab.addEventListener('pointerdown',e=>{
dragging=true; moved=false; startX=e.clientX; startY=e.clientY;
const r=fab.getBoundingClientRect(); origLeft=r.left; origTop=r.top;
fab.setPointerCapture(e.pointerId); fab.classList.add('dam-fab-pressed');
});
fab.addEventListener('pointermove',e=>{
if (!dragging) return;
const dx=e.clientX-startX,dy=e.clientY-startY;
if (!moved&&Math.abs(dx)<4&&Math.abs(dy)<4) return;
if (!moved){fab.classList.remove('dam-fab-pressed');fab.style.transition='none';}
moved=true; fab.classList.add('dam-fab-dragging');
fab.style.right='auto'; fab.style.bottom='auto';
fab.style.left=Math.max(8,Math.min(origLeft+dx,window.innerWidth-fab.offsetWidth-8))+'px';
fab.style.top=Math.max(8,Math.min(origTop+dy,window.innerHeight-fab.offsetHeight-8))+'px';
});
fab.addEventListener('pointerup',()=>{
if (!dragging) return; dragging=false;
fab.classList.remove('dam-fab-dragging','dam-fab-pressed'); fab.style.transition='';
if (!moved){
panel.classList.toggle('hidden');
if (!panel.classList.contains('hidden')){applyTheme(settings);render(document.getElementById('dam-search')?.value||'');}
}
});
fab.addEventListener('pointercancel',()=>{
dragging=false; fab.classList.remove('dam-fab-dragging','dam-fab-pressed'); fab.style.transition='';
});
})();
// ─── Toast ────────────────────────────────────────────────────────────────────
let toastTimer;
function showToast(msg) {
toast.textContent=msg; toast.classList.add('show');
clearTimeout(toastTimer);
toastTimer=setTimeout(()=>toast.classList.remove('show'),2500);
}
buildUI();
// ─── Detect failed/successful login on page load ──────────────────────────────
(function checkPendingLogin() {
const pending = GM_getValue(PENDING_LOGIN_KEY, '');
if (!pending) return;
GM_setValue(PENDING_LOGIN_KEY, '');
const path = location.pathname;
const search = location.search;
const failed = path.startsWith('/login') || search.includes('loginRedirect') || path === '/';
if (failed) {
setTimeout(() => {
panel.classList.remove('hidden');
applyTheme(settings);
render();
showToast(t('toastTokenDetected'));
}, 600);
} else {
// Login redirect landed on /learn — identify which account is now active
setTimeout(() => {
updateFab();
const acc = detectCurrentAccount();
if (acc) showToast(t('toastLoginSuccess', acc.label));
render();
}, 800);
}
})();
})();