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:
commit
cec967f953
321 changed files with 80506 additions and 0 deletions
327
extension/popup.js
Normal file
327
extension/popup.js
Normal 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 30–90s.', '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 30–90s.', '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);
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue