You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
472 lines
13 KiB
472 lines
13 KiB
// ================================== |
|
// 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) { |
|
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 () { |
|
await removeAllBookmarks() |
|
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 () { |
|
let maxId = 0 |
|
const localBookmarks = await api.getBookmarkList() |
|
localBookmarks.forEach(bookmark => { |
|
if (parseInt(bookmark.id) > maxId) { |
|
maxId = parseInt(bookmark.id) |
|
} |
|
}) |
|
// 确保远程书签和本地书签的id不一致 |
|
let remoteBookmarks = await serverApi.downloadBookmarks() |
|
remoteBookmarks = remoteBookmarks.filter(bookmark => { |
|
if (parseInt(bookmark.id) > 2) { |
|
bookmark.id = maxId + parseInt(bookmark.id) + '' |
|
if (parseInt(bookmark.parentId) > 2) { |
|
bookmark.parentId = maxId + parseInt(bookmark.parentId) + '' |
|
} |
|
return true |
|
} |
|
return false |
|
}) |
|
|
|
const mergedBookmarks = [].concat(remoteBookmarks, localBookmarks) |
|
await removeAllBookmarks() |
|
// 按照树的层次结构合并同一层级的书签 |
|
// eslint-disable-next-line no-unused-vars |
|
function getNewbookmarkId (array, parentId) { |
|
for (const item of array) { |
|
if (item.id === parentId) { |
|
return item.newId |
|
} |
|
} |
|
} |
|
function getSameBookmark (bookmark, bookmarks) { |
|
for (const item of bookmarks) { |
|
if (item === undefined) { |
|
continue |
|
} |
|
if (item.deleted) { |
|
continue |
|
} |
|
// 同类型,同标题,同url判断为同一个书签 |
|
if ( |
|
item.id !== bookmark.id && |
|
item.title === bookmark.title && |
|
item.url === bookmark.url) { |
|
return item |
|
} |
|
} |
|
} |
|
const map = listToMap(mergedBookmarks) |
|
const maxDepth = Object.keys(map).length |
|
|
|
for (let depth = 0; depth < maxDepth; depth++) { |
|
const bookmarks = map[depth] |
|
for (const index in bookmarks) { |
|
const bookmark = bookmarks[index] |
|
// 第0层应该自动过滤 |
|
if (parseInt(bookmark.id) < 3) { |
|
bookmark.newId = bookmark.id |
|
continue |
|
} |
|
const sameBookmark = getSameBookmark(bookmark, bookmarks) |
|
if (sameBookmark) { |
|
// 书签组 |
|
if (bookmark.url === undefined) { |
|
if (depth + 1 < maxDepth) { |
|
// 叶子节点中所有节点挂在同类书签上 |
|
for (const item of map[depth + 1]) { |
|
if (item.parentId === bookmark.id) { |
|
item.parentId = sameBookmark.id |
|
} |
|
} |
|
} |
|
} |
|
// 标记为删除 |
|
bookmark.deleted = true |
|
} else { |
|
bookmark.deleted = false |
|
} |
|
map[depth] = map[depth].filter(bookmark => { |
|
return !bookmark.deleted |
|
}) |
|
} |
|
} |
|
|
|
for (let depth = 0; depth < maxDepth; depth++) { |
|
const bookmarks = map[depth] |
|
for (const index in bookmarks) { |
|
const bookmark = bookmarks[index] |
|
if (parseInt(bookmark.id) < 3) { |
|
bookmark.newId = bookmark.id |
|
continue |
|
} |
|
// 重新计算index |
|
bookmark.index = parseInt(index) |
|
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.title} error`) |
|
console.error(error) |
|
} |
|
} |
|
} |
|
} |
|
|
|
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')
|
|
|