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);