let Zotero, addon, items, api; let selectedFolderId = null; let selectedParentId = null; function createHtmlElement(tagName) { return document.createElementNS("http://www.w3.org/1999/xhtml", tagName); } function init() { const args = window.arguments[0]; Zotero = args.Zotero; addon = args.addon; items = args.items; api = args.api; loadFolders(); } async function loadFolders() { const container = document.getElementById("folders-container"); try { const folders = await api.sendEncryptedGetRequest("/api/extension/folders"); if (!folders || folders.length === 0) { container.innerHTML = `
No folders found in Research OS. Please create one on the website first.
`; return; } container.innerHTML = ""; const treeRootUl = createHtmlElement("ul"); treeRootUl.className = "folder-tree"; container.appendChild(treeRootUl); folders.forEach(rootFolder => { // 1. Create root folder node const rootNodeLi = createHtmlElement("li"); rootNodeLi.className = "folder-node"; const rootDiv = createHtmlElement("div"); rootDiv.className = "folder-item"; rootDiv.innerHTML = `📁${rootFolder.name}`; rootDiv.onclick = () => selectFolder(rootFolder.id, null, rootDiv); rootNodeLi.appendChild(rootDiv); treeRootUl.appendChild(rootNodeLi); const subfolders = rootFolder.children || []; // Helper to render child folders recursively function renderSubfolders(parentId, parentElement) { const children = subfolders.filter(sub => { if (parentId === null) { return sub.parent_id === null || sub.parent_id === undefined; } return sub.parent_id === parentId; }); if (children.length > 0) { const branchUl = createHtmlElement("ul"); branchUl.className = "folder-branch"; parentElement.appendChild(branchUl); children.forEach(sub => { const childLi = createHtmlElement("li"); childLi.className = "folder-node"; const childDiv = createHtmlElement("div"); childDiv.className = "folder-item"; childDiv.innerHTML = `📂${sub.name}`; childDiv.onclick = () => selectFolder(rootFolder.id, sub.id, childDiv); childLi.appendChild(childDiv); branchUl.appendChild(childLi); // Recurse for nested folders renderSubfolders(sub.id, childLi); }); } } // Start recursion renderSubfolders(null, rootNodeLi); }); } catch (err) { Zotero.debug(err); container.innerHTML = `
Failed to fetch Research OS folders. ${err.message}
`; } } function cleanDoi(input) { if (!input) return ""; let cleaned = input.trim().toLowerCase(); cleaned = cleaned.replace(/^(https?:\/\/)?(www\.)?(dx\.)?doi\.org\//i, ""); cleaned = cleaned.replace(/^doi\s*:\s*/i, ""); const doiRegex = /(10\.\d{4,9}\/[-._;()/:a-zA-Z0-9]+)/i; const match = cleaned.match(doiRegex); return match ? match[1] : cleaned; } function selectFolder(folderId, parentId, element) { selectedFolderId = folderId; selectedParentId = parentId; // Clear selection styles const items = document.querySelectorAll(".folder-item"); items.forEach(item => item.classList.remove("selected")); // Highlight selected element element.classList.add("selected"); // Enable Sync button document.getElementById("btn-sync").disabled = false; } async function startSync() { // Switch screens document.getElementById("folder-screen").classList.remove("active"); document.getElementById("progress-screen").classList.add("active"); const itemsContainer = document.getElementById("items-container"); itemsContainer.innerHTML = ""; // Create progress rows for each item items.forEach((item, index) => { const row = createHtmlElement("div"); row.className = "sync-item-row"; row.id = `sync-row-${index}`; // Determine if it has a PDF attachment let hasPdf = false; if (item.isRegularItem()) { const attachments = item.getAttachments(); for (let attId of attachments) { const att = Zotero.Items.get(attId); if (att.attachmentContentType === "application/pdf" || att.getField("title").toLowerCase().endsWith(".pdf")) { hasPdf = true; break; } } } row.innerHTML = `
${escapeHtml(item.getField("title") || "Untitled")}
${hasPdf ? "📎 PDF Attachment found" : "📄 No PDF Attachment"}
Pending
`; itemsContainer.appendChild(row); }); updateProgress(0, "Fetching existing papers in target location..."); let existingPapers = []; try { const parentParam = selectedParentId ? `?parent_id=${selectedParentId}` : ""; existingPapers = await api.sendEncryptedGetRequest(`/api/extension/folders/${selectedFolderId}/papers${parentParam}`) || []; } catch (err) { Zotero.debug(`Failed to fetch existing papers: ${err.message}`); } updateProgress(0, `Starting synchronization of ${items.length} items...`); let successCount = 0; for (let i = 0; i < items.length; i++) { const item = items[i]; const statusLabel = document.getElementById(`sync-status-${i}`); const detailLabel = document.getElementById(`sync-detail-${i}`); statusLabel.className = "sync-item-status status-syncing"; statusLabel.textContent = "Syncing"; updateProgress(Math.round((i / items.length) * 100), `Syncing: ${item.getField("title")}`); try { // Step 1: Gather metadata const paperTitle = item.getField("title") || "Untitled"; let paperAbstract = item.getField("abstractNote") || ""; let paperAuthors = ""; try { paperAuthors = item.getCreators().map(c => `${c.firstName} ${c.lastName}`.trim()).join(", "); } catch (e) {} let paperJournal = item.getField("publicationTitle") || item.getField("journalAbbreviation") || ""; let paperYear = ""; try { const date = item.getField("date"); if (date) { const match = date.match(/\d{4}/); if (match) paperYear = match[0]; } } catch (e) {} let paperPublisher = ""; try { paperPublisher = item.getField("publisher") || ""; } catch (e) {} let paperSjr = ""; let paperQuartile = ""; // Step 2: Try to get DOI and check duplicate let doi = ""; try { doi = item.getField("DOI"); } catch (e) {} if (!doi) { try { const extra = item.getField("extra") || ""; const match = extra.match(/DOI:\s*(\S+)/i); if (match) doi = match[1]; } catch (e) {} } // Duplicate matching logic const cleanZoteroDoi = cleanDoi(doi); const normalizedZoteroTitle = paperTitle.toLowerCase().replace(/[^a-z0-9]/g, ""); const duplicatePaper = existingPapers.find(existing => { // 1. Try DOI matching let existingDoi = ""; if (existing.metadata) { try { const meta = typeof existing.metadata === 'string' ? JSON.parse(existing.metadata) : existing.metadata; existingDoi = meta.doi || ""; } catch (e) {} } if (cleanZoteroDoi && existingDoi) { if (cleanZoteroDoi === cleanDoi(existingDoi)) { return true; } } // 2. Try Title matching const normalizedExistingTitle = (existing.name || "").toLowerCase().replace(/[^a-z0-9]/g, ""); if (normalizedZoteroTitle && normalizedExistingTitle && normalizedZoteroTitle === normalizedExistingTitle) { return true; } return false; }); const isDuplicate = !!duplicatePaper; if (isDuplicate) { detailLabel.textContent = `Duplicate found. Merging attachment into existing paper (ID: ${duplicatePaper.id})...`; } let enriched = null; // If NOT duplicate, fetch metadata if (!isDuplicate) { if (doi) { detailLabel.textContent = "Extracting DOI and enriching metadata..."; try { const enrichUrl = `/api/extension/paper?doi=${encodeURIComponent(doi)}`; enriched = await api.sendEncryptedGetRequest(enrichUrl); } catch (enrichErr) { Zotero.debug(`Failed to enrich via DOI: ${enrichErr.message}`); } } else { detailLabel.textContent = "No DOI found, searching by title..."; try { const enrichUrl = `/api/extension/paper?title=${encodeURIComponent(paperTitle)}`; enriched = await api.sendEncryptedGetRequest(enrichUrl); } catch (enrichErr) { Zotero.debug(`Failed to enrich via title: ${enrichErr.message}`); } } if (enriched && enriched.metadata) { if (enriched.metadata.abstract) paperAbstract = enriched.metadata.abstract; if (enriched.metadata.journal && enriched.metadata.journal !== 'N/A') paperJournal = enriched.metadata.journal; if (enriched.metadata.year && enriched.metadata.year !== 'N/A') paperYear = enriched.metadata.year; if (enriched.metadata.authors && enriched.metadata.authors.length > 0) { paperAuthors = Array.isArray(enriched.metadata.authors) ? enriched.metadata.authors.join(', ') : enriched.metadata.authors; } if (enriched.metadata.publisher && enriched.metadata.publisher !== 'N/A') { paperPublisher = enriched.metadata.publisher; } else if (enriched.ranking && enriched.ranking.publisher) { paperPublisher = enriched.ranking.publisher; } if (enriched.ranking) { paperSjr = enriched.ranking.sjr || ""; paperQuartile = enriched.ranking.quartile || ""; } } } // Step 3: Handle PDF attachments const pdfs = []; const attachments = item.getAttachments(); let pdfAttachment = null; for (let attId of attachments) { const att = Zotero.Items.get(attId); if (att.attachmentContentType === "application/pdf" || att.getField("title").toLowerCase().endsWith(".pdf")) { pdfAttachment = att; break; } } if (pdfAttachment) { detailLabel.textContent = isDuplicate ? "Uploading duplicate PDF attachment..." : "Reading local PDF copy..."; const filePath = await pdfAttachment.getFilePathAsync(); if (filePath) { // Read local file bytes const bytes = await IOUtils.read(filePath); const blob = new Blob([bytes], { type: "application/pdf" }); detailLabel.textContent = "Uploading PDF to Research OS (Cloudflare R2)..."; const uploadRes = await api.uploadPdf(blob, `${paperTitle.replace(/[^a-zA-Z0-9]/g, "_")}.pdf`); if (uploadRes && uploadRes.url) { pdfs.push({ id: Date.now().toString() + "_" + Math.random().toString(36).substr(2, 9), name: isDuplicate ? `Attached PDF (${new Date().toLocaleDateString()})` : "Main Paper", url: uploadRes.url, type: "file", createdAt: new Date().toISOString() }); detailLabel.textContent = "PDF successfully uploaded."; } } else { Zotero.debug("PDF attachment file path was empty. PDF will not be uploaded."); } } // Step 4: Insert/Update paper in database detailLabel.textContent = isDuplicate ? "Updating existing paper record..." : "Saving paper metadata to Research OS..."; const paperPayload = { title: paperTitle, doi: doi, abstract: paperAbstract, authors: paperAuthors, journal: paperJournal, year: paperYear, publisher: paperPublisher, sjr: paperSjr, quartile: paperQuartile, pdfs: pdfs }; const payload = { folderId: selectedFolderId, parent_id: selectedParentId, paper: paperPayload }; if (isDuplicate) { payload.paperId = duplicatePaper.id; } await api.sendEncryptedRequest("/api/extension/insert", payload); statusLabel.className = "sync-item-status status-success"; statusLabel.textContent = "Success"; detailLabel.textContent = isDuplicate ? (pdfs.length > 0 ? "Merged new PDF successfully" : "Already matched (no new PDF)") : (pdfs.length > 0 ? "Synced successfully with PDF" : "Synced successfully (no PDF)"); successCount++; } catch (err) { Zotero.debug(err); statusLabel.className = "sync-item-status status-error"; statusLabel.textContent = "Error"; detailLabel.textContent = `Error: ${err.message}`; appendErrorLog(item.getField("title") || "Untitled", err); } } updateProgress(100, `Completed! Successfully synced ${successCount} of ${items.length} items.`); document.getElementById("btn-close").disabled = false; } function appendErrorLog(title, err) { const container = document.getElementById("error-log-container"); const logBox = document.getElementById("error-log-box"); if (container && logBox) { container.style.display = "flex"; const timestamp = new Date().toLocaleTimeString(); const stackInfo = err.stack ? `\nStack trace:\n${err.stack}` : ""; logBox.value += `[${timestamp}] Error syncing "${title}":\n${err.message}${stackInfo}\n\n`; } } function updateProgress(percent, text) { document.getElementById("progress-bar").style.width = `${percent}%`; document.getElementById("progress-percent").textContent = `${percent}%`; document.getElementById("progress-text").textContent = text; } function escapeHtml(text) { const map = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }; return text.replace(/[&<>"']/g, m => map[m]); }