加入导入书签功能
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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": {
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user