重构:提交全新项目代码

This commit is contained in:
2026-05-04 00:04:03 +08:00
commit bca6c31df7
100 changed files with 5524 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
{
"name": "defgov-bookmark-sync",
"version": "1.0",
"manifest_version": 3,
"permissions": ["bookmarks", "storage"],
"chrome_url_overrides": {
"newtab": "newtab.html"
},
"host_permissions": ["<all_urls>"],
"background": {
"service_worker": "controller.js"
},
"browser_specific_settings": {
"gecko": {
"id": "bookmark-sync@example.com",
"strict_min_version": "109.0"
}
}
}

View File

@@ -0,0 +1,15 @@
{
"name": "defgov-bookmark-sync",
"version": "0.0.0",
"private": true,
"dependencies": {
"typescript": "^6.0.3"
},
"devDependencies": {
"@types/chrome": "^0.1.40",
"@types/firefox-webext-browser": "^143.0.0",
"@types/node": "^25.6.0",
"@types/webextension-polyfill": "^0.12.5",
"webextension-polyfill": "^0.12.0"
}
}

View File

@@ -0,0 +1,79 @@
import browser from "webextension-polyfill";
export class Controller {
/**
* 书签栏 (Bookmarks Bar) 固定ID为 "1"
*
* 其他书签 (Other Bookmarks) 固定ID为 "2"
*
* 移动设备书签 (Mobile Bookmarks) 固定ID为 "3"
*/
rootFolder: string;
constructor(rootFolder: string = "1") {
this.rootFolder = rootFolder;
}
init() {}
setRootFolder(rootFolder: string) {
this.rootFolder = rootFolder;
}
async createBookmark(parentId: string, title: string, url?: string) {
try {
await browser.bookmarks.create({
parentId: parentId,
title: title,
url: url,
type: "bookmark",
});
} catch (error) {
console.error("❌ 创建书签失败:", error);
}
}
async createFolder(parentId: string, title: string, url?: string) {
try {
await browser.bookmarks.create({
parentId: parentId,
title: title,
url: url,
type: "folder",
});
} catch (error) {
console.error(error);
}
}
async createSeparator(parentId: string) {
try {
await browser.bookmarks.create({
parentId: parentId,
type: "separator",
});
} catch (error) {
console.error(error);
}
}
async deleteNode(id: string) {
const results = await browser.bookmarks.get(id);
if (results[0].type === "bookmark" || results[0].type === "separator") {
deleteBookmarkOrSeparator(id);
} else if (results[0].type === "folder") {
deleteFolder(id);
}
}
async moveNode(id: string, destinationId: string, index: number) {
try {
await browser.bookmarks.move(id, {
parentId: destinationId,
index: index,
});
} catch (error) {
console.error(error);
}
}
}

View File

@@ -0,0 +1,47 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>我的自定义书签</title>
<style>
body {
font-family: sans-serif;
padding: 20px;
background: #f5f5f5;
}
#bookmark-container {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 20px;
}
.bookmark-item {
display: block;
width: 120px;
padding: 15px;
background: white;
border-radius: 8px;
text-decoration: none;
color: #333;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
text-align: center;
}
.bookmark-item:hover {
background: #e0e0e0;
}
</style>
</head>
<body>
<h2>我的书签管理器</h2>
<!-- 文件夹切换下拉框 -->
<select id="folder-selector">
<option value="1">书签栏 (ID: 1)</option>
<!-- 其他文件夹可以通过代码动态加载到这里 -->
</select>
<!-- 书签渲染容器 -->
<div id="bookmark-container"></div>
<script src="popup.js"></script>
</body>
</html>

View File

