// "Telefon bağlantısı" window UI. Talks to electron/mobile-host.mjs over the // `mobileHost` bridge (mobile-preload.cjs); all logic lives in the main process. /* global window, document */ import { drawQr } from './mobile/qr.mjs' const TEXT = { tr: { eyebrow: 'AITURK · BİLGİSAYAR', title: 'Telefon bağlantısı', intro: 'AITURK Mobil uygulaması, aynı Wi-Fi ağındayken bu bilgisayardaki IDE konuşmalarınızı görür ve sohbet eder. Hesap girişi gerekmez.', enabled: 'Telefon bağlantısı açık', disabled: 'Telefon bağlantısı kapalı', remote: 'IDE şu an uzak bir ağ geçidine bağlı. Telefon bağlantısı yalnız yerel IDE bağlantısıyla çalışır; Ayarlar’dan yerel bağlantıyı seçin.', firewallNeeded: 'Windows Güvenlik Duvarı izni henüz verilmedi. İzin olmadan telefon bu bilgisayara ulaşamayabilir.', firewallDenied: 'Windows izin penceresi reddedildi; Güvenlik Duvarı kuralı eklenemedi. İzin olmadan telefon bu bilgisayara ulaşamayabilir.', firewallFailed: 'Windows Güvenlik Duvarı kuralı eklenemedi. İzin olmadan telefon bu bilgisayara ulaşamayabilir.', retry: 'Yeniden dene', addPhone: 'Telefon ekle', cancel: 'Vazgeç', scan: 'Telefonda AITURK Mobil → “Bilgisayara bağlan” → bu QR kodunu okutun.', orCode: 'ya da telefonda şu kodu yazın:', codePending: 'Kodla bağlanma hazırlanıyor…', codeReady: 'Kodla bağlanma hazır.', codeUnavailable: 'Kodla bağlanma şu an kullanılamıyor; QR kodunu okutun.', sameWifi: 'Telefon ve bilgisayar aynı Wi-Fi ağında olmalı.', countdown: (m, s) => `Kod ${m}:${s} sonra geçersiz olur`, expired: 'Kodun süresi doldu. Yeniden “Telefon ekle” seçin.', devices: 'Bağlı telefonlar', noDevices: 'Henüz bağlı telefon yok.', added: 'Eklenme', lastSeen: 'Son görülme', online: 'şu an bağlı', remove: 'Kaldır', removed: name => `${name} kaldırıldı.`, paired: name => `${name} bağlandı.`, footer: port => `Bağlantı noktası: ${port}`, removeRule: 'Güvenlik Duvarı kuralını kaldır', ruleRemoved: 'Güvenlik Duvarı kuralı kaldırıldı.', errors: { remote_mode: 'IDE uzak ağ geçidine bağlıyken telefon bağlantısı açılamaz.', vault_unavailable: 'Windows güvenli anahtar kasası kullanılamıyor; telefon bağlantısı açılamadı.', not_enabled: 'Önce telefon bağlantısını açın.', no_lan_address: 'Bu bilgisayar bir yerel ağa (Wi-Fi ya da kablo) bağlı görünmüyor.', port_unavailable: 'Boş bağlantı noktası bulunamadı (47321–47340 kullanımda).', failed: 'İşlem tamamlanamadı. Yeniden deneyin.', }, }, en: { eyebrow: 'AITURK · COMPUTER', title: 'Phone connection', intro: 'The AITURK Mobile app can see and continue this computer’s IDE conversations while on the same Wi-Fi network. No account sign-in is needed.', enabled: 'Phone connection is on', disabled: 'Phone connection is off', remote: 'The IDE is connected to a remote gateway. Phone connection only works with the local IDE connection; choose the local connection in Settings.', firewallNeeded: 'Windows Firewall permission has not been granted yet. Without it the phone may not reach this computer.', firewallDenied: 'The Windows permission prompt was declined, so the Firewall rule was not added. Without it the phone may not reach this computer.', firewallFailed: 'The Windows Firewall rule could not be added. Without it the phone may not reach this computer.', retry: 'Try again', addPhone: 'Add a phone', cancel: 'Cancel', scan: 'On the phone: AITURK Mobile → “Connect to computer” → scan this QR code.', orCode: 'or type this code on the phone:', codePending: 'Preparing code sign-in…', codeReady: 'Connecting with the code is ready.', codeUnavailable: 'Connecting with the code is unavailable right now; scan the QR code.', sameWifi: 'The phone and the computer must be on the same Wi-Fi network.', countdown: (m, s) => `The code expires in ${m}:${s}`, expired: 'The code expired. Choose “Add a phone” again.', devices: 'Connected phones', noDevices: 'No phone is connected yet.', added: 'Added', lastSeen: 'Last seen', online: 'connected now', remove: 'Remove', removed: name => `${name} was removed.`, paired: name => `${name} connected.`, footer: port => `Port: ${port}`, removeRule: 'Remove the Firewall rule', ruleRemoved: 'The Firewall rule was removed.', errors: { remote_mode: 'Phone connection cannot be turned on while the IDE uses a remote gateway.', vault_unavailable: 'The Windows secure key store is unavailable; phone connection could not be turned on.', not_enabled: 'Turn phone connection on first.', no_lan_address: 'This computer does not seem to be on a local network (Wi-Fi or cable).', port_unavailable: 'No free port was found (47321–47340 are in use).', failed: 'That did not work. Try again.', }, }, } const lang = (navigator.language || '').toLowerCase().startsWith('en') ? 'en' : 'tr' const t = TEXT[lang], host = window.mobileHost, $ = id => document.getElementById(id) const when = value => new Date(value).toLocaleString(lang === 'tr' ? 'tr-TR' : 'en-GB', { dateStyle: 'medium', timeStyle: 'short' }) document.documentElement.lang = lang; document.title = `AITURK · ${t.title}` for (const [id, key] of [['t-eyebrow', 'eyebrow'], ['t-title', 'title'], ['t-intro', 'intro'], ['t-remote', 'remote'], ['t-scan', 'scan'], ['t-or-code', 'orCode'], ['t-same-wifi', 'sameWifi'], ['t-devices', 'devices'], ['devices-empty', 'noDevices'], ['firewall-retry', 'retry'], ['pair-begin', 'addPhone'], ['pair-cancel', 'cancel'], ['firewall-remove', 'removeRule']]) $(id).textContent = t[key] let current = null, drawnQr = '', busy = false function notice(text) { $('notice').textContent = text || ''; $('notice').hidden = !text } const errorText = error => t.errors[/aiturk-mobile:([a-z_]+)/.exec(String(error?.message))?.[1]] || t.errors.failed function tick() { const pairing = current?.pairing if (!pairing) return const left = Math.max(0, Math.ceil((pairing.expiresAt - Date.now()) / 1000)) $('pair-countdown').textContent = t.countdown(Math.floor(left / 60), String(left % 60).padStart(2, '0')) if (left === 0) { notice(t.expired); void refresh() } } function render(state) { current = state $('enabled').checked = state.enabled; $('t-enabled').textContent = state.enabled ? t.enabled : t.disabled $('remote-box').hidden = !state.remote const firewall = { needed: t.firewallNeeded, denied: t.firewallDenied, failed: t.firewallFailed }[state.firewall] $('firewall-box').hidden = !(state.enabled && firewall); $('firewall-text').textContent = firewall || '' $('pair-section').hidden = !state.enabled || state.remote $('pair-begin').hidden = Boolean(state.pairing); $('pair-live').hidden = !state.pairing if (state.pairing) { if (drawnQr !== state.pairing.qr) { drawQr($('pair-qr'), state.pairing.qr); drawnQr = state.pairing.qr } $('pair-qr').setAttribute('aria-label', t.scan) $('pair-code').textContent = `${state.pairing.code.slice(0, 4)}-${state.pairing.code.slice(4)}` $('pair-code-path').textContent = { pending: t.codePending, ready: t.codeReady, unavailable: t.codeUnavailable }[state.pairing.codePath] tick() } else { drawnQr = ''; $('pair-code').textContent = '' } $('devices-section').hidden = !state.enabled && state.devices.length === 0 $('devices-empty').hidden = state.devices.length > 0 $('devices').replaceChildren(...state.devices.map(device => { const row = document.createElement('div'), label = document.createElement('div'), name = document.createElement('strong') const added = document.createElement('small'), seen = document.createElement('small'), button = document.createElement('button') row.className = 'device'; name.textContent = device.name added.textContent = `${t.added}: ${when(device.createdAt)}` seen.textContent = device.live > 0 ? t.online : `${t.lastSeen}: ${when(device.lastSeenAt)}` button.textContent = t.remove; button.className = 'danger' button.onclick = () => run(async () => { render(await host.call('device.revoke', { deviceId: device.id })); notice(t.removed(device.name)) }) label.append(name, added, seen); row.append(label, button) return row })) $('footer').hidden = state.port === null $('footer-info').textContent = state.port === null ? '' : t.footer(state.port) $('firewall-remove').hidden = state.firewall !== 'ok' } async function run(work) { if (busy) return busy = true; document.body.setAttribute('aria-busy', 'true') for (const button of document.querySelectorAll('button,input')) button.disabled = true try { await work() } catch (error) { notice(errorText(error)); await refresh() } finally { busy = false; document.body.removeAttribute('aria-busy'); for (const button of document.querySelectorAll('button,input')) button.disabled = false } } async function refresh() { try { render(await host.call('state')) } catch (error) { notice(errorText(error)) } } $('enabled').onchange = () => { const on = $('enabled').checked; void run(async () => { notice(''); render(await host.call(on ? 'enable' : 'disable')) }) } $('pair-begin').onclick = () => run(async () => { notice(''); render(await host.call('pair.begin')) }) $('pair-cancel').onclick = () => run(async () => { render(await host.call('pair.cancel')) }) $('firewall-retry').onclick = () => run(async () => { notice(''); render(await host.call('firewall.retry')) }) $('firewall-remove').onclick = () => run(async () => { render(await host.call('firewall.remove')); notice(t.ruleRemoved) }) host.onEvent(event => { if (!event?.state) return; render(event.state); if (event.type === 'paired') notice(t.paired(event.name)) }) setInterval(tick, 1000) // "Last seen" and the live marker change without an event; keep the list fresh while the window is open. setInterval(() => { if (!busy && current?.enabled) void refresh() }, 15000) void refresh()