// ================================== // dom action helper const domHelper = { loadScript (url) { var script = document.createElement('script') script.type = 'text/javacript' script.src = url document.body.appendChild(script) }, loadStyle (url) { var sytle = document.createElement('link') sytle.rel = 'stylesheet' sytle.type = 'text/css' sytle.href = url sytle.media = 'screen' var headobj = document.getElementsByTagName('head')[0] headobj.appendChild(sytle) } } // ================================== // chrome bookmark api wrapper const api = { /** * 获得整个书签树 */ getTreeAsync () { return new Promise(resolve => { chrome.bookmarks.getTree(resolve) }) }, /** * 获得特定父书签组下的所有子书签和子书签组, * 返回的书签数组中是不包含children字段的,即不包含子节点以下的节点 * @param {String} id 父书签组id */ getChildrenAsync (id) { return new Promise(resolve => { chrome.bookmarks.getChildren(id, resolve) }) }, /** * 获得特定书签组下的所有书签, * 返回的书签数组中包含children字段,即包含子节点以下的节点 * @param {String} id 父书签组id */ getSubTreeAsync (id) { return new Promise(resolve => { chrome.bookmarks.getSubTree(id, resolve) }) }, /** * 删除指定id的书签 * @param {String} id 需要删除的书签的id */ removeAsync (id) { return new Promise(resolve => { chrome.bookmarks.remove(id, resolve) }) }, /** * 删除指定id的空书签组,如果书签组下有子书签或子书签组,删除将失败 * @param {String} id 需要删除的书签文件夹id */ removeTreeAsync (id) { return new Promise(resolve => { chrome.bookmarks.removeTree(id, resolve) }) }, /** * 创建一个书签 * @param {Object} bookmark * @param {String} (optional) bookmark.parentId 父书签组,如果不填,则默认在**其他书签**一栏中 * @param {Number} (optional) bookmark.index * @param {String} bookmark.title * @param {String} (optional) bookmark.url 如果为NULL或者不填,则代表一个书签组文件夹 */ createAsync (bookmark) { return new Promise(resolve => { chrome.bookmarks.create(bookmark, resolve) }) }, /** * 获得浏览器书签的数组 */ async getBookmarkList () { function tree2List (tree) { const cacheMap = {} function add2Map (tree) { for (const item of tree) { // 给书签文件创建group属性 if ( parseInt(item.id) < 3 || typeof item.dateGroupModified === 'number' ) { item.group = true } cacheMap[item.id] = item if (Array.isArray(item.children)) { add2Map(item.children) } } } add2Map(tree) for (const key in cacheMap) { const item = cacheMap[key] if (typeof item.parentId === 'string') { if (!Array.isArray(cacheMap[item.parentId].nodes)) { cacheMap[item.parentId].nodes = [] } cacheMap[item.parentId].nodes.push(item.id) } } const list = [] for (const key in cacheMap) { const item = cacheMap[key] list.push({ id: item.id, parentId: item.parentId, title: item.title, url: item.url, index: item.index, group: item.group, nodes: item.nodes }) } return list } const localTree = await api.getTreeAsync() return tree2List(localTree) } } // ================================== // backend server api wrapper const serverApi = (function () { const API_VERSION = 'v1' let serverAddress = '' const setServerAddress = (address) => { serverAddress = address } const getServerAddress = () => { return serverAddress } const getVersion = () => { return new Promise((resolve, reject) => { const xhttp = new XMLHttpRequest() xhttp.onreadystatechange = function () { if (this.readyState === 4 && this.status === 200) { const result = JSON.parse(this.responseText) resolve(result.data) } } xhttp.open('GET', `${serverAddress}/${API_VERSION}/version`, true) xhttp.send() }) } const uploadBookmarks = (data) => { return new Promise((resolve, reject) => { const xhttp = new XMLHttpRequest() xhttp.open('POST', `${serverAddress}/${API_VERSION}/bookmark`, true) xhttp.setRequestHeader('content-type', 'application/json') xhttp.onreadystatechange = function () { if (this.readyState === 4 && this.status === 200) { const result = JSON.parse(this.responseText) resolve(result.data) } } // 将用户输入值序列化成字符串 xhttp.send(JSON.stringify(data)) }) } const downloadBookmarks = () => { return new Promise((resolve, reject) => { const xhttp = new XMLHttpRequest() xhttp.open('GET', `${serverAddress}/${API_VERSION}/bookmark`, true) xhttp.onreadystatechange = function () { if (this.readyState === 4 && this.status === 200) { const result = JSON.parse(this.responseText) resolve(result.data) } } xhttp.send() }) } return { setServerAddress, getServerAddress, getVersion, uploadBookmarks, downloadBookmarks } })() // ================================== // starter const PROTO = 'http' const SERVER_ADDRESS = '192.168.31.238' const SERVER_PORT = '3000' const SERVER_URL = `${PROTO}://${SERVER_ADDRESS}:${SERVER_PORT}` /** * 数组转map表 * @param {Object} bookmarkList */ function listToMap (bookmarkList) { const map = {} for (let i = 0; i < bookmarkList.length; i++) { const bookmark = bookmarkList[i] map[bookmark.id] = bookmark } function getDepth (bookmark, depth) { if (parseInt(bookmark.id) === 0) { return depth } else { return getDepth(map[bookmark.parentId], depth + 1) } } const finalMap = {} for (let i = 0; i < bookmarkList.length; i++) { const bookmark = bookmarkList[i] const finalDepth = getDepth(bookmark, 0) if (!Array.isArray(finalMap[finalDepth])) { finalMap[finalDepth] = [] } finalMap[finalDepth].push(bookmark) } return finalMap } async function removeAllBookmarks () { async function removeBookmark (item) { // 不可删除节点 if (parseInt(item.id) <= 2) { return } try { if (Array.isArray(item.children)) { await api.removeTreeAsync(item.id) } else { await api.removeAsync(item.id) } console.log(`remove bookmark "${item.title}" success`) } catch (err) { console.error(`remove bookmark "${item.title}" error`) console.error(err) } } async function dfsRemove (tree) { if (!Array.isArray(tree)) { throw new Error('tree should be an array') } for (const item of tree) { console.log(`checking item ${item.title}`) if (Array.isArray(item.children) && item.children.length > 0) { // 根节点 await dfsRemove(item.children) } await removeBookmark(item) } } const localTree = await api.getTreeAsync() await dfsRemove(localTree) } /** * 覆盖同步 */ async function restoreByOverwrite () { const remoteBookmarkArray = await serverApi.downloadBookmarks() function getNewbookmarkId (array, parentId) { for (const item of array) { if (item.id === parentId) { return item.newId } } } const map = listToMap(remoteBookmarkArray) const maxDepth = Object.keys(map).length for (let depth = 0; depth < maxDepth; depth++) { const bookmarks = map[depth] for (const bookmark of bookmarks) { // 第0层应该自动过滤 if (parseInt(bookmark.id) < 3) { bookmark.newId = bookmark.id continue } try { const newParentId = getNewbookmarkId(map[depth - 1], bookmark.parentId) const newBookmark = await api.createAsync({ parentId: newParentId, index: bookmark.index, title: bookmark.title, url: bookmark.url }) bookmark.newId = newBookmark.id } catch (error) { console.error(`create bookmark ${bookmark.titel} error`) console.error(error) } } } } /** * 合并同步 */ async function restoreByMerge () { const remoteBookmarks = await serverApi.downloadBookmarks() const localBookmarks = await api.getBookmarkList() console.log(remoteBookmarks) console.log(localBookmarks) } const connectButton = document.getElementById('connect') if (connectButton) { connectButton.addEventListener('click', async () => { const serverAddress = document.getElementById('server').value || SERVER_URL serverApi.setServerAddress(serverAddress) const version = await serverApi.getVersion() document.getElementById('version').innerHTML = version }) } const uploadButton = document.getElementById('upload') if (uploadButton) { uploadButton.addEventListener('click', async () => { const bookmarks = await api.getBookmarkList() serverApi.uploadBookmarks({ bookmarks }) }) } const overwriteSyncButton = document.getElementById('overwriteSync') if (overwriteSyncButton) { overwriteSyncButton.addEventListener('click', async () => { return restoreByOverwrite() }) } const mergeSyncButton = document.getElementById('mergeSync') if (mergeSyncButton) { mergeSyncButton.addEventListener('click', async () => { return restoreByMerge() }) } const removeButton = document.getElementById('removeAll') if (removeButton) { removeButton.addEventListener('click', async () => { await removeAllBookmarks() const bookmarkArray = await api.getBookmarkList() if (Array.isArray(bookmarkArray) && bookmarkArray.length === 3) { console.log('clear bookmarks success') } else { console.error('clear bookmarks error') } }) } domHelper.loadScript('./js/test.js') domHelper.loadStyle('./css/normalize.css') domHelper.loadStyle('./css/style.css')