diff --git a/package-lock.json b/package-lock.json index 00d27fd9ee..1528872f92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "supercluster": "^8.0.1", "telegram": "^2.26.22", "topojson-client": "^3.1.0", + "uqr": "^0.1.2", "ws": "^8.19.0", "youtubei.js": "^16.0.1" }, @@ -27327,8 +27328,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.2.tgz", "integrity": "sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/use-sync-external-store": { "version": "1.6.0", diff --git a/package.json b/package.json index 52a5d2508a..2f41e3cc83 100644 --- a/package.json +++ b/package.json @@ -110,9 +110,9 @@ "fast-xml-parser": "^5.3.7", "globe.gl": "^2.45.0", "hls.js": "^1.6.15", - "jose": "^6.0.11", "i18next": "^25.8.10", "i18next-browser-languagedetector": "^8.2.1", + "jose": "^6.0.11", "maplibre-gl": "^5.16.0", "marked": "^17.0.3", "onnxruntime-web": "^1.23.2", @@ -123,6 +123,7 @@ "supercluster": "^8.0.1", "telegram": "^2.26.22", "topojson-client": "^3.1.0", + "uqr": "^0.1.2", "ws": "^8.19.0", "youtubei.js": "^16.0.1" }, diff --git a/src/services/preferences-content.ts b/src/services/preferences-content.ts index b89e4c7e77..4f9eed21ca 100644 --- a/src/services/preferences-content.ts +++ b/src/services/preferences-content.ts @@ -7,6 +7,7 @@ import type { StreamQuality } from '@/services/ai-flow-settings'; import { getThemePreference, setThemePreference, type ThemePreference } from '@/utils/theme-manager'; import { getFontFamily, setFontFamily, type FontFamily } from '@/services/font-settings'; import { escapeHtml } from '@/utils/sanitize'; +import { renderSVG } from 'uqr'; import { trackLanguageChange } from '@/services/analytics'; import { exportSettings, importSettings, type ImportResult } from '@/utils/settings-persistence'; import { @@ -808,23 +809,57 @@ export function renderPreferences(host: PreferencesHost): PreferencesResult { container.addEventListener('click', (e) => { const target = e.target as HTMLElement; - if (target.closest('#usConnectTelegram')) { - const rowEl = target.closest('.us-notif-ch-row') as HTMLElement | null; - if (!rowEl) return; + if (target.closest('.us-notif-tg-copy-btn')) { + const btn = target.closest('.us-notif-tg-copy-btn') as HTMLButtonElement; + const cmd = btn.dataset.cmd ?? ''; + const markCopied = () => { + btn.textContent = 'Copied!'; + setTimeout(() => { btn.textContent = 'Copy'; }, 2000); + }; + const execFallback = () => { + const ta = document.createElement('textarea'); + ta.value = cmd; + ta.style.cssText = 'position:fixed;opacity:0;pointer-events:none'; + document.body.appendChild(ta); + ta.select(); + try { document.execCommand('copy'); markCopied(); } catch { /* ignore */ } + document.body.removeChild(ta); + }; + if (navigator.clipboard?.writeText) { + navigator.clipboard.writeText(cmd).then(markCopied).catch(execFallback); + } else { + execFallback(); + } + return; + } + + const startTelegramPairing = (rowEl: HTMLElement) => { + rowEl.innerHTML = `
${channelIcon('telegram')}
Telegram
Generating code…
`; createPairingToken().then(({ token, expiresAt }) => { if (signal.aborted) return; const botUsername = (typeof import.meta !== 'undefined' && import.meta.env?.VITE_TELEGRAM_BOT_USERNAME as string | undefined) ?? 'WorldMonitorBot'; - const deepLink = `https://t.me/${botUsername}?start=${token}`; + const deepLink = `https://t.me/${String(botUsername)}?start=${token}`; + const startCmd = `/start ${token}`; const secsLeft = Math.max(0, Math.floor((expiresAt - Date.now()) / 1000)); + const qrSvg = renderSVG(deepLink, { ecc: 'M', border: 1 }); rowEl.innerHTML = `
${channelIcon('telegram')}
-
Telegram
-
Waiting for pairing...
+
Connect Telegram
+
Open the bot. If Telegram doesn't send the code automatically, paste this command.
+
+
+ Open Telegram +
+ ${escapeHtml(startCmd)} + +
+
+
${qrSvg}
+
- Open Telegram - ${secsLeft}s + Waiting… ${secsLeft}s
`; let remaining = secsLeft; @@ -833,20 +868,39 @@ export function renderPreferences(host: PreferencesHost): PreferencesResult { if (signal.aborted) { clearNotifPoll(); return; } remaining -= 3; const countdownEl = container.querySelector('#usTgCountdown'); - if (countdownEl) countdownEl.textContent = `${Math.max(0, remaining)}s`; + if (countdownEl) countdownEl.textContent = `Waiting… ${Math.max(0, remaining)}s`; const expired = remaining <= 0; - if (expired) clearNotifPoll(); + if (expired) { + clearNotifPoll(); + rowEl.innerHTML = ` +
${channelIcon('telegram')}
+
+
Telegram
+
Code expired
+
+
+ +
+ `; + return; + } getChannelsData().then((data) => { const tg = data.channels.find(c => c.channelType === 'telegram'); - if (tg?.verified || expired) { - if (tg?.verified) saveRuleWithNewChannel('telegram'); + if (tg?.verified) { + saveRuleWithNewChannel('telegram'); reloadNotifSection(); } - }).catch(() => { - if (expired) reloadNotifSection(); - }); + }).catch(() => {}); }, 3000); - }).catch(() => {}); + }).catch(() => { + rowEl.innerHTML = `
${channelIcon('telegram')}
Telegram
Failed to generate code
`; + }); + }; + + if (target.closest('#usConnectTelegram') || target.closest('.us-notif-tg-regen')) { + const rowEl = target.closest('.us-notif-ch-row') as HTMLElement | null; + if (!rowEl) return; + startTelegramPairing(rowEl); return; } diff --git a/src/styles/settings-window.css b/src/styles/settings-window.css index 9fc8726432..4b36d0d2e6 100644 --- a/src/styles/settings-window.css +++ b/src/styles/settings-window.css @@ -1255,9 +1255,97 @@ tr.diag-err td { color: var(--settings-red); } background: color-mix(in srgb, var(--settings-accent) 85%, white); } +.us-notif-tg-inline-link { + color: var(--settings-accent); + text-decoration: none; +} + +.us-notif-tg-inline-link:hover { + text-decoration: underline; +} + +.us-notif-tg-pair-layout { + display: flex; + align-items: flex-start; + gap: 10px; + margin-top: 6px; +} + +.us-notif-tg-qr { + flex-shrink: 0; + width: 88px; + height: 88px; + border-radius: 6px; + overflow: hidden; + background: #fff; + padding: 3px; + box-sizing: border-box; +} + +.us-notif-tg-qr svg { + width: 100%; + height: 100%; + display: block; +} + +.us-notif-tg-cmd-col { + display: flex; + flex-direction: column; + gap: 6px; + justify-content: center; + min-width: 0; +} + +.us-notif-tg-cmd-row { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; +} + +.us-notif-tg-cmd { + font-family: monospace; + font-size: 11px; + background: var(--settings-bg-secondary, rgba(0,0,0,0.15)); + border: 1px solid var(--settings-border, rgba(255,255,255,0.1)); + border-radius: 4px; + padding: 2px 6px; + color: var(--settings-text-primary); + user-select: all; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 180px; +} + +.us-notif-tg-copy-btn { + font: inherit; + font-size: 10px; + font-weight: 600; + padding: 2px 8px; + background: transparent; + border: 1px solid var(--settings-accent); + border-radius: 4px; + color: var(--settings-accent); + cursor: pointer; + white-space: nowrap; + transition: background 0.15s, color 0.15s; +} + +.us-notif-tg-copy-btn:hover { + background: var(--settings-accent); + color: #fff; +} + +.us-notif-tg-expired { + color: var(--settings-text-secondary); + font-style: italic; +} + .us-notif-tg-countdown { font-size: 10px; color: var(--settings-text-secondary); + white-space: nowrap; } .us-notif-signin {