一键同步,无处不在的书签体验:探索多电脑Chrome书签同步插件

说在前面

平时大家都是怎么管理自己的浏览器书签数据的呢?有没有过公司和家里的电脑浏览器书签不同步的情况?有没有过电脑突然坏了但书签数据没有导出,导致书签数据丢失了?解决这些问题的方法有很多,我选择自己写个chrome插件来做书签同步。

实现方案

通过 gitee 来做存取

建一个私有仓库来保存自己的书签目录信息,需要同步的时候再获取 gitee 仓库的书签目录到本地。这样不用自己写服务端对数据进行存储,减少了很多不必要的开发工作。

实现步骤

一、准备工作

1、新建 gitee 仓库

直接在gitee上新建仓库即可。

我们不想要书签信息公开,所以选择勾选上私有:
image.png

创建完的初始仓库是这样的:
image.png

我们再新增一个目录,用于存放和书签相关的文件:
image.png

在该目录下新增一个文件,用于保存书签导出的数据:
image.png

二、插件编写

完成前面的准备工作,新建完 gitee 仓库之后,我们便可以正式开始进行插件的编写了。

1、插件模板
  • 安装依赖jyeontu
npm i -g jyeontu
  • 获取模板
jyeontu create

image.png

  • 生成模板

根据提示输入相关信息即可

image.png

image.png

2、giteeAPI

我们可以通过 giteeAPI 来对 gitee 仓库进行操作,下面是 giteeAPI 的操作文档:
https://gitee.com/api/v5/swagger#/getV5ReposOwnerRepoStargazers?ex=no

获取gitee指定文件的内容

我们可以通过下面代码来获取到gitee指定仓库指定文件的内容:

async function fetchFileContent(apiUrl, accessToken) {const response = await fetch(apiUrl, {headers: {Authorization: "token " + accessToken,},});const fileData = await response.json();return fileData.content;
}export async function getFile(gitInfo) {const accessToken = gitInfo.token;const apiUrl ="https://gitee.com/api/v5/repos/" +gitInfo.owner +"/" +gitInfo.repo +"/contents/" +gitInfo.filePath;const fileContent = await fetchFileContent(apiUrl, accessToken);const decodedContent = atob(fileContent); // 解码Base64编码的文件内容const decoder = new TextDecoder();const decodedData = decoder.decode(new Uint8Array([...decodedContent].map((char) => char.charCodeAt(0))));return JSON.parse(decodedData);
}
修改指定文件的内容数据

我们需要先获取到文件,拿到文件的sha值,后面通过sha来对文件进行编辑操作。
btoa函数只能处理Latin1字符范围内的字符串,对超出Latin1字符范围的字符串进行Base64编码,我们需要进行以下操作,使用TextEncoder对象来将字符串转换为字节数组,然后再进行Base64编码。

async function fetchFileContent(apiUrl, accessToken) {const response = await fetch(apiUrl, {headers: {Authorization: "token " + accessToken,},});const fileData = await response.json();return fileData.content;
}
async function getDecodedContent(content) {const decodedContent = atob(content); // 解码Base64编码的文件内容const decoder = new TextDecoder();const decodedData = decoder.decode(new Uint8Array([...decodedContent].map((char) => char.charCodeAt(0))));return JSON.parse(decodedData);
}
async function putFileContent(apiUrl, accessToken, encodedContent, sha) {const commitData = {access_token: accessToken,content: encodedContent,message: "Modified file",sha: sha,};const putResponse = await fetch(apiUrl, {method: "PUT",headers: {"Content-Type": "application/json",Authorization: "token " + accessToken,},body: JSON.stringify(commitData),});if (putResponse.ok) {console.log("File modified successfully.");} else {console.error("Failed to modify file.");}
}
export async function modifyFile(gitInfo, modifiedContent) {const accessToken = gitInfo.token;const apiUrl ="https://gitee.com/api/v5/repos/" +gitInfo.owner +"/" +gitInfo.repo +"/contents/" +gitInfo.filePath;try {const fileContent = await fetchFileContent(apiUrl, accessToken);const content = await getDecodedContent(fileContent);modifiedContent = mergeBookmarks(content, modifiedContent);modifiedContent = JSON.stringify(modifiedContent);const encoder = new TextEncoder();const data = encoder.encode(modifiedContent);const encodedContent = btoa(String.fromCharCode.apply(null, new Uint8Array(data)));await putFileContent(apiUrl, accessToken, encodedContent, fileContent.sha);} catch (error) {console.error("An error occurred:", error);}
}
3、indexDb存取

