加入导入书签功能

This commit is contained in:
yinjun.chen1
2026-09-16 17:31:26 +08:00
parent dac4717ae9
commit abfcec734b
6 changed files with 302 additions and 2 deletions
+28
View File
@@ -798,5 +798,33 @@
"logGetLockStatusFail": { "logGetLockStatusFail": {
"message": "Failed to get lock status", "message": "Failed to get lock status",
"description": "Log - failed to get lock status" "description": "Log - failed to get lock status"
},
"importBookmarkBtnTitle": {
"message": "Import from bookmarks",
"description": "Bookmark import button title"
},
"bookmarkImportTitle": {
"message": "Import from bookmarks",
"description": "Bookmark import dialog title"
},
"bookmarkSelectAll": {
"message": "Select all",
"description": "Bookmark select all"
},
"bookmarkSearchPlaceholder": {
"message": "Search bookmarks",
"description": "Bookmark search placeholder"
},
"bookmarkImportBtn": {
"message": "Import selected",
"description": "Bookmark import confirm button"
},
"bookmarkLoadFail": {
"message": "Failed to load bookmarks",
"description": "Toast - bookmark load failed"
},
"toastBookmarkImported": {
"message": "Imported $1, skipped $2",
"description": "Toast - bookmark import result, $1 imported, $2 skipped"
} }
} }
+28
View File
@@ -798,5 +798,33 @@
"logGetLockStatusFail": { "logGetLockStatusFail": {
"message": "获取锁状态失败", "message": "获取锁状态失败",
"description": "日志 - 获取锁状态失败" "description": "日志 - 获取锁状态失败"
},
"importBookmarkBtnTitle": {
"message": "从书签导入",
"description": "书签导入按钮 title"
},
"bookmarkImportTitle": {
"message": "从书签导入",
"description": "书签导入弹窗标题"
},
"bookmarkSelectAll": {
"message": "全选",
"description": "书签全选"
},
"bookmarkSearchPlaceholder": {
"message": "搜索书签",
"description": "书签搜索框 placeholder"
},
"bookmarkImportBtn": {
"message": "导入选中",
"description": "书签导入确认按钮"
},
"bookmarkLoadFail": {
"message": "书签加载失败",
"description": "toast - 书签加载失败"
},
"toastBookmarkImported": {
"message": "已导入 $1 条,跳过 $2 条",
"description": "toast - 书签导入结果,$1 导入数,$2 跳过数"
} }
} }
+1 -1
View File
@@ -4,7 +4,7 @@
"version": "1.0.0", "version": "1.0.0",
"description": "__MSG_extDescription__", "description": "__MSG_extDescription__",
"default_locale": "zh_CN", "default_locale": "zh_CN",
"permissions": ["storage", "activeTab", "scripting", "tabs", "sidePanel"], "permissions": ["storage", "activeTab", "scripting", "tabs", "sidePanel", "bookmarks"],
"host_permissions": ["<all_urls>"], "host_permissions": ["<all_urls>"],
"action": { "action": {
"default_icon": { "default_icon": {
+82
View File
@@ -1154,3 +1154,85 @@ h2 {
.strength-info #crackTime { .strength-info #crackTime {
color: #888; color: #888;
} }
/* 书签导入对话框 */
.bookmark-modal-content {
max-width: 420px;
}
.bookmark-toolbar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.bookmark-select-all {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #e0e0e0;
cursor: pointer;
white-space: nowrap;
}
.bookmark-select-all input[type="checkbox"] {
width: auto;
}
.bookmark-toolbar input[type="text"] {
flex: 1;
padding: 4px 8px;
font-size: 12px;
background: #0f1b35;
border: 1px solid #0f3460;
border-radius: 4px;
color: #e0e0e0;
}
.bookmark-list {
max-height: 320px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 2px;
margin-bottom: 8px;
}
.bookmark-item {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 8px;
border-radius: 4px;
cursor: pointer;
}
.bookmark-item:hover {
background: rgba(15, 52, 96, 0.5);
}
.bookmark-item input[type="checkbox"] {
width: auto;
flex-shrink: 0;
}
.bookmark-item-text {
font-size: 12px;
color: #e0e0e0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.bookmark-item-url {
font-size: 10px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 1;
min-width: 0;
}
+23 -1
View File
@@ -124,7 +124,7 @@
<section class="list-section" id="listSection"> <section class="list-section" id="listSection">
<div class="list-header"> <div class="list-header">
<h2 data-i18n="configuredDomainsTitle">已配置域名</h2>
<div class="list-actions"> <div class="list-actions">
<button type="button" id="searchBtn" class="btn-io btn-icon" data-i18n-title="searchBtnTitle" title="搜索"> <button type="button" id="searchBtn" class="btn-io btn-icon" data-i18n-title="searchBtnTitle" title="搜索">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
@@ -142,6 +142,9 @@
<button type="button" id="importBtn" class="btn-io btn-icon" data-i18n-title="importBtn" title="导入"> <button type="button" id="importBtn" class="btn-io btn-icon" data-i18n-title="importBtn" title="导入">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21V9"/><path d="M7 16l5 5 5-5"/><path d="M5 7V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v2"/></svg> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21V9"/><path d="M7 16l5 5 5-5"/><path d="M5 7V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v2"/></svg>
</button> </button>
<button type="button" id="importBookmarkBtn" class="btn-io btn-icon" data-i18n-title="importBookmarkBtnTitle" title="从书签导入">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
</button>
<input type="file" id="importFile" accept="application/json,.json" hidden> <input type="file" id="importFile" accept="application/json,.json" hidden>
</div> </div>
</div> </div>
@@ -310,6 +313,25 @@
</div> </div>
</div> </div>
<!-- 从书签导入对话框 -->
<div id="bookmarkModal" class="modal">
<div class="modal-content bookmark-modal-content">
<h3 data-i18n="bookmarkImportTitle">从书签导入</h3>
<div class="bookmark-toolbar">
<label class="bookmark-select-all">
<input type="checkbox" id="bookmarkSelectAll">
<span data-i18n="bookmarkSelectAll">全选</span>
</label>
<input type="text" id="bookmarkSearch" data-i18n-placeholder="bookmarkSearchPlaceholder" placeholder="搜索书签">
</div>
<div id="bookmarkList" class="bookmark-list"></div>
<div class="modal-actions">
<button type="button" id="bookmarkCancel" class="btn-edit" data-i18n="cancelBtn">取消</button>
<button type="button" id="bookmarkConfirm" class="btn-primary" data-i18n="bookmarkImportBtn">导入选中</button>
</div>
</div>
</div>
<script src="utils.js"></script> <script src="utils.js"></script>
<script src="env-store.js"></script> <script src="env-store.js"></script>
<script src="crypto-store.js"></script> <script src="crypto-store.js"></script>
+140
View File
@@ -66,6 +66,13 @@ document.addEventListener("DOMContentLoaded", () => {
const exportBtn = $("exportBtn"); const exportBtn = $("exportBtn");
const importBtn = $("importBtn"); const importBtn = $("importBtn");
const importFile = $("importFile"); const importFile = $("importFile");
const importBookmarkBtn = $("importBookmarkBtn");
const bookmarkModal = $("bookmarkModal");
const bookmarkList = $("bookmarkList");
const bookmarkSelectAll = $("bookmarkSelectAll");
const bookmarkSearch = $("bookmarkSearch");
const bookmarkCancel = $("bookmarkCancel");
const bookmarkConfirm = $("bookmarkConfirm");
const toast = $("toast"); const toast = $("toast");
// 编辑配置弹窗 // 编辑配置弹窗
@@ -1309,6 +1316,139 @@ document.addEventListener("DOMContentLoaded", () => {
} }
}); });
// 从书签导入
function flattenBookmarks(nodes, out) {
for (const node of nodes) {
if (node.url) {
out.push({ title: node.title || node.url, url: node.url });
}
if (node.children) {
flattenBookmarks(node.children, out);
}
}
}
function urlToDomain(url) {
try {
const u = new URL(url);
let domain = u.host + u.pathname;
if (domain.endsWith("/") && u.pathname !== "/") domain = domain.replace(/\/$/, "");
if (u.pathname === "/") domain = u.host;
return domain;
} catch (e) {
return null;
}
}
let bookmarkItems = [];
function renderBookmarkList(filter) {
const kw = (filter || "").trim().toLowerCase();
bookmarkList.innerHTML = "";
const items = kw
? bookmarkItems.filter((b) => b.title.toLowerCase().includes(kw) || b.url.toLowerCase().includes(kw))
: bookmarkItems;
const frag = document.createDocumentFragment();
for (const b of items) {
const row = document.createElement("label");
row.className = "bookmark-item";
const cb = document.createElement("input");
cb.type = "checkbox";
cb.dataset.url = b.url;
const text = document.createElement("span");
text.className = "bookmark-item-text";
text.textContent = b.title;
const sub = document.createElement("span");
sub.className = "bookmark-item-url";
sub.textContent = b.url;
row.appendChild(cb);
row.appendChild(text);
row.appendChild(sub);
frag.appendChild(row);
}
bookmarkList.appendChild(frag);
updateBookmarkSelectAllState();
}
function updateBookmarkSelectAllState() {
const cbs = bookmarkList.querySelectorAll("input[type=checkbox]");
const checked = bookmarkList.querySelectorAll("input[type=checkbox]:checked");
bookmarkSelectAll.checked = cbs.length > 0 && cbs.length === checked.length;
bookmarkConfirm.disabled = checked.length === 0;
}
importBookmarkBtn.addEventListener("click", async () => {
try {
const tree = await chrome.bookmarks.getTree();
bookmarkItems = [];
flattenBookmarks(tree, bookmarkItems);
bookmarkSearch.value = "";
bookmarkSelectAll.checked = false;
renderBookmarkList("");
bookmarkModal.classList.add("show");
} catch (e) {
showToast(t("bookmarkLoadFail"));
}
});
bookmarkSearch.addEventListener("input", () => {
renderBookmarkList(bookmarkSearch.value);
});
bookmarkList.addEventListener("change", () => {
updateBookmarkSelectAllState();
});
bookmarkSelectAll.addEventListener("change", () => {
const cbs = bookmarkList.querySelectorAll("input[type=checkbox]");
cbs.forEach((cb) => (cb.checked = bookmarkSelectAll.checked));
bookmarkConfirm.disabled = !bookmarkSelectAll.checked;
});
bookmarkCancel.addEventListener("click", () => {
bookmarkModal.classList.remove("show");
});
bookmarkModal.addEventListener("click", (e) => {
if (e.target === bookmarkModal) bookmarkModal.classList.remove("show");
});
bookmarkConfirm.addEventListener("click", async () => {
const checked = bookmarkList.querySelectorAll("input[type=checkbox]:checked");
if (!checked.length) return;
const configs = await loadConfigs();
const existingDomains = new Set(configs.map((c) => c.domain));
let added = 0;
let skipped = 0;
for (const cb of checked) {
const domain = urlToDomain(cb.dataset.url);
if (!domain || existingDomains.has(domain)) {
skipped++;
continue;
}
existingDomains.add(domain);
configs.push({
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 8),
domain,
alias: cb.parentElement.querySelector(".bookmark-item-text").textContent || null,
username: "",
password: "",
autoSubmit: true,
usernameSelector: null,
passwordSelector: null,
submitSelector: null,
enabled: true,
delayMs: null,
});
added++;
}
await saveConfigs(configs);
bookmarkModal.classList.remove("show");
renderConfigList();
renderEnvSelect();
showToast(t("toastBookmarkImported", String(added), String(skipped)));
});
// 消息发送:SW 刚休眠唤醒时首次 sendMessage 会失败,需重试 // 消息发送:SW 刚休眠唤醒时首次 sendMessage 会失败,需重试
function sendMessage(message, retries = 3) { function sendMessage(message, retries = 3) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {