起因
在我的日常工作流中,read-later-list 是必备项,一直以来 我都是用 Notion Page 来记录和同步每天待看的文章
(有很多的未读)(但是不重要)
由于标记了日期,也在回溯文章时有迹可循
可能就会有人问:那你为什么不用收藏夹 or 集锦?答案是删除时费工夫、不能根据时间检索、不能多端同步,同时 过于独立的页面也很难让人有想打开看的欲望,久而久之积攒的更多了
错误范例:
(更多 更多的未读)
所以 Notion Page 姑且成为了最佳选择,它被我固定到标签栏最上边(我是用竖栏标签页的),闲着没事就会进去消灭几个未读
然而时间长了另一个痛点逐渐涌现:把文章作为超链接加到 Page 中真的好麻烦!我需要复制标题、复制网页链接、进入 Notion Page、添加 To do list block、添加超链接内容,在我经历这一过程 1 年多以后心态逐渐崩溃(不是),我开始寻找有没有更简单的方式 —— 哪怕,哪怕只有一个前端简陋的 dashboard page,只要能够显示类似 Notion Page 的内容就可以 —— 于是有了下面的探索
前置
read-later 扩展的痛点
无意中搜到了 read-later 这个浏览器扩展,安装后可以在当前页面右键(或超链接右键)将网页加入待读,在扩展的 popup 页面即可显示
对一个网页可以显示 favicon、标题、阅读进度(部分),还可以方便的删除 & 同步(利用扩展的同步性),看似已经是完美符合我的需求,但实际体验一段时间后我发现了这样几个通点
- 无法在更多端进行同步:比如我的 ipad mini6,比如我的 firefox
- popup 页面和收藏夹、集锦一样,侵入性太强
- 不能根据时间做分类
- 不能自定义添加网页的标题
于是开始了改造之路
read-later 的程序逻辑
让我们先对这个扩展进行简易的代码分析,核心的代码其实只有 background 文件夹中的 4 个 js
下面简单说一下每个 js 的主要功能:
- actions.js
包含添加页面、删除页面、更新 storage 的函数
- background.js
注册 contextMenus,监听鼠标右键操作 并把需要的信息交给 action.js 中处理,也监听 runtime 的消息(更新、安装)
- pageInfo.js
定义了 PageInfo, PositionInfo 和 SelectionInfo(后两个均基于 PageInfo),分别表示页面信息、当前浏览的位置信息、超链接的选择信息,向外导出 initPageInfo 和 completeInfo 用于添加页面到 storage
- request.js
主要是针对超链接文章,用于获取未实际访问的超链接的标题
改造
自定义添加网页的标题
* 私信认为这是个相当必需的需求,但不知为何作者并未做到这一点?
background.js 原本的监听是这样做的
contextMenus.onClicked(async (selection, tab) => {
selection.linkUrl
? await action.saveSelection(tab, selection)
: await action.savePage()
})
对点击区域按是否存在 selection.linkUrl 来区分即将添加的是超链接还是当前网页,如果是超链接 会默认不存在标题、交给 request.js 获取 <title>
中的内容作为标题;如果是网页,直接从 tab.title 获取
—— 怎么看都没有我们插手的地方,我选择首先把 saveXXX 的判断改掉
contextMenus.onClicked(async (selection, tab) => {
if (typeof selection.linkUrl === "undefined" && typeof selection.selectionText === "undefined"){
await action.savePage()
} else {
await action.saveSelection(tab, selection) // use selectionText as page.title
}
})
当右键划动选中文字区域时,虽然没有 selection.linkUrl,但是 selection.selectionText 仍然存在,依靠二者共同判断是添加 Page 还是 Selection
之后修改 SelectionInfo 类的方法
我们把 “在选中内容处右键” 作为添加 Page 的一种方式 但仍然归类到 SelectionInfo 中,最小限度的修改代码、实现需求
修改 "done" 浮现的条件
添加成功就会浮现这个 Badge,但并不是所有添加都会浮现!强迫症不服
同时我也删去了很奇葩的一个设置:将当前页面添加 read-list 后自动关闭当前页面,很智障,很让人窒息
对接后端 server
因为涉及到了数据展示和更新,本想用我最喜欢的 streamlit,但 streamlit 不能像正常的后端一样接收 api 请求,无奈选用了 Flask;因为之前 CTF 接触了太多的 Flask,代码部分倒是不难(还有 chatgpt 强力驱动)
import sqlite3
from flask import Flask, render_template, request, redirect, jsonify, abort
from datetime import datetime
import os
app = Flask(__name__)
@app.before_request
def auth():
if request.headers.get('Authorization') == os.getenv('seckey'):
return None
else:
abort(403)
@app.route('/api/add', methods=['POST'])
def add():
data = request.get_json()
# print(data)
conn = sqlite3.connect('data.db')
c = conn.cursor()
c.execute("INSERT INTO reading_list (title, url) VALUES (?, ?)",
(data['title'], data['url']))
conn.commit()
conn.close()
return jsonify({'message': 'Data added successfully'})
@app.route('/dashboard')
def dashboard():
conn = sqlite3.connect('data.db')
cursor = conn.cursor()
cursor.execute(
"SELECT id, title, url, strftime('%Y-%m-%d %H:%M:%S', create_time, '+8 hour'), status FROM reading_list ORDER BY create_time DESC")
rows = cursor.fetchall()
conn.close()
data = {}
for row in rows:
record = {
'id': row[0],
'status': row[4],
'title': row[1],
'url': row[2],
'create_time': datetime.strptime(row[3], "%Y-%m-%d %H:%M:%S")
}
create_time = record['create_time'].strftime("%m%d")
if create_time not in data:
data[create_time] = []
data[create_time].append(record)
# print(data)
return render_template('dashboard.html', data=data, seckey=request.headers.get('Authorization'))
@app.route('/sync/<int:id>/<int:status>')
def sync_status(id, status):
conn = sqlite3.connect('data.db')
cursor = conn.cursor()
cursor.execute("UPDATE reading_list SET status=? WHERE id=?", (status, id))
conn.commit()
conn.close()
return redirect('/dashboard')
@app.route('/delete/<int:id>')
def delete_record(id):
conn = sqlite3.connect('data.db')
cursor = conn.cursor()
cursor.execute("DELETE FROM reading_list WHERE id=?", (id,))
conn.commit()
conn.close()
return redirect('/dashboard')
if __name__ == '__main__':
app.run(host="0.0.0.0", port=10393)
模板渲染部分,用 jinja2 真的非常丝滑
经过前面的修改,现在页面右键、对超链接右键、选择内容右键都可以在原扩展的基础上请求自己搭建的 server,并存到 data.db 数据库中,访问 /dashboard 会有类似这样的显示效果
checkbox 和代表 delete 的 × 都可以点击,联动后端的存储
虽然一看是让前端 er 闻者落泪的显示效果…… 但这已经是我和 chatgpt 大战了 500 个回合 + 自己修改了 N 次的结果了()能用就行嗯嗯嗯
可能细心的师傅已经注意到代码中频繁出现的 seckey
了,惭愧的承认 这时另一个较为失败的地方,在脑内思考了多种鉴权、认证方式后,选择了代码最少的方式 ——Flask 自带的装饰器 @app.before_request
,服务端通过设置环境变量 seckey,整个 app 靠 Authorization 头来做一刀切的鉴权;不过纵使有种种缺点,它最大的有点还是代码少,之后会进行修改的(迫真)
仍存在的问题
- 过于简陋的前端
- 需要在前端加入随便写的便签功能
- 修改认证 / 鉴权逻辑,做到多端丝滑访问
- 适当修改 readme
(虽然根本不会有第二个人用就是了)