feat: add dedicated account settings page
This commit is contained in:
+11
-8
@@ -875,14 +875,17 @@ async function main() {
|
||||
|
||||
// ── Settings dialog ──
|
||||
const settingsDialog = document.getElementById('settings-dialog');
|
||||
document.getElementById('open-settings-btn').addEventListener('click', () => {
|
||||
// Update auth section for active account
|
||||
if (state.activeAccount) {
|
||||
updateAuthSection(state.activeAccount);
|
||||
}
|
||||
renderSettingsAccounts();
|
||||
settingsDialog.showModal();
|
||||
});
|
||||
const openSettingsBtn = document.getElementById('open-settings-btn');
|
||||
if (!openSettingsBtn.matches('a[href]')) {
|
||||
openSettingsBtn.addEventListener('click', () => {
|
||||
// Update auth section for active account
|
||||
if (state.activeAccount) {
|
||||
updateAuthSection(state.activeAccount);
|
||||
}
|
||||
renderSettingsAccounts();
|
||||
settingsDialog.showModal();
|
||||
});
|
||||
}
|
||||
document.getElementById('close-settings-btn').addEventListener('click', () => {
|
||||
settingsDialog.close();
|
||||
});
|
||||
|
||||
+2
-1
@@ -17,6 +17,7 @@
|
||||
|
||||
<nav class="nav-links">
|
||||
<a class="nav-link active" href="/">Dashboard</a>
|
||||
<a class="nav-link" href="/settings">Settings</a>
|
||||
<a class="nav-link" href="/viewer">Message Viewer</a>
|
||||
<a class="nav-link" href="/swagger">API Docs</a>
|
||||
<a class="nav-link" href="/health">Health</a>
|
||||
@@ -33,7 +34,7 @@
|
||||
<button class="button primary" id="scrape-all-btn">Scrape all channels</button>
|
||||
<button class="button" id="export-all-btn">Export all data</button>
|
||||
<button class="button" id="refresh-dialogs-btn">Refresh dialogs</button>
|
||||
<button class="button" id="open-settings-btn" type="button">Settings</button>
|
||||
<a class="button" id="open-settings-btn" href="/settings">Settings</a>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Telegram Scraper Settings</title>
|
||||
<link rel="stylesheet" href="/static/style.css?v=5" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell settings-page-shell">
|
||||
<aside class="sidebar">
|
||||
<div class="brand-block">
|
||||
<div class="eyebrow">Telegram Scraper</div>
|
||||
<h1>Settings</h1>
|
||||
<p class="muted">Account credentials, import/export, and runtime safety controls.</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-links">
|
||||
<a class="nav-link" href="/">Dashboard</a>
|
||||
<a class="nav-link active" href="/settings">Settings</a>
|
||||
<a class="nav-link" href="/viewer">Message Viewer</a>
|
||||
<a class="nav-link" href="/swagger">API Docs</a>
|
||||
<a class="nav-link" href="/health">Health</a>
|
||||
</nav>
|
||||
|
||||
<section class="status-card">
|
||||
<div class="section-title">Accounts</div>
|
||||
<div id="settings-account-list" class="settings-account-list"></div>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<main class="content settings-page">
|
||||
<section class="panel settings-hero">
|
||||
<div>
|
||||
<div class="eyebrow">Account Settings</div>
|
||||
<h2 id="settings-title">Loading accounts</h2>
|
||||
<p id="settings-subtitle" class="muted">Select an account to manage credentials and scraper options.</p>
|
||||
</div>
|
||||
<div class="action-row">
|
||||
<button class="button" id="settings-export-btn" type="button" disabled>Export</button>
|
||||
<button class="button danger" id="settings-delete-btn" type="button" disabled>Delete</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="settings-empty" class="panel hidden">
|
||||
<div class="empty-state">
|
||||
<h2>Add first account</h2>
|
||||
<p class="muted">Create an account below or import an exported account JSON.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel-grid settings-health-grid">
|
||||
<div class="panel stat-panel">
|
||||
<div class="section-title">Credentials</div>
|
||||
<div class="stat-value stat-compact" id="health-credentials">-</div>
|
||||
</div>
|
||||
<div class="panel stat-panel">
|
||||
<div class="section-title">Session</div>
|
||||
<div class="stat-value stat-compact" id="health-session">-</div>
|
||||
</div>
|
||||
<div class="panel stat-panel">
|
||||
<div class="section-title">Continuous</div>
|
||||
<div class="stat-value stat-compact" id="health-continuous">-</div>
|
||||
</div>
|
||||
<div class="panel stat-panel">
|
||||
<div class="section-title">Last scrape</div>
|
||||
<div class="stat-value stat-compact" id="health-last-scrape">-</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-layout">
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2>Create or Import</h2>
|
||||
<p class="muted">New accounts start without continuous scraping enabled.</p>
|
||||
</div>
|
||||
</div>
|
||||
<form id="settings-add-account-form" class="stack-form">
|
||||
<input id="settings-add-account-id" name="account_id" placeholder="Account ID (e.g. work)" required />
|
||||
<input id="settings-add-account-label" name="label" placeholder="Display name" />
|
||||
<input id="settings-add-account-api-id" name="api_id" placeholder="API ID" />
|
||||
<input id="settings-add-account-api-hash" name="api_hash" placeholder="API Hash" />
|
||||
<button class="button primary" type="submit">Add Account</button>
|
||||
</form>
|
||||
<label class="import-account-row">
|
||||
<span class="muted small">Import account settings JSON</span>
|
||||
<input id="settings-import-account-file" type="file" accept="application/json,.json" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2>Credentials & Auth</h2>
|
||||
<p class="muted" id="settings-auth-label">No account selected.</p>
|
||||
</div>
|
||||
</div>
|
||||
<form id="settings-credentials-form" class="stack-form">
|
||||
<input id="settings-api-id-input" name="api_id" placeholder="API ID" />
|
||||
<input id="settings-api-hash-input" name="api_hash" placeholder="API Hash" />
|
||||
<button class="button" type="submit">Save credentials</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-actions">
|
||||
<button class="button primary" id="settings-start-qr-btn" type="button">Start QR login</button>
|
||||
</div>
|
||||
|
||||
<div id="settings-qr-wrap" class="qr-wrap hidden">
|
||||
<img id="settings-qr-image" alt="Telegram QR login" />
|
||||
<p class="muted small">Open Telegram -> Settings -> Devices -> Scan QR.</p>
|
||||
</div>
|
||||
|
||||
<form id="settings-phone-form" class="stack-form">
|
||||
<input id="settings-phone-input" name="phone" placeholder="+1234567890" />
|
||||
<button class="button" type="submit">Send code</button>
|
||||
</form>
|
||||
|
||||
<form id="settings-code-form" class="stack-form hidden">
|
||||
<input id="settings-code-input" name="code" placeholder="Telegram code" />
|
||||
<button class="button" type="submit">Confirm code</button>
|
||||
</form>
|
||||
|
||||
<form id="settings-password-form" class="stack-form hidden">
|
||||
<input id="settings-password-input" name="password" type="password" placeholder="2FA password" />
|
||||
<button class="button" type="submit">Confirm password</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2>Scraping</h2>
|
||||
<p class="muted">Account-level parser settings.</p>
|
||||
</div>
|
||||
</div>
|
||||
<label class="toggle-row">
|
||||
<span>Download media</span>
|
||||
<span class="switch">
|
||||
<input id="settings-scrape-media-toggle" type="checkbox" />
|
||||
<span class="switch-slider"></span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="settings-meta-list">
|
||||
<div>Tracked channels: <span id="settings-channel-count">0</span></div>
|
||||
<div>Messages: <span id="settings-message-count">0</span></div>
|
||||
<div>Media: <span id="settings-media-count">0</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/static/settings.js?v=1"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,424 @@
|
||||
const settingsState = {
|
||||
accounts: [],
|
||||
activeAccount: null,
|
||||
dashboard: null,
|
||||
auth: null,
|
||||
continuous: null,
|
||||
};
|
||||
|
||||
const ACTIVE_ACCOUNT_STORAGE_KEY = 'telegramScraper.activeAccount';
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const response = await fetch(path, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
...options,
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Request failed');
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
let root = document.getElementById('toast-root');
|
||||
if (!root) {
|
||||
root = document.createElement('div');
|
||||
root.id = 'toast-root';
|
||||
root.className = 'toast-root';
|
||||
document.body.appendChild(root);
|
||||
}
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.textContent = message;
|
||||
root.appendChild(toast);
|
||||
window.setTimeout(() => {
|
||||
toast.classList.add('toast-hide');
|
||||
window.setTimeout(() => toast.remove(), 250);
|
||||
}, 3600);
|
||||
}
|
||||
|
||||
function downloadJson(filename, payload) {
|
||||
const blob = new Blob([JSON.stringify(payload, null, 2) + '\n'], {
|
||||
type: 'application/json',
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function displayTime(value) {
|
||||
if (!value) return '-';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return String(value).replace('T', ' ').replace('+00:00', ' UTC');
|
||||
return date.toLocaleString(undefined, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function isAccountAuthorized(auth) {
|
||||
const status = auth?.auth_status || auth || {};
|
||||
return (
|
||||
auth?.phase === 'authorized' ||
|
||||
auth?.status === 'authorized' ||
|
||||
status.status === 'ready' ||
|
||||
status.status === 'authorized'
|
||||
);
|
||||
}
|
||||
|
||||
function accountLabel(account) {
|
||||
return account?.label || account?.id || 'No account';
|
||||
}
|
||||
|
||||
function setActiveAccount(accountId) {
|
||||
settingsState.activeAccount = accountId || null;
|
||||
if (accountId) {
|
||||
localStorage.setItem(ACTIVE_ACCOUNT_STORAGE_KEY, accountId);
|
||||
} else {
|
||||
localStorage.removeItem(ACTIVE_ACCOUNT_STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
function chooseInitialAccount() {
|
||||
const requested = new URLSearchParams(window.location.search).get('account');
|
||||
if (requested && settingsState.accounts.some((account) => account.id === requested)) {
|
||||
return requested;
|
||||
}
|
||||
const saved = localStorage.getItem(ACTIVE_ACCOUNT_STORAGE_KEY);
|
||||
if (saved && settingsState.accounts.some((account) => account.id === saved)) {
|
||||
return saved;
|
||||
}
|
||||
return settingsState.accounts[0]?.id || null;
|
||||
}
|
||||
|
||||
function renderAccountList() {
|
||||
const root = document.getElementById('settings-account-list');
|
||||
root.innerHTML = '';
|
||||
|
||||
if (!settingsState.accounts.length) {
|
||||
root.innerHTML = '<div class="empty-state">No accounts yet.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
settingsState.accounts.forEach((account) => {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'settings-account-button';
|
||||
button.type = 'button';
|
||||
button.classList.toggle('active', account.id === settingsState.activeAccount);
|
||||
|
||||
const name = document.createElement('span');
|
||||
name.className = 'settings-account-name';
|
||||
name.textContent = accountLabel(account);
|
||||
|
||||
const status = document.createElement('span');
|
||||
status.className = isAccountAuthorized(account.auth) ? 'settings-account-status ok' : 'settings-account-status';
|
||||
status.textContent = isAccountAuthorized(account.auth) ? 'Authorized' : 'Needs auth';
|
||||
|
||||
button.append(name, status);
|
||||
button.addEventListener('click', () => loadAccount(account.id));
|
||||
root.appendChild(button);
|
||||
});
|
||||
}
|
||||
|
||||
function renderEmptyState() {
|
||||
const hasAccount = Boolean(settingsState.activeAccount);
|
||||
document.getElementById('settings-empty').classList.toggle('hidden', hasAccount);
|
||||
document.getElementById('settings-export-btn').disabled = !hasAccount;
|
||||
document.getElementById('settings-delete-btn').disabled = !hasAccount;
|
||||
document.getElementById('settings-credentials-form').querySelectorAll('input, button').forEach((node) => {
|
||||
node.disabled = !hasAccount;
|
||||
});
|
||||
document.getElementById('settings-start-qr-btn').disabled = !hasAccount;
|
||||
document.getElementById('settings-phone-form').querySelectorAll('input, button').forEach((node) => {
|
||||
node.disabled = !hasAccount;
|
||||
});
|
||||
document.getElementById('settings-scrape-media-toggle').disabled = !hasAccount;
|
||||
}
|
||||
|
||||
function renderAccountData() {
|
||||
const account = settingsState.accounts.find((item) => item.id === settingsState.activeAccount);
|
||||
const dashboard = settingsState.dashboard || {};
|
||||
const health = dashboard.health || {};
|
||||
const auth = settingsState.auth || account?.auth || {};
|
||||
const continuous = settingsState.continuous || {};
|
||||
const continuousStatus = continuous.status || {};
|
||||
const continuousConfig = continuous.config || {};
|
||||
|
||||
document.getElementById('settings-title').textContent = account ? accountLabel(account) : 'No account selected';
|
||||
document.getElementById('settings-subtitle').textContent = account
|
||||
? `Account ID: ${account.id}`
|
||||
: 'Create or import an account to manage settings.';
|
||||
document.getElementById('settings-auth-label').textContent = account
|
||||
? `${accountLabel(account)} auth status`
|
||||
: 'No account selected.';
|
||||
|
||||
document.getElementById('health-credentials').textContent = health.api_credentials ? 'Saved' : 'Missing';
|
||||
document.getElementById('health-credentials').style.color = health.api_credentials ? 'var(--ok)' : 'var(--warn)';
|
||||
document.getElementById('health-session').textContent = health.session_ready || isAccountAuthorized(auth) ? 'Ready' : 'Missing';
|
||||
document.getElementById('health-session').style.color =
|
||||
health.session_ready || isAccountAuthorized(auth) ? 'var(--ok)' : 'var(--warn)';
|
||||
document.getElementById('health-continuous').textContent = continuousStatus.running
|
||||
? 'Running'
|
||||
: continuousConfig.enabled
|
||||
? 'Enabled'
|
||||
: 'Stopped';
|
||||
document.getElementById('health-continuous').style.color = continuousStatus.running ? 'var(--ok)' : 'var(--dim)';
|
||||
document.getElementById('health-last-scrape').textContent = displayTime(
|
||||
health.last_scrape || continuousStatus.last_iteration_at,
|
||||
);
|
||||
|
||||
document.getElementById('settings-scrape-media-toggle').checked = Boolean(dashboard.dashboard?.scrape_media);
|
||||
document.getElementById('settings-channel-count').textContent = String(health.channel_count || 0);
|
||||
document.getElementById('settings-message-count').textContent = String(health.message_count || 0);
|
||||
document.getElementById('settings-media-count').textContent = String(health.media_count || 0);
|
||||
renderEmptyState();
|
||||
renderAccountList();
|
||||
}
|
||||
|
||||
async function loadAccounts() {
|
||||
const payload = await api('/api/accounts');
|
||||
settingsState.accounts = payload.accounts || [];
|
||||
setActiveAccount(chooseInitialAccount());
|
||||
renderAccountList();
|
||||
if (settingsState.activeAccount) {
|
||||
await loadAccount(settingsState.activeAccount, { preserveUrl: true });
|
||||
} else {
|
||||
settingsState.dashboard = null;
|
||||
settingsState.auth = null;
|
||||
settingsState.continuous = null;
|
||||
renderAccountData();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAccount(accountId, options = {}) {
|
||||
setActiveAccount(accountId);
|
||||
const [dashboard, auth, continuous] = await Promise.all([
|
||||
api(`/api/accounts/${encodeURIComponent(accountId)}`),
|
||||
api(`/api/accounts/${encodeURIComponent(accountId)}/auth`).catch(() => ({})),
|
||||
api(`/api/accounts/${encodeURIComponent(accountId)}/continuous`).catch(() => ({})),
|
||||
]);
|
||||
settingsState.dashboard = dashboard;
|
||||
settingsState.auth = auth;
|
||||
settingsState.continuous = continuous;
|
||||
if (!options.preserveUrl) {
|
||||
history.replaceState(null, '', `/settings?account=${encodeURIComponent(accountId)}`);
|
||||
}
|
||||
renderAccountData();
|
||||
}
|
||||
|
||||
async function createAccount(event) {
|
||||
event.preventDefault();
|
||||
const accountId = document.getElementById('settings-add-account-id').value.trim();
|
||||
const label = document.getElementById('settings-add-account-label').value.trim();
|
||||
const apiId = document.getElementById('settings-add-account-api-id').value.trim();
|
||||
const apiHash = document.getElementById('settings-add-account-api-hash').value.trim();
|
||||
if (!accountId) return;
|
||||
|
||||
await api('/api/accounts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
account_id: accountId,
|
||||
label: label || accountId,
|
||||
api_id: apiId ? parseInt(apiId) : undefined,
|
||||
api_hash: apiHash || undefined,
|
||||
}),
|
||||
});
|
||||
event.currentTarget.reset();
|
||||
await loadAccounts();
|
||||
await loadAccount(accountId);
|
||||
showToast(`Added ${accountId}.`, 'success');
|
||||
}
|
||||
|
||||
async function importAccount(event) {
|
||||
const file = event.currentTarget.files?.[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const payload = JSON.parse(await file.text());
|
||||
const accountId = payload.account_id || payload.id;
|
||||
if (!accountId) throw new Error('account_id is missing in import file');
|
||||
await api('/api/accounts/import', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
event.currentTarget.value = '';
|
||||
await loadAccounts();
|
||||
await loadAccount(accountId);
|
||||
showToast(`Imported ${accountId}.`, 'success');
|
||||
} catch (err) {
|
||||
showToast(`Failed to import account: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCredentials(event) {
|
||||
event.preventDefault();
|
||||
if (!settingsState.activeAccount) return;
|
||||
const apiId = document.getElementById('settings-api-id-input').value.trim();
|
||||
const apiHash = document.getElementById('settings-api-hash-input').value.trim();
|
||||
if (!apiId || !apiHash) return;
|
||||
await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}/auth/credentials`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ api_id: apiId, api_hash: apiHash }),
|
||||
});
|
||||
await loadAccount(settingsState.activeAccount);
|
||||
showToast('Credentials saved.', 'success');
|
||||
}
|
||||
|
||||
async function startQrLogin() {
|
||||
if (!settingsState.activeAccount) return;
|
||||
const data = await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}/auth/qr/start`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
if (data.qr_image) {
|
||||
document.getElementById('settings-qr-image').src = data.qr_image;
|
||||
document.getElementById('settings-qr-wrap').classList.remove('hidden');
|
||||
}
|
||||
await loadAccount(settingsState.activeAccount);
|
||||
}
|
||||
|
||||
async function requestPhoneCode(event) {
|
||||
event.preventDefault();
|
||||
if (!settingsState.activeAccount) return;
|
||||
const phone = document.getElementById('settings-phone-input').value.trim();
|
||||
if (!phone) return;
|
||||
await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}/auth/phone/request`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ phone }),
|
||||
});
|
||||
document.getElementById('settings-code-form').classList.remove('hidden');
|
||||
await loadAccount(settingsState.activeAccount);
|
||||
}
|
||||
|
||||
async function submitPhoneCode(event) {
|
||||
event.preventDefault();
|
||||
if (!settingsState.activeAccount) return;
|
||||
const code = document.getElementById('settings-code-input').value.trim();
|
||||
if (!code) return;
|
||||
await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}/auth/phone/submit`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ code }),
|
||||
});
|
||||
document.getElementById('settings-code-input').value = '';
|
||||
await loadAccount(settingsState.activeAccount);
|
||||
}
|
||||
|
||||
async function submitPassword(event) {
|
||||
event.preventDefault();
|
||||
if (!settingsState.activeAccount) return;
|
||||
const password = document.getElementById('settings-password-input').value.trim();
|
||||
if (!password) return;
|
||||
await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}/auth/password`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ password }),
|
||||
});
|
||||
document.getElementById('settings-password-input').value = '';
|
||||
await loadAccount(settingsState.activeAccount);
|
||||
}
|
||||
|
||||
async function exportAccount() {
|
||||
if (!settingsState.activeAccount) return;
|
||||
const payload = await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}/export`);
|
||||
downloadJson(`telegram-scraper-account-${settingsState.activeAccount}.json`, payload);
|
||||
showToast('Account exported.', 'success');
|
||||
}
|
||||
|
||||
async function deleteAccount() {
|
||||
if (!settingsState.activeAccount) return;
|
||||
const account = settingsState.accounts.find((item) => item.id === settingsState.activeAccount);
|
||||
if (!window.confirm(`Remove account "${accountLabel(account)}"? All its account data will be deleted.`)) return;
|
||||
await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}`, { method: 'DELETE' });
|
||||
setActiveAccount(null);
|
||||
await loadAccounts();
|
||||
showToast('Account deleted.', 'success');
|
||||
}
|
||||
|
||||
async function toggleMedia(event) {
|
||||
if (!settingsState.activeAccount) return;
|
||||
await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}/settings/media`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ value: event.currentTarget.checked }),
|
||||
});
|
||||
await loadAccount(settingsState.activeAccount);
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
document.getElementById('settings-add-account-form').addEventListener('submit', async (event) => {
|
||||
try {
|
||||
await createAccount(event);
|
||||
} catch (err) {
|
||||
showToast(`Failed to add account: ${err.message}`, 'error');
|
||||
}
|
||||
});
|
||||
document.getElementById('settings-import-account-file').addEventListener('change', importAccount);
|
||||
document.getElementById('settings-credentials-form').addEventListener('submit', async (event) => {
|
||||
try {
|
||||
await saveCredentials(event);
|
||||
} catch (err) {
|
||||
showToast(`Failed to save credentials: ${err.message}`, 'error');
|
||||
}
|
||||
});
|
||||
document.getElementById('settings-start-qr-btn').addEventListener('click', async () => {
|
||||
try {
|
||||
await startQrLogin();
|
||||
} catch (err) {
|
||||
showToast(`QR login failed: ${err.message}`, 'error');
|
||||
}
|
||||
});
|
||||
document.getElementById('settings-phone-form').addEventListener('submit', async (event) => {
|
||||
try {
|
||||
await requestPhoneCode(event);
|
||||
} catch (err) {
|
||||
showToast(`Phone login failed: ${err.message}`, 'error');
|
||||
}
|
||||
});
|
||||
document.getElementById('settings-code-form').addEventListener('submit', async (event) => {
|
||||
try {
|
||||
await submitPhoneCode(event);
|
||||
} catch (err) {
|
||||
showToast(`Code submit failed: ${err.message}`, 'error');
|
||||
}
|
||||
});
|
||||
document.getElementById('settings-password-form').addEventListener('submit', async (event) => {
|
||||
try {
|
||||
await submitPassword(event);
|
||||
} catch (err) {
|
||||
showToast(`Password submit failed: ${err.message}`, 'error');
|
||||
}
|
||||
});
|
||||
document.getElementById('settings-export-btn').addEventListener('click', async () => {
|
||||
try {
|
||||
await exportAccount();
|
||||
} catch (err) {
|
||||
showToast(`Export failed: ${err.message}`, 'error');
|
||||
}
|
||||
});
|
||||
document.getElementById('settings-delete-btn').addEventListener('click', async () => {
|
||||
try {
|
||||
await deleteAccount();
|
||||
} catch (err) {
|
||||
showToast(`Delete failed: ${err.message}`, 'error');
|
||||
}
|
||||
});
|
||||
document.getElementById('settings-scrape-media-toggle').addEventListener('change', async (event) => {
|
||||
try {
|
||||
await toggleMedia(event);
|
||||
} catch (err) {
|
||||
showToast(`Media setting failed: ${err.message}`, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bindEvents();
|
||||
loadAccounts().catch((err) => {
|
||||
console.error(err);
|
||||
showToast(`Failed to load settings: ${err.message}`, 'error');
|
||||
});
|
||||
@@ -194,6 +194,10 @@ input,
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.status-card .button:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
padding: 6px 10px;
|
||||
@@ -224,6 +228,10 @@ input,
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.settings-health-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@@ -260,6 +268,78 @@ input,
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.settings-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 0.9fr) minmax(320px, 1.1fr);
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.settings-layout .panel:last-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.settings-page-shell .content {
|
||||
max-width: 1280px;
|
||||
}
|
||||
|
||||
.settings-hero {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.settings-account-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.settings-account-button {
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
background: #101010;
|
||||
color: var(--text-color);
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settings-account-button.active {
|
||||
border-color: #0f6f67;
|
||||
background: #092522;
|
||||
}
|
||||
|
||||
.settings-account-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.settings-account-status {
|
||||
color: var(--warn);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.settings-account-status.ok {
|
||||
color: var(--ok);
|
||||
}
|
||||
|
||||
.settings-meta-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
color: var(--dim);
|
||||
}
|
||||
|
||||
input {
|
||||
min-width: 160px;
|
||||
padding: 10px 13px;
|
||||
|
||||
Reference in New Issue
Block a user