我们不希望每次打开都需要去重新填写gitee仓库的相关信息,所以这里我们使用indexDb来对gitee仓库的相关信息做一个保存。

export class IndexedDB {constructor(databaseName, storeName) {this.databaseName = databaseName;this.storeName = storeName;this.db = null;}open() {return new Promise((resolve, reject) => {const request = window.indexedDB.open(this.databaseName);request.onerror = () => {reject(new Error("Failed to open database"));};request.onsuccess = () => {this.db = request.result;resolve();};request.onupgradeneeded = (event) => {this.db = event.target.result;if (!this.db.objectStoreNames.contains(this.storeName)) {this.db.createObjectStore(this.storeName, {keyPath: "id",autoIncrement: true,});}};});}createDatabase() {return new Promise((resolve, reject) => {const request = window.indexedDB.open(this.databaseName);request.onerror = () => {reject(new Error("Failed to create database"));};request.onsuccess = () => {this.db = request.result;this.db.close();resolve();};request.onupgradeneeded = (event) => {this.db = event.target.result;if (!this.db.objectStoreNames.contains(this.storeName)) {this.db.createObjectStore(this.storeName, {keyPath: "id",autoIncrement: true,});}this.db.close();resolve();};});}close() {if (this.db) {this.db.close();this.db = null;}}add(data) {return new Promise((resolve, reject) => {const transaction = this.db.transaction(this.storeName, "readwrite");const objectStore = transaction.objectStore(this.storeName);const request = objectStore.add(data);request.onsuccess = () => {resolve(request.result);};request.onerror = () => {reject(new Error("Failed to add data"));};});}getAll() {return new Promise((resolve, reject) => {const transaction = this.db.transaction(this.storeName, "readonly");const objectStore = transaction.objectStore(this.storeName);const request = objectStore.getAll();request.onsuccess = () => {resolve(request.result);};request.onerror = () => {reject(new Error("Failed to get data"));};});}getById(id) {return new Promise((resolve, reject) => {const transaction = this.db.transaction(this.storeName, "readonly");const objectStore = transaction.objectStore(this.storeName);const request = objectStore.get(id);request.onsuccess = () => {resolve(request.result);};request.onerror = () => {reject(new Error("Failed to get data"));};});}delete(id) {return new Promise((resolve, reject) => {const transaction = this.db.transaction(this.storeName, "readwrite");const objectStore = transaction.objectStore(this.storeName);const request = objectStore.delete(id);request.onsuccess = () => {resolve();};request.onerror = () => {reject(new Error("Failed to delete data"));};});}update(id, newData) {return new Promise((resolve, reject) => {const transaction = this.db.transaction(this.storeName, "readwrite");const objectStore = transaction.objectStore(this.storeName);const getRequest = objectStore.get(id);getRequest.onsuccess = () => {const oldData = getRequest.result;if (!oldData) {const addRequest = objectStore.add({ ...newData, id });addRequest.onsuccess = () => {resolve({ ...newData, id });};addRequest.onerror = () => {reject(new Error("Failed to add data"));};} else {const mergedData = { ...oldData, ...newData };const putRequest = objectStore.put(mergedData);putRequest.onsuccess = () => {resolve(mergedData);};putRequest.onerror = () => {reject(new Error("Failed to update data"));};}};getRequest.onerror = () => {reject(new Error("Failed to get data"));};});}
}
4、书签存取
获取chrome书签