@@ -0,0 +1,76 @@
// 递归提取某个节点下的所有书签 URL防止文件夹嵌套
function extractUrls(
bookmarkNode: chrome.bookmarks.BookmarkTreeNode[],
): string[] {
let urls: string[] = [];
for (const node of bookmarkNode) {
if (node.url) {
urls.push(node.url);
} else if (node.children) {
urls = urls.concat(extractUrls(node.children));
}
}
return urls;
}
// 渲染指定 ID 文件夹下的书签
async function renderBookmarksByFolderId(folderId: string) {
try {
const results = await chrome.bookmarks.getSubTree(folderId);
const folderNode = results[0];
if (!folderNode || !folderNode.children) return;
const container = document.getElementById("bookmark-container");
if (container) container.innerHTML = "";
// 遍历直接子节点
folderNode.children.forEach((bookmark) => {
if (bookmark.url) {
const link = document.createElement("a");
link.className = "bookmark-item";
link.href = bookmark.url;
link.textContent = bookmark.title || bookmark.url;
// 自动获取网站 favicon 图标
const faviconUrl = `https://www.google.com/s2/favicons?domain=${new URL(bookmark.url).hostname}&sz=32`;
link.innerHTML = `<img src="${faviconUrl}" style="vertical-align: middle; margin-right: 5px;">${bookmark.title}`;
container?.appendChild(link);
}
});
} catch (error) {
console.error("获取书签失败:", error);
}
}
// 初始化:自动获取所有一级文件夹并填充到下拉框
async function initFolderSelector() {
const results = await chrome.bookmarks.getTree();
const bookmarkBar = results[0].children?.[0]; // ID为1的书签栏
const selector = document.getElementById(
"folder-selector",
) as HTMLSelectElement;
if (bookmarkBar && bookmarkBar.children) {
bookmarkBar.children.forEach((folder) => {
// 只把文件夹加到下拉框里
if (!folder.url && folder.id !== "1") {
const option = document.createElement("option");
option.value = folder.id || "";
option.textContent = folder.title || "未命名文件夹";
selector.appendChild(option);
}
});
}
// 绑定切换事件
selector.addEventListener("change", (e) => {
const targetId = (e.target as HTMLSelectElement).value;
renderBookmarksByFolderId(targetId);
});
// 默认渲染书签栏
renderBookmarksByFolderId("1");
}
// 页面加载完成后启动
document.addEventListener("DOMContentLoaded", initFolderSelector);

View File

@@ -0,0 +1,5 @@
import browser from "webextension-polyfill";
export type Account = { id: string; email: string; password: string };
export type BookmarkTreeNode = browser.Bookmarks.BookmarkTreeNode;

View File

@@ -0,0 +1,20 @@
async function getLastIndex(parentId: string) {
const children = await browser.bookmarks.getChildren(parentId);
return children.length > 0 ? children[children.length - 1].index : 0;
}
async function deleteBookmarkOrSeparator(id: string) {
try {
await browser.bookmarks.remove(id);
} catch (error) {
console.error(error);
}
}
async function deleteFolder(id: string) {
try {
await browser.bookmarks.removeTree(id);
} catch (error) {
console.error(error);
}
}

View File

@@ -0,0 +1,118 @@
{
// tsconfig.lib.json
// 直接复制本文件内容到子项目的 tsconfig.json 即可,不要用 entends 继承本文件
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/**
* Browser api "DOM""DOM.Iterable"
* Node api "ES2025"使
* NextJs apiserver DOMurl "ES2025", "DOM", "DOM.Iterable"
*/
"lib": ["ES2025", "DOM", "DOM.Iterable"],
/**
* React JSX
* - 使 React 17+ JSX Transform
* - import React
*/
"jsx": "react-jsx",
/**
*
* - tsc / tsc -b
*/
"outDir": "./dist",
/**
*
* - dist src
* - declaration
*/
"rootDir": "./src",
/**
* .d.ts
* - / npm
* -
*/
"declaration": true,
/**
* JS
* - Vite / Next / Nuxt bundler
* - tsc emit
*/
"noEmit": true,
/**
*
* - esbuild / SWC / bundler
* - enum / namespace
*/
"isolatedModules": true,
/**
* import 使 .ts / .tsx
* - Node ESM / bundler
* - `import './foo'` TS + ESM
*/
"allowImportingTsExtensions": true
},
/**
*
* - src
* - exclude
*/
"include": ["src", "scripts"],
/**
*
* -
* - dist / test / config
* -
*/
"exclude": [
"scripts",
"node_modules",
"dist",
// ---------- build / cache ----------
".turbo/**/*",
".cache/**/*",
".vite/**/*",
// ---------- 配置文件 ----------
"vite.config.ts",
"*.config.ts",
"*.config.js",
"tsconfig.*.json",
// ---------- 测试相关 ----------
"__tests__/**/*",
"test/**/*",
"tests/**/*",
"**/*.test.ts",
"**/*.test.tsx",
"**/*.spec.ts",
"**/*.spec.tsx",
// ---------- Storybook ----------
".storybook/**/*",
"stories/**/*",
// ---------- 示例 / 脚本 ----------
"example/**/*",
"examples/**/*",
"scripts/**/*",
// ---------- 环境与静态资源 ----------
".env",
".env.*",
"public/**/*",
// ---------- 文档 ----------
"docs/**/*",
"README.md",
"LICENSE"
]
}