|
|
|
// ==================================
|
|
|
|
// 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')
|