要获取 Chrome 浏览器的书签目录,我们可以使用 Chrome 浏览器提供的 API——chrome.bookmarks。下面是一个示例代码,演示如何使用chrome.bookmarks API 获取 Chrome 浏览器的书签目录:

export const getBookmarks = () => {return new Promise((resolve) => {chrome.bookmarks.getTree(function (bookmarkTreeNodes) {resolve(bookmarkTreeNodes);});});
};

在上述代码中,我们首先使用chrome.bookmarks.getTree()方法获取 Chrome 浏览器的书签目录树。

请注意,要使用chrome.bookmarks API,你需要在你的 Chrome 插件中声明"bookmarks"权限。具体来说,在插件清单文件(manifest.json)中添加以下内容:

{"manifest_version": 2,"name": "你的插件名称","version": "1.0","permissions": ["bookmarks"],"background": {"scripts": ["bg.js"]}
}

在上述代码中,我们在"permissions"字段中声明了"bookmarks"权限,以便我们可以使用chrome.bookmarks API。同时,在"background"字段中指定了一个后台脚本(bg.js),以便我们在后台执行上述代码。

删除chrome浏览器书签

导入书签前我们需要先清除一下当前浏览器的书签,通过chrome.bookmarks.removeTree可以删除书签节点。

export function removeBookmarks(bookmarkTreeNodes) {// 遍历书签树,删除所有的书签function traverseBookmarks(bookmarkNodes) {for (const node of bookmarkNodes) {if (node.children) {traverseBookmarks(node.children);}// 删除书签节点chrome.bookmarks.removeTree(node.id);}}traverseBookmarks(bookmarkTreeNodes);
}
导入书签

使用chrome.bookmarks.create来新建书签。

export function importBookmarks(bookmarkTreeNodes) {// 遍历书签树function traverseBookmarks(bookmarkNodes, parentId) {for (const node of bookmarkNodes) {// 如果节点是文件夹if (node.children) {// 创建一个新的文件夹节点chrome.bookmarks.create({parentId: parentId,title: node.title,},function (newFolderNode) {// 递归遍历子节点traverseBookmarks(node.children, newFolderNode.id);});}// 如果节点是书签else {// 创建一个新的书签节点chrome.bookmarks.create({parentId: parentId,title: node.title,url: node.url,});}}}// 从根节点开始遍历书签树traverseBookmarks(bookmarkTreeNodes[0].children, "1");
}

插件使用

1、插件下载

直接到gitee上下载源码即可:

源码地址:https://gitee.com/zheng_yongtao/chrome-plug-in.git

2、导入插件

书签同步插件的目录如下:
image.png

