picgo-plugin-gitee 2.1.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/readme.md +1 -0
  3. package/src/index.js +218 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "picgo-plugin-gitee",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "picgo uploader for gitee",
5
5
  "main": "src/index.js",
6
6
  "files": [
package/readme.md CHANGED
@@ -46,6 +46,7 @@ git push -u origin master
46
46
  ### Feature
47
47
  - support sync gitee file delete
48
48
  - 上传/删除结束均会弹出系统通知(PicGo 消息中心)
49
+ - GUI 菜单提供「从 Gitee 同步远程文件到相册」按钮(递归仓库所有图片,自动跳过非图片文件)
49
50
 
50
51
  ### Todo
51
52
 
package/src/index.js CHANGED
@@ -18,6 +18,223 @@ module.exports = (ctx) => {
18
18
  ctx.on("remove", onRemove);
19
19
  };
20
20
 
21
+ // GUI 菜单项:手动触发"从 Gitee 拉取远程文件到相册"
22
+ const guiMenu = (ctx) => {
23
+ return [
24
+ {
25
+ label: "同步 Gitee 远程文件到相册",
26
+ async handle(ctx, guiApi) {
27
+ // picgo GUI 有时会把 async handle 里的异常静默吞掉。
28
+ // 手动加 try/catch 确保任何错误都能反馈到 UI 和日志。
29
+ const started = Date.now();
30
+ log("info", "[同步]按钮被点击");
31
+ log("info", `[同步]ctx 类型: ${typeof ctx}, guiApi 类型: ${typeof guiApi}`);
32
+ log(
33
+ "info",
34
+ `[同步]guiApi.galleryDB: ${guiApi && !!guiApi.galleryDB}, guiApi.showNotification: ${guiApi && !!guiApi.showNotification}`
35
+ );
36
+ try {
37
+ await syncRemoteToGallery(ctx, guiApi);
38
+ log("success", `[同步]完成,耗时 ${Date.now() - started}ms`);
39
+ } catch (err) {
40
+ const msg = err && err.message ? err.message : String(err);
41
+ const stack = err && err.stack ? err.stack : "";
42
+ log("error", "[同步]失败:" + msg);
43
+ if (stack) log("error", stack);
44
+ // 弹窗 + 系统通知,三重保险
45
+ if (guiApi && guiApi.showMessageBox) {
46
+ try {
47
+ guiApi.showMessageBox({
48
+ type: "error",
49
+ title: "Gitee 同步失败",
50
+ message: msg,
51
+ detail: stack ? stack.split("\n").slice(0, 3).join("\n") : "",
52
+ });
53
+ } catch (e) {
54
+ log("error", "[同步]showMessageBox 失败:" + e.message);
55
+ }
56
+ }
57
+ if (guiApi && guiApi.showNotification) {
58
+ try {
59
+ guiApi.showNotification({
60
+ title: "Gitee 同步失败",
61
+ body: msg,
62
+ });
63
+ } catch (e) {
64
+ log("error", "[同步]showNotification 失败:" + e.message);
65
+ }
66
+ }
67
+ }
68
+ },
69
+ },
70
+ ];
71
+ };
72
+
73
+ // 统一的日志输出,兼容 picgo-core (ctx.log.info) 和 picgo GUI (ctx.log 可能行为不同)
74
+ // 同时打 console 兜底,确保 Electron 主进程也能看到
75
+ const log = function (level, msg) {
76
+ try {
77
+ // picgo 的 ctx.log 支持 info / warn / success / error
78
+ const fn = ctx.log && (ctx.log[level] || ctx.log.info);
79
+ if (fn) fn.call(ctx.log, msg);
80
+ } catch (e) {
81
+ // 静吞
82
+ }
83
+ // 兜底:直接打到 stdout,这样无论 picgo GUI 把日志写到哪,这里都至少有一条记录
84
+ // (用户终端如果通过 npm 链接加载插件,能看到;GUI 主进程 console 也能看到)
85
+ if (typeof console !== "undefined") {
86
+ try {
87
+ console.log("[picgo-plugin-gitee]", level.toUpperCase(), msg);
88
+ } catch (e) {
89
+ // 静吞
90
+ }
91
+ }
92
+ };
93
+
94
+ // 相册里只保留图片文件,避免把 README/配置文件等也拉进来
95
+ const IMAGE_EXTS = new Set([
96
+ "jpg", "jpeg", "png", "gif", "webp", "bmp", "svg",
97
+ "tif", "tiff", "ico", "avif", "heic",
98
+ ]);
99
+
100
+ // 递归列出 Gitee 仓库所有图片文件(含子目录)。
101
+ // 返回 [{ fileName, imgUrl, sha, size }]
102
+ const listAllRemoteFiles = async function (userConfig) {
103
+ const headers = getHeaders();
104
+ const listUrl =
105
+ userConfig.baseUrl + "/contents" + formatConfigPath(userConfig);
106
+ const out = [];
107
+ await walkContents(listUrl, userConfig, headers, out);
108
+ return out
109
+ .filter((it) => {
110
+ const ext = (it.name || "").split(".").pop().toLowerCase();
111
+ return IMAGE_EXTS.has(ext);
112
+ })
113
+ .map((it) => ({
114
+ fileName: it.path,
115
+ imgUrl: userConfig.previewUrl + "/" + it.path,
116
+ sha: it.sha,
117
+ size: it.size,
118
+ }));
119
+ };
120
+
121
+ const walkContents = async function (url, userConfig, headers, out) {
122
+ const fullUrl = url + "?access_token=" + userConfig.token;
123
+ let res;
124
+ try {
125
+ res = await ctx.Request.request({
126
+ method: "GET",
127
+ url: fullUrl,
128
+ headers: headers,
129
+ });
130
+ } catch (err) {
131
+ throw new Error("获取 Gitee 目录失败:" + err.message);
132
+ }
133
+ // 兼容 picgo-core(字符串)和 picgo GUI(对象)
134
+ const items = typeof res === "string" ? JSON.parse(res) : res;
135
+ if (!Array.isArray(items)) {
136
+ // 单文件(理论上 listAllRemoteFiles 不会传单文件路径,兜底)
137
+ if (items && items.type === "file") {
138
+ out.push(items);
139
+ }
140
+ return;
141
+ }
142
+ for (const item of items) {
143
+ if (item.type === "file") {
144
+ out.push(item);
145
+ } else if (item.type === "dir") {
146
+ // 递归子目录
147
+ await walkContents(url + "/" + item.path, userConfig, headers, out);
148
+ }
149
+ }
150
+ };
151
+
152
+ // 把 Gitee 远程文件写入 PicGo GUI 相册。
153
+ // 关键点:每条记录带 type="gitee",这样从相册删除时会触发 onRemove 真正删除 gitee 上的文件。
154
+ // 异常统一向上抛出,由 guiMenu.handle 里的 catch 统一弹窗。
155
+ const syncRemoteToGallery = async function (ctx, guiApi) {
156
+ if (!guiApi || !guiApi.galleryDB) {
157
+ throw new Error("当前不在 PicGo GUI 环境(galleryDB 不可用)");
158
+ }
159
+ log("info", "[同步]开始检查配置");
160
+ let userConfig;
161
+ try {
162
+ userConfig = getUserConfig();
163
+ } catch (err) {
164
+ throw new Error("请先在插件设置中填写 owner / repo / token");
165
+ }
166
+ if (!userConfig.owner || !userConfig.repo || !userConfig.token) {
167
+ throw new Error("请先在插件设置中填写 owner / repo / token");
168
+ }
169
+ log(
170
+ "info",
171
+ `[同步]配置 OK: owner=${userConfig.owner} repo=${userConfig.repo} path=${userConfig.path || "(root)"}`
172
+ );
173
+
174
+ if (guiApi.showNotification) {
175
+ guiApi.showNotification({
176
+ title: "正在同步",
177
+ body: "正在从 Gitee 拉取远程文件...",
178
+ });
179
+ }
180
+
181
+ log("info", "[同步]开始递归拉取 Gitee 文件列表");
182
+ const remoteFiles = await listAllRemoteFiles(userConfig);
183
+ log("info", `[同步]仓库共 ${remoteFiles.length} 个图片文件`);
184
+
185
+ if (remoteFiles.length === 0) {
186
+ log("warn", "[同步]仓库里没有任何图片文件");
187
+ if (guiApi.showNotification) {
188
+ guiApi.showNotification({
189
+ title: "Gitee 同步完成",
190
+ body: "仓库里没有任何图片文件",
191
+ });
192
+ }
193
+ return;
194
+ }
195
+
196
+ log("info", "[同步]读取本地相册");
197
+ const existing = (await guiApi.galleryDB.get()) || [];
198
+ log("info", `[同步]本地相册已有 ${existing.length} 条`);
199
+ const existingUrls = new Set(existing.map((x) => x.imgUrl));
200
+ const newItems = remoteFiles
201
+ .filter((f) => !existingUrls.has(f.imgUrl))
202
+ .map((f) => ({
203
+ fileName: f.fileName,
204
+ imgUrl: f.imgUrl,
205
+ type: uploadedName,
206
+ sha: f.sha,
207
+ extname: f.fileName.split(".").pop(),
208
+ }));
209
+ log(
210
+ "info",
211
+ `[同步]需要新增 ${newItems.length} 条(已存在 ${remoteFiles.length - newItems.length} 条)`
212
+ );
213
+
214
+ if (newItems.length === 0) {
215
+ if (guiApi.showNotification) {
216
+ guiApi.showNotification({
217
+ title: "Gitee 同步完成",
218
+ body: "没有新增文件,相册已是最新",
219
+ });
220
+ }
221
+ return;
222
+ }
223
+
224
+ log("info", "[同步]写入相册 insertMany");
225
+ await guiApi.galleryDB.insertMany(newItems);
226
+ log(
227
+ "success",
228
+ `[同步]完成:共 ${remoteFiles.length} 个,新增 ${newItems.length} 个`
229
+ );
230
+ if (guiApi.showNotification) {
231
+ guiApi.showNotification({
232
+ title: "Gitee 同步完成",
233
+ body: `新增 ${newItems.length} 个文件到相册(共发现 ${remoteFiles.length} 个)`,
234
+ });
235
+ }
236
+ };
237
+
21
238
  const getHeaders = function () {
22
239
  return {
23
240
  "Content-Type": "application/json;charset=UTF-8",
@@ -327,5 +544,6 @@ module.exports = (ctx) => {
327
544
  return {
328
545
  uploader: "gitee",
329
546
  register,
547
+ guiMenu,
330
548
  };
331
549
  };