diff --git a/src/zotero_kb/templates/index.html b/src/zotero_kb/templates/index.html index ec6ac8b..4ac0629 100644 --- a/src/zotero_kb/templates/index.html +++ b/src/zotero_kb/templates/index.html @@ -461,7 +461,7 @@ currentCards: [], collectionTree: [], isImportWindowOpen: false, - isImportModalOpen: false, + isImportWindowOpen: false, windowX: typeof windowSessionState.x === "number" ? windowSessionState.x : null, windowY: typeof windowSessionState.y === "number" ? windowSessionState.y : null, windowWidth: typeof windowSessionState.width === "number" ? windowSessionState.width : null, @@ -562,6 +562,110 @@ ); } + function applyWindowPositionAndSize() { + const fw = elements.importWindow; + if (!fw) return; + fw.style.left = state.windowX !== null ? `${state.windowX}px` : ""; + fw.style.top = state.windowY !== null ? `${state.windowY}px` : ""; + fw.style.width = state.windowWidth !== null ? `${state.windowWidth}px` : ""; + fw.style.height = state.windowHeight !== null ? `${state.windowHeight}px` : ""; + } + + function initWindowFromSession() { + const fw = elements.importWindow; + const bar = elements.importWindowMinimizedBar; + if (state.isWindowMinimized) { + fw.classList.add("minimized"); + fw.classList.remove("maximized"); + bar.hidden = false; + } else if (state.isWindowMaximized) { + fw.classList.add("maximized"); + fw.classList.remove("minimized"); + fw.style.width = "100vw"; + fw.style.height = "100vh"; + fw.style.left = "0"; + fw.style.top = "0"; + bar.hidden = true; + } else { + if (state.windowX !== null) { + applyWindowPositionAndSize(); + } + fw.classList.remove("minimized", "maximized"); + bar.hidden = true; + } + } + + function startWindowDrag(event) { + const fw = elements.importWindow; + if (!fw || fw.classList.contains("maximized") || fw.classList.contains("minimized")) return; + event.preventDefault(); + state.isWindowDragging = true; + state.windowDragStartX = event.clientX; + state.windowDragStartY = event.clientY; + state.windowDragStartLeft = parseFloat(fw.style.left) || 0; + state.windowDragStartTop = parseFloat(fw.style.top) || 0; + fw.style.transition = "none"; + } + + function moveWindowDrag(event) { + if (!state.isWindowDragging) return; + event.preventDefault(); + const fw = elements.importWindow; + const dx = event.clientX - state.windowDragStartX; + const dy = event.clientY - state.windowDragStartY; + let newLeft = state.windowDragStartLeft + dx; + let newTop = state.windowDragStartTop + dy; + const maxLeft = window.innerWidth - (parseFloat(fw.style.width) || fw.offsetWidth); + const maxTop = window.innerHeight - (parseFloat(fw.style.height) || fw.offsetHeight); + newLeft = Math.max(0, Math.min(newLeft, maxLeft)); + newTop = Math.max(0, Math.min(newTop, maxTop)); + fw.style.left = `${newLeft}px`; + fw.style.top = `${newTop}px`; + state.windowX = newLeft; + state.windowY = newTop; + } + + function endWindowDrag() { + if (!state.isWindowDragging) return; + state.isWindowDragging = false; + persistWindowSessionState(); + } + + function minimizeWindow() { + state.isWindowMinimized = true; + state.isWindowMaximized = false; + elements.importWindow.classList.add("minimized"); + elements.importWindow.classList.remove("maximized"); + elements.importWindowMinimizedBar.hidden = false; + persistWindowSessionState(); + } + + function maximizeWindow() { + state.isWindowMaximized = !state.isWindowMaximized; + if (state.isWindowMaximized) { + state.isWindowMinimized = false; + elements.importWindow.classList.add("maximized"); + elements.importWindow.classList.remove("minimized"); + elements.importWindowMinimizedBar.hidden = true; + elements.importWindow.style.width = "100vw"; + elements.importWindow.style.height = "100vh"; + elements.importWindow.style.left = "0"; + elements.importWindow.style.top = "0"; + } else { + elements.importWindow.classList.remove("maximized"); + applyWindowPositionAndSize(); + } + persistWindowSessionState(); + } + + function restoreWindow() { + state.isWindowMinimized = false; + elements.importWindow.classList.remove("minimized"); + elements.importWindowMinimizedBar.hidden = true; + applyWindowPositionAndSize(); + persistWindowSessionState(); + } + function setStatus(target, message, isError = false) { target.textContent = message || ""; target.classList.toggle("error", Boolean(isError)); @@ -677,7 +781,7 @@ } function renderSelectedCount() { - elements.selectedCount.textContent = `已选 ${state.selectedItemKeys.size} 篇`; + elements.windowSelectedCount.textContent = `已选 ${state.selectedItemKeys.size} 篇`; } function resetCollectionItemsLoadingState(clearItems = false) { @@ -694,21 +798,20 @@ if (currentProject) { const label = `当前项目:${currentProject.name}`; elements.currentProjectLabel.textContent = label; - elements.importModalProjectLabel.textContent = label; + elements.windowProjectLabel.textContent = label; return; } elements.currentProjectLabel.textContent = "当前未选择项目。"; - elements.importModalProjectLabel.textContent = "未选择项目"; + elements.windowProjectLabel.textContent = "未选择项目"; } function updateImportActionState() { const hasProject = Boolean(state.currentProjectId); const hasSelection = state.selectedItemKeys.size > 0; syncCurrentProjectLabels(); - elements.importSelectedItemsButton.disabled = !hasProject || !hasSelection; - elements.importSelectedItemsButton.title = !hasProject ? "请先选择项目" : (hasSelection ? "" : "请先选择文献"); - elements.selectDescendantsButton.disabled = !state.collectionItemsLoaded || !state.visibleCollectionItems.length; - elements.clearSelectionButton.disabled = !state.selectedItemKeys.size; + elements.windowImportSelectedItemsButton.disabled = !hasProject || !hasSelection; + elements.windowImportSelectedItemsButton.title = !hasProject ? "请先选择项目" : (hasSelection ? "" : "请先选择文献"); + elements.windowClearSelectionButton.disabled = !state.selectedItemKeys.size; renderSelectedCount(); } @@ -752,11 +855,11 @@ function renderCollectionTree() { if (!state.collectionTree.length) { - elements.collectionTree.innerHTML = '
暂无 collection。
'; + elements.windowCollectionTree.innerHTML = '
暂无 collection。
'; return; } - elements.collectionTree.innerHTML = ""; + elements.windowCollectionTree.innerHTML = ""; const fragment = document.createDocumentFragment(); function appendNode(node, depth) { @@ -801,7 +904,7 @@ `; button.addEventListener("click", () => { loadCollectionItems(node.collection_key).catch((error) => { - setStatus(elements.collectionStatus, error.message, true); + setStatus(elements.windowCollectionStatus, error.message, true); }); }); row.appendChild(button); @@ -818,29 +921,29 @@ for (const node of state.collectionTree) { appendNode(node, 0); } - elements.collectionTree.appendChild(fragment); + elements.windowCollectionTree.appendChild(fragment); } function renderCollectionItems() { if (!state.selectedCollectionKey) { - elements.collectionItems.innerHTML = '
先选择一个 collection。
'; + elements.windowCollectionItems.innerHTML = '
先选择一个 collection。
'; updateImportActionState(); return; } if (!state.collectionItemsLoaded) { - elements.collectionItems.innerHTML = state.loadingCollectionKey === state.selectedCollectionKey + elements.windowCollectionItems.innerHTML = state.loadingCollectionKey === state.selectedCollectionKey ? '
正在加载当前 collection 文献。
' : '
当前 collection 文献尚未加载。
'; updateImportActionState(); return; } if (!state.visibleCollectionItems.length) { - elements.collectionItems.innerHTML = '
当前目录及子目录下没有可导入文献。
'; + elements.windowCollectionItems.innerHTML = '
当前目录及子目录下没有可导入文献。
'; updateImportActionState(); return; } - elements.collectionItems.innerHTML = ""; + elements.windowCollectionItems.innerHTML = ""; for (const item of state.visibleCollectionItems) { const row = document.createElement("label"); row.className = "project-item"; @@ -856,13 +959,13 @@ `; row.querySelector("input").addEventListener("change", () => toggleItemSelection(item.item_key)); - elements.collectionItems.appendChild(row); + elements.windowCollectionItems.appendChild(row); } updateImportActionState(); } async function loadCollectionTree() { - setStatus(elements.collectionStatus, "正在加载 collection..."); + setStatus(elements.windowCollectionStatus, "正在加载 collection..."); const payload = await api("/api/zotero/collections/tree"); state.collectionTree = Array.isArray(payload.collections) ? payload.collections : []; normalizeSelectedCollectionKey(); @@ -876,7 +979,7 @@ state.visibleCollectionItems = []; resetCollectionItemsLoadingState(); renderCollectionItems(); - setStatus(elements.collectionStatus, ""); + setStatus(elements.windowCollectionStatus, ""); } async function loadCollectionItems(collectionKey) { @@ -888,7 +991,7 @@ resetCollectionItemsLoadingState(true); renderCollectionTree(); renderCollectionItems(); - setStatus(elements.collectionStatus, state.selectedCollectionKey ? "已切换到可用 collection。" : ""); + setStatus(elements.windowCollectionStatus, state.selectedCollectionKey ? "已切换到可用 collection。" : ""); if (state.selectedCollectionKey) { await loadCollectionItems(state.selectedCollectionKey); } @@ -903,7 +1006,7 @@ state.nextCollectionItemsRequestToken = requestToken; state.activeCollectionItemsRequestToken = requestToken; renderCollectionItems(); - setStatus(elements.collectionStatus, "正在加载文献..."); + setStatus(elements.windowCollectionStatus, "正在加载文献..."); try { const payload = await api(`/api/zotero/collections/${collectionKey}/items`); if (state.activeCollectionItemsRequestToken !== requestToken || state.selectedCollectionKey !== collectionKey) { @@ -915,7 +1018,7 @@ renderCollectionItems(); const visibleCount = state.visibleCollectionItems.length; setStatus( - elements.collectionStatus, + elements.windowCollectionStatus, visibleCount ? `当前目录及子目录共 ${visibleCount} 篇文献。` : "当前目录及子目录下没有可导入文献。" ); } catch (error) { @@ -959,7 +1062,7 @@ updateImportActionState(); return; } - setStatus(elements.collectionStatus, "正在导入..."); + setStatus(elements.windowCollectionStatus, "正在导入..."); const payload = await api(`/api/projects/${state.currentProjectId}/imports/item-keys`, { method: "POST", body: JSON.stringify({ item_keys: Array.from(state.selectedItemKeys) }), @@ -968,22 +1071,24 @@ persistImportSessionState(); renderCollectionItems(); renderCards(payload.project_view?.cards || []); - setStatus(elements.collectionStatus, "导入完成。"); + setStatus(elements.windowCollectionStatus, "导入完成。"); updateImportActionState(); - closeImportModal(); + closeImportWindow(); } - async function openImportModal() { - state.isImportModalOpen = true; - elements.importModal.hidden = false; - elements.importModal.setAttribute("aria-hidden", "false"); + async function openImportWindow() { + state.isImportWindowOpen = true; + elements.importWindowShell.hidden = false; + elements.importWindowShell.setAttribute("aria-hidden", "false"); document.body.classList.toggle("modal-open", true); + cancelAbstractPreview(); updateImportActionState(); + initWindowFromSession(); if (!state.collectionTree.length) { try { await loadCollectionTree(); } catch (error) { - setStatus(elements.collectionStatus, error.message, true); + setStatus(elements.windowCollectionStatus, error.message, true); } } else if ( state.selectedCollectionKey && @@ -992,7 +1097,7 @@ try { await loadCollectionItems(state.selectedCollectionKey); } catch (error) { - setStatus(elements.collectionStatus, error.message, true); + setStatus(elements.windowCollectionStatus, error.message, true); } } else { renderCollectionTree(); @@ -1000,11 +1105,12 @@ } } - function closeImportModal() { - state.isImportModalOpen = false; - elements.importModal.hidden = true; - elements.importModal.setAttribute("aria-hidden", "true"); + function closeImportWindow() { + state.isImportWindowOpen = false; + elements.importWindowShell.hidden = true; + elements.importWindowShell.setAttribute("aria-hidden", "true"); document.body.classList.toggle("modal-open", false); + cancelAbstractPreview(); } async function loadProjects() { @@ -1071,20 +1177,28 @@ elements.refreshProjectsButton.addEventListener("click", loadProjects); elements.openImportModalButton.addEventListener("click", () => { - openImportModal(); + openImportWindow(); }); - elements.closeImportModalButton.addEventListener("click", closeImportModal); - elements.importModalOverlay.addEventListener("click", closeImportModal); - elements.selectDescendantsButton.addEventListener("click", selectVisibleItems); - elements.clearSelectionButton.addEventListener("click", clearSelectedItems); - elements.importSelectedItemsButton.addEventListener("click", () => { + elements.windowCloseButton.addEventListener("click", closeImportWindow); + elements.windowMinimizeButton.addEventListener("click", minimizeWindow); + elements.windowMaximizeButton.addEventListener("click", maximizeWindow); + elements.windowRestoreButton.addEventListener("click", restoreWindow); + elements.importWindowMinimizedBar.addEventListener("click", (e) => { + if (e.target === elements.windowRestoreButton) return; + restoreWindow(); + }); + elements.windowToolbar.addEventListener("pointerdown", startWindowDrag); + document.addEventListener("pointermove", moveWindowDrag); + document.addEventListener("pointerup", endWindowDrag); + elements.windowClearSelectionButton.addEventListener("click", clearSelectedItems); + elements.windowImportSelectedItemsButton.addEventListener("click", () => { importSelectedItems().catch((error) => { - setStatus(elements.collectionStatus, error.message, true); + setStatus(elements.windowCollectionStatus, error.message, true); }); }); document.addEventListener("keydown", (event) => { - if (event.key === "Escape" && state.isImportModalOpen) { - closeImportModal(); + if (event.key === "Escape" && state.isImportWindowOpen) { + closeImportWindow(); } }); @@ -1139,7 +1253,7 @@ elements.recommendTab.addEventListener("click", () => activateTab("recommend")); elements.planTab.addEventListener("click", () => activateTab("plan")); - closeImportModal(); + closeImportWindow(); updateImportActionState(); loadProjects().catch((error) => { setStatus(elements.projectStatus, error.message, true);