Livrare Lot 3 (Frontend): aplicație web, aplicație mobilă Android, extensie browser

- Surse complete web (React/Vite) + mobil (React Native/Expo) + extensie (MV3)
- Documentație de livrare: ghid utilizare, matrice trasabilitate cerințe, raport testare furnizor
- Artefacte binare: imagine Docker didi-frontend:lot3-1.0, APK, extensie v3.2.6 + SHA256SUMS
- Configurare adresă platformă externalizată (build args / .env / config.js)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Top Clossers 2026-07-17 12:33:59 +03:00
commit cec967f953
321 changed files with 80506 additions and 0 deletions

327
extension/popup.js Normal file
View file

@ -0,0 +1,327 @@
// DIDI Analysis Tool - Popup Script (OIDC auth + URL/text analysis)
console.log('[POPUP] Script loaded');
document.addEventListener('DOMContentLoaded', async () => {
console.log('[POPUP] DOM loaded');
const loginSection = document.getElementById('loginSection');
const mainSection = document.getElementById('mainSection');
const authStatusEl = document.getElementById('authStatus');
const loginBtn = document.getElementById('loginBtn');
const loginStatus = document.getElementById('loginStatus');
const activateSnippingBtn = document.getElementById('activateSnipping');
const activateVideoSnippingBtn = document.getElementById('activateVideoSnipping');
const urlInput = document.getElementById('urlInput');
const analyzeUrlBtn = document.getElementById('analyzeUrlBtn');
const analyzeCurrentTabBtn = document.getElementById('analyzeCurrentTab');
const textInput = document.getElementById('textInput');
const analyzeTextBtn = document.getElementById('analyzeText');
const charCount = document.getElementById('charCount');
const statusDiv = document.getElementById('status');
const viewHistoryBtn = document.getElementById('viewHistory');
const disconnectBtn = document.getElementById('disconnectBtn');
// === AUTH UI ===
function showLogin() {
loginSection.style.display = 'block';
mainSection.style.display = 'none';
const dot = authStatusEl.querySelector('.auth-dot');
const text = authStatusEl.querySelector('.auth-text');
dot.className = 'auth-dot disconnected';
text.textContent = 'Neconectat';
}
function showMain(email) {
loginSection.style.display = 'none';
mainSection.style.display = 'block';
const dot = authStatusEl.querySelector('.auth-dot');
const text = authStatusEl.querySelector('.auth-text');
dot.className = 'auth-dot connected';
text.textContent = email || 'Conectat';
}
// Check auth on load
chrome.runtime.sendMessage({ action: 'getAuthStatus' }, (response) => {
if (response?.authenticated) {
showMain(response.email);
prefillFromActiveTab();
} else {
showLogin();
}
});
// === LOGIN (OIDC/PKCE prin Keycloak) ===
loginBtn.addEventListener('click', () => {
loginBtn.disabled = true;
loginBtn.textContent = 'Se deschide autentificarea…';
loginStatus.textContent = '';
chrome.runtime.sendMessage({ action: 'login' }, (response) => {
if (response?.success) {
loginStatus.className = 'login-status success';
loginStatus.textContent = 'Autentificat!';
setTimeout(() => {
showMain(response.email);
prefillFromActiveTab();
}, 400);
} else {
loginStatus.className = 'login-status error';
loginStatus.textContent = 'Autentificare eșuată: ' + (response?.error || 'eroare necunoscută');
}
loginBtn.disabled = false;
loginBtn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M11 7L9.6 8.4l2.6 2.6H2v2h10.2l-2.6 2.6L11 17l5-5-5-5zm9 12h-8v2h8c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2h-8v2h8v14z" fill="currentColor"/>
</svg>
Autentificare`;
});
});
// === DISCONNECT ===
disconnectBtn.addEventListener('click', () => {
chrome.runtime.sendMessage({ action: 'logout' }, () => {
showLogin();
});
});
// === PREFILL: URL-ul tabului activ + textul selectat în pagină ===
async function prefillFromActiveTab() {
try {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab) return;
if (tab.url && /^https?:/i.test(tab.url)) {
urlInput.value = tab.url;
analyzeUrlBtn.disabled = false;
}
// Selected text from the page → prefill the textarea (cerință caiet 2a)
try {
const results = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => window.getSelection().toString(),
});
const selected = (results?.[0]?.result || '').trim();
if (selected && !textInput.value) {
textInput.value = selected.substring(0, 5000);
textInput.dispatchEvent(new Event('input'));
showStatus('Textul selectat în pagină a fost preluat.', 'info');
}
} catch { /* restricted page (chrome://, PDF) — nothing to prefill */ }
} catch (e) {
console.warn('[POPUP] prefill failed:', e.message);
}
}
// === SCREENSHOT ===
activateSnippingBtn.addEventListener('click', async () => {
try {
showStatus('Se activează modul captură…', 'info');
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab) { showStatus('Nu există tab activ', 'error'); return; }
try {
// Inject render.js BEFORE content.js so window.DidiRender exists when
// content.js's 'showAnalysis' handler tries to render the modal. Required
// for tabs opened before the extension was last reloaded — manifest's
// content_scripts only inject on fresh page loads.
await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['render.js', 'content.js'] });
} catch (e) { /* already injected */ }
await new Promise(r => setTimeout(r, 100));
chrome.tabs.sendMessage(tab.id, { action: 'activateSnipping' }, (response) => {
if (chrome.runtime.lastError) {
showStatus('Activare eșuată. Reîncarcă pagina.', 'error');
return;
}
if (response?.success) {
showStatus('Mod captură activ!', 'success');
setTimeout(() => window.close(), 800);
} else {
showStatus('Captura necesită consimțământul pentru trimiterea imaginii.', 'error');
}
});
} catch (error) {
showStatus('Eroare: ' + error.message, 'error');
}
});
// === VIDEO REGION RECORDING ===
activateVideoSnippingBtn.addEventListener('click', async () => {
try {
showStatus('Se activează modul video…', 'info');
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab) { showStatus('Nu există tab activ', 'error'); return; }
try {
// Same injection ordering rationale as the screenshot flow above.
await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['render.js', 'content.js'] });
} catch (e) { /* already injected */ }
await new Promise(r => setTimeout(r, 100));
chrome.tabs.sendMessage(tab.id, { action: 'activateVideoSnipping' }, (response) => {
if (chrome.runtime.lastError) {
showStatus('Activare eșuată. Reîncarcă pagina.', 'error');
return;
}
if (response?.success) {
showStatus('Mod video activ!', 'success');
setTimeout(() => window.close(), 800);
} else {
showStatus('Înregistrarea necesită consimțământul pentru trimiterea capturii.', 'error');
}
});
} catch (error) {
showStatus('Eroare: ' + error.message, 'error');
}
});
// === SOCIAL POST SELECTION (extractoare Facebook / Twitter-X) ===
const selectSocialPostBtn = document.getElementById('selectSocialPost');
function socialPlatformFor(url) {
if (/https?:\/\/([a-z0-9-]+\.)?facebook\.com\//i.test(url)) return 'facebook';
if (/https?:\/\/([a-z0-9-]+\.)?(twitter|x)\.com\//i.test(url)) return 'twitter';
return null;
}
(async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab?.url && socialPlatformFor(tab.url)) {
selectSocialPostBtn.style.display = '';
}
})();
selectSocialPostBtn.addEventListener('click', async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
const platform = tab?.url ? socialPlatformFor(tab.url) : null;
if (!platform) { showStatus('Deschide o pagină Facebook sau X/Twitter.', 'error'); return; }
try {
await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['render.js', 'content.js'] });
} catch (e) { /* already injected */ }
await new Promise(r => setTimeout(r, 100));
chrome.tabs.sendMessage(tab.id, { action: 'activateSelection', platform }, (response) => {
if (chrome.runtime.lastError) {
showStatus('Activare eșuată. Reîncarcă pagina.', 'error');
return;
}
if (response?.success) {
showStatus('Mod selecție activ — dă click pe o postare.', 'success');
setTimeout(() => window.close(), 800);
}
});
});
// === URL ANALYSIS (cerință caiet 1a/1b) ===
function isValidHttpUrl(value) {
try {
const u = new URL(/^www\./i.test(value) ? `https://${value}` : value);
return u.protocol === 'http:' || u.protocol === 'https:';
} catch {
return false;
}
}
urlInput.addEventListener('input', () => {
analyzeUrlBtn.disabled = !isValidHttpUrl(urlInput.value.trim());
});
analyzeCurrentTabBtn.addEventListener('click', async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab?.url || !/^https?:/i.test(tab.url)) {
showStatus('Pagina curentă nu are un URL analizabil.', 'error');
return;
}
urlInput.value = tab.url;
analyzeUrlBtn.disabled = false;
submitUrl(tab.url);
});
analyzeUrlBtn.addEventListener('click', () => {
const url = urlInput.value.trim();
if (!isValidHttpUrl(url)) {
showStatus('Introdu un URL valid (http/https).', 'error');
return;
}
submitUrl(url);
});
function submitUrl(url) {
analyzeUrlBtn.disabled = true;
showStatus('Trimis către DiDi… rezultatul se deschide într-un tab nou în 3090s.', 'info');
chrome.runtime.sendMessage({ action: 'analyzeUrl', url }, (response) => {
analyzeUrlBtn.disabled = false;
if (response?.success) {
showStatus('Trimis! Rezultatul se va deschide când e gata.', 'success');
setTimeout(() => window.close(), 1500);
} else {
showStatus('Eroare: ' + (response?.error || 'necunoscută'), 'error');
}
});
}
// === TEXT ANALYSIS ===
textInput.addEventListener('input', () => {
const len = textInput.value.trim().length;
charCount.textContent = len;
analyzeTextBtn.disabled = len === 0 || len > 5000;
});
analyzeTextBtn.addEventListener('click', () => {
const text = textInput.value.trim();
if (!text) return;
analyzeTextBtn.disabled = true;
analyzeTextBtn.textContent = 'Se trimite…';
showStatus('Trimis către DiDi… rezultatul se deschide într-un tab nou în 3090s.', 'info');
// Background acks immediately, then runs the polling in the service worker
// and opens a new tab with the rendered standalone result page when done.
chrome.runtime.sendMessage({ action: 'analyzeText', text }, (response) => {
analyzeTextBtn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M8 2L10 6L14 7L10 8L8 14L6 8L2 7L6 6L8 2Z" fill="currentColor"/>
</svg>
Analizează text`;
analyzeTextBtn.disabled = false;
if (response?.success) {
showStatus('Trimis! Rezultatul se va deschide când e gata.', 'success');
textInput.value = '';
charCount.textContent = '0';
setTimeout(() => window.close(), 1500);
} else {
showStatus('Eroare: ' + (response?.error || 'necunoscută'), 'error');
}
});
});
// === HISTORY ===
viewHistoryBtn.addEventListener('click', () => {
chrome.tabs.create({ url: chrome.runtime.getURL('history.html') });
});
// === STATUS ===
function showStatus(message, type) {
statusDiv.textContent = message;
statusDiv.className = `status ${type}`;
statusDiv.style.display = 'block';
setTimeout(() => { statusDiv.style.display = 'none'; }, 3000);
}
});