0%

基于剪切板实现单词监控

github 地址:https://github.com/killlowkey/olu-word

最近看英文文档比较多,因为在这过程中遇到的生词比较多嘛,所以需要将这些生词添加到生词本中,以便于复习。偶然在 ipad 上看到了欧路词典,使用下来各方面体验都还不错。然而在 pc 上,找了一圈没有找到欧路词典的浏览器插件,这就使得我很懵逼了。这时刚好看到了欧路词典的官网提供了 API 接口,于是萌发了自己写一个添加生词app想法。

需求分析

我们要实现的功能很简单,就是通过将生词添加到欧路词典的生词本中。刚开始本来想用 spring shell 来实现,写一个基于命令行 app,最后还是放弃了,因为这种实现使用也比较麻烦,每次添加单词时还需要敲一个命令行这不是闲的蛋疼嘛。于是改用监控剪切板进行实现,我们只需要将生词进行复制,之后app监听到剪切板的内容,如果是一个单词那么就添加到生词本中。欧路词典提供 API 中需要在请求添加一个 Authorization 头来进行请求,这时 Authorization 值肯定不能写死在程序中,不然 Authorization 过期了还需要修改代码重新打包,所以需要从外部的配置文件来进行加载。

基于上述的需求,我们实现的功能大致分为以下三步

  1. 应用启动时从外部配置文件加载 Authorization
  2. 监听剪切板
  3. 上传单词到欧路词典

配置加载

应用打包之后时一个 jar 文件,配置要与 jar 文件存放同一目录。首先应用启动时需要获取 jar 文件存放路径,之后加载该路径下的 config.properties,最后从该配置文件中获取 Authorization

获取 jar 文件存放路径这里有一个小坑,就是我们得到的路径是这个样子的 /C:/xxx/xxx.jar 的,所以需要对路径来进行处理,我们只需要 /c:/xxx/ 。获取路径之后,需要找到路径中最后一个 / 位置,之后通过 String#subString 方法来对路径进行截取。

代码使用 reactor 来进行编写,所以这里解释一下代码的含义。在 static 代码块中,通过 Mono#just 来下发一个 Properties 实例元素,之后通过 doOnNext 来加载 config.properties ,最后下游进行订阅从 properties 中获取 authorization 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private static String authorization;

static {
Mono.just(new Properties())
.doOnNext(properties -> {
try {
properties.load(new FileInputStream(getPath() + "config.properties"));
} catch (IOException e) {
e.printStackTrace();
}
})
.subscribe(properties -> authorization = properties.getProperty("authorization"));
}

private static String getPath() {
String path = OluWordApplication.class.getProtectionDomain().getCodeSource().getLocation().getPath();
int index = 0;

char[] chars = path.toCharArray();
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
if (c == '/' || c == '\\') {
index = i;
}
}

return path.substring(0, index + 1);
}

剪切板监听

加载配置完成之后,需要做的就是监听剪切板。这里我们采用的是定时监听,每200ms就监听一次,在 Reactor 可以通过 Flux#interval 来进行实现,根据传入 Duration 实例然后定时下发元素。每次进行下发时,就从剪切板获取内容。获取之后我们通过正则进行判断(单词开头是小写或大写字母开头,其余都是小写字母,那么判定该文本是单词),如果该文本是单词的话那么就添加到生词本中。

监听剪切板有这里有一个小坑,假设我复制了一个单词,并且添加到生词本。之后进行监听时还会将这个单词添加到生词本中,这里我们通过 currentWord 来解决这个问题,每次添加单词时就更新 currentWord,如果当前剪切板内容是 currentWord,那么就不需要将该剪切板文本添加到单词本中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private static String currentWord = "";

public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(1);

Flux.interval(Duration.ofMillis(200))
// 获取剪切板内容
.map(seed -> getSysClipboardText())
// 判断该文本是否是单词并且word不等于currentWord
.filter(word -> !word.equals(currentWord) && word.matches("[a-zA-Z][a-z]+"))
// 添加单词时是 IO 操作所以,采用调度器来进行处理
.publishOn(Schedulers.parallel())
// 更新 currentWord
.doOnNext(word -> currentWord = word)
.doOnSubscribe(subscription -> System.out.println("欧路词典监控已启动..."))
.doOnComplete(countDownLatch::countDown)
// 添加到生词本
.subscribe(OluWordApplication::submitWord);

countDownLatch.await();
}


private static String getSysClipboardText() {
String clipboard = ClipboardUtil.getStr();
return clipboard != null ? clipboard.trim() : "";
}

添加生词

添加生词这个步骤很简单,就是构建一个 post 请求,然后进行请求就好了。这里还是使用 Mono#just 来进行下发元素,通过 hutoolHttpUtil 来创建一个 post 请求,之后在 doOnNext 中添加请求头与请求体。然后在 map 中执行该请求并获取它的响应码,最后下游订阅时响应码是201 说明我们添加成功了,401说明Authorization过期了或者有误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static void submitWord(String word) {
Mono.just(HttpUtil.createPost("https://api.frdic.com/api/open/v1/studylist/words"))
.doOnNext(request -> request
.header("Authorization", authorization)
.body(String.format("{\"id\":\"0\",\"language\":\"en\",\"words\":[\"%s\"]}",
word.toLowerCase())))
.map(request -> request.execute().getStatus())
.subscribe(status -> {
if (status == 201) {
System.out.println(word.toLowerCase() + ":导入成功");
} else if (status == 401) {
System.out.println("授权过期,请更新Authorization,并重启应用");
}
});
}

打包与启动

之后在控制台输入mvn clean package 打包成功 jar 包,最后通过 java -jar xxx.jar 命令启动应用即可。