下载完后打开浏览器扩展程序管理页面(chrome://extensions/),选择加载已解压的扩展程序:

image.png

选择插件目录导入即可:

image.png

导入成功后就可以看到下面这个插件了
image.png

可以勾选上下面这个,勾选后插件就会显示在导航栏上
image.png

3、补充gitee仓库信息数据

导入插件后,我们点击导航栏的插件图标,可以看到这样一个面板,其中有四个数据需要我们填写:
image.png

获取 token

进入到giteeAPI文档进行授权获取到返回填写即可,具体步骤如下:
image.png

image.png

image.png

image.png

仓库所属空间地址(owner)

就是个人主页的一个空间地址,如下图:
image.png

仓库路径(repo)

前面新建仓库的路径(仓库名),如下图:
image.png

书签文件路径(filePath)

新建用于保存书签数据的文件,想保存多份不同的数据的话可以多件几个不同的文件分别进行存储,同步的时候选择对应的目录即可,如下图:
image.png

将对应信息填写上之后我们就可以开始进行同步操作了:
image.png

4、同步方式

(1)覆盖保存

使用当前浏览器书签数据覆盖保存到gitee仓库中。

(2)合并保存

将当前浏览器书签数据与gitee仓库中的书签数据合并好再进行保存。

(3)覆盖获取

使用gitee仓库中的书签数据覆盖掉本地的书签数据。

(4)合并获取

将gitee仓库中的书签数据和本地的书签数据合并后再覆盖掉本地的书签数据。

(5)合并规则

同一层级并且同名的目录我们会将其子节点合并到同一目录下,同一层级下我们会根据 书签名 + 书签url 对该层级的书签进行去重。

源码

1、gitee

gitee 地址:https://gitee.com/zheng_yongtao/chrome-plug-in/tree/master/chrome-bookmarks-manage

2、公众号

关注公众号『前端也能这么有趣』发送 chrome插件即可获取源码。

说在后面

🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/119485.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2018年亚太杯APMCM数学建模大赛B题人才与城市发展求解全过程文档及程序

2018年亚太杯APMCM数学建模大赛 B题 人才与城市发展 原题再现 招贤纳士是过去几年来许多城市的亮点之一。北京、上海、武汉、成都、西安、深圳,实际上都在用各种吸引人的政策来争夺人才。人才代表着城市创新发展的动力,因为他们能够在更短的时间内学习…

新生儿黄疸:原因、科普和注意事项

引言: 新生儿黄疸是许多新生儿面临的常见情况。虽然它通常是一种暂时的现象,但对于许多父母来说,这可能引发担忧。本文将科普新生儿黄疸的原因,提供相关信息,并为父母和监护人提供注意事项,以帮助他们更好…

使用AOP切面实现日志记录功能

系列文章 1.SpringBoot整合RabbitMQ并实现消息发送与接收 2. 解析JSON格式参数 & 修改对象的key 3. VUE整合Echarts实现简单的数据可视化 4. Java中运用BigDecimal对字符串的数值进行加减乘除等操作 5. List<HashMap<String,String>&…

密码学-SHA-1算法

实验七 SHA-1 一、实验目的 熟悉SHA-1算法的运行过程,能够使用C语言编写实现SHA-1算法程序,增 加对摘要函数的理解。 二、实验要求 (1)理解SHA-1轮函数的定义和工作过程。 (2)利用VC语言实现SHA- 1算法。 (3)分析SHA- 1算法运行的性能。 三、实验…

【深蓝学院】手写VIO第8章--相机与IMU时间戳同步--笔记

0. 内容 1. 时间戳同步问题及意义 时间戳同步的原因:如果不同步,由于IMU频率高,可能由于时间戳不同步而导致在两帧camera之间的时间内用多了或者用少了IMU的数据,且时间不同步会导致我们首尾camera和IMU数据时间不同,…

65、内网安全-域环境工作组局域网探针方案

目录 案例1-基本信息收集操作演示案例2-网络信息收集操作演示案例3-用户信息收集操作演示案例4-凭据信息收集操作演示案例5-探针主机域控架构服务操作演示涉及资源 我们攻击内网一般是借助web攻击,直接进去,然后再去攻击内网,那么攻击的对象一…

Redis 命令 和 数据类型 您知道多少

文章目录 一、概述二、Redis 命令行客户端连接 Redis 服务器三、在 Redis 帮助命令的说明四、Redis 通用命令 generic4.1 通用命令说明4.1 keys 命令,列举出当前库的所有键4.2 type 命令,可以查看键对应值的类型4.3 object encoding 命令,查看…

深度学习 anaconda 安装问题

配置anaconda 在官网下载匹配版本的anaconda(官网下载可能时间比较长),可以选择清华镜像。 安装过程默认即可,或者根据情况进行修改。 旧版本是可以在安装的时候勾选添加路径到环境变量中的,但是我安装的是2023.9月…

react 中setState 的三种写法

目录 1:使用对象形式的setState: 2:使用函数形式的setState: 3:使用回调函数: 1:使用对象形式的setState: this.setState({ count: 0 });2:使用函数形式的setState: this.setSt…

基于ResNet34的花朵分类

一.数据集准备 新建一个项目文件夹ResNet,并在里面建立data_set文件夹用来保存数据集,在data_set文件夹下创建新文件夹"flower_data",点击链接下载花分类数据集https://storage.googleapis.com/download.tensorflow.org/example_i…

Qt QWebEngine 更换语言

背景 使用Qt QWebEngine开发的应用,在一些场景下,会显示英文文本,比如右键、JS弹出的对话框,所以需要进行汉化,更改语言。 准备翻译文件 Qt有提供翻译好的ts文件,我们可以直接下载ts文件qtwebengine_zh_…

深度学习——图像分类(CIFAR-10)

深度学习——图像分类(CIFAR-10) 文章目录 前言一、实现图像分类1.1. 获取并组织数据集1.2. 划分训练集、验证集1.3. 图像增广1.4. 引入数据集1.5. 定义模型1.6. 定义训练函数1.7. 训练模型并保存模型参数 二、生成一个桌面小程序2.1. 使用QT设计师设计界…

【Unity程序技巧】异步保险箱管理器

👨‍💻个人主页:元宇宙-秩沅 👨‍💻 hallo 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 👨‍💻 本文由 秩沅 原创 👨‍💻 收录于专栏:Uni…

华为eNSP配置专题-路由策略的配置

文章目录 华为eNSP配置专题-路由策略的配置0、概要介绍1、前置环境1.1、宿主机1.2、eNSP模拟器 2、基本环境搭建2.1、终端构成和连接2.2、终端的基本配置 3、配置路由策略3.1、目标3.2、配置路由策略 华为eNSP配置专题-路由策略的配置 0、概要介绍 路由策略就是通过一系列工具…

【云原生】portainer管理多个独立docker服务器

目录 一、portainer简介 二、安装Portainer 1.1 内网环境下: 1.1.1 方式1:命令行运行 1.1.2 方式2:通过compose-file来启动 2.1 配置本地主机(node-1) 3.1 配置其他主机(被node-1管理的节点服务器&…

c++类和对象(1)

目录 目录: 1.类的定义 1.1:类的语法及相关概念 1.2:类的两种定义方式 1.3:类的成员变量名命名规范 2.类的访问限定符 2.1:三种访问限定符关键字引入 2.2:访问限定符的语法说明 2.3:c中struct与class的区别(面试常考) 3.类的作用域 4.类的实例化 5.类对象 5.1:类对…

uniapp实现webview页面关闭功能

实现思路: 1.关闭按钮是使用原生button添加的close属性。(见page.json页面) 2.监听关闭按钮的方法。(onNavigationBarButtonTap) 3.写实现关闭webview所有页面的逻辑。 废话不多说,直接上代码 1.page.…

《GB/T 8566-2022/ISO/IEC/IEEE:系统与软件工程生存周期过程》国家标准解读,附下载地址

关于企业架构、软件工程等相关内容,基本在行业内工作一段时间都能解释出各自的理解,网络资料更是知识爆炸,看似哪一种都对,其实相对都是个人理解,算不上严谨。 上周工作中涉及架构的企业标准编制审查,对严…

网工内推 | 国企,解决方案工程师,最高30k,有软考证书优先

01 中电信数智科技有限公司海南分公司 招聘岗位:解决方案经理(ICT) 职责描述: 1、负责调动前后端资源做好全省ICT业务的售前支撑服务工作。 2、根据实际项目需求,主动协同客户渠道开展ICT项目商机挖掘,促进…

用友U8SMSProxy -SQL注入漏洞

0x01 漏洞介绍 用友GRP-U8 R10政务管理软件是由用友政务公司基于云技术所推出的第十代政务产品。这款产品继承了用友R9、R9i、U8等行政事业版产品的各项优点,并融合了全国广大用户的最佳实践应用。它旨在为政府财政部门、社保部门、卫生部门、教育部门、民政部门、党…