Skip to content

Commit fe843cd

Browse files
authored
feat: support publish ing (#89)
1 parent 3c38686 commit fe843cd

File tree

8 files changed

+286
-4
lines changed

8 files changed

+286
-4
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
- [导出 pdf](#导出-pdf)
2020
- [提取图片](#提取图片)
2121
- [博文设置面板](#博文设置面板)
22+
- [闪存](#闪存)
2223
- [vscode 版本要求](#vscode-版本要求)
2324
- [插件设置](#插件设置)
2425

@@ -144,6 +145,28 @@
144145

145146
<kbd><img src="https://img2022.cnblogs.com/blog/1596066/202203/1596066-20220331113211016-1457564407.png?v=202204242" height="550"></kbd>
146147

148+
### 闪存
149+
150+
支持通过快捷键 `ctrl+s ctrl+c`(windows) `cmd + s cmd+c`(mac) 调用发闪存命令, 发闪存前需要先编辑, 编辑完成后可以发布; 也可以在编辑器中选中一段文本或代码, 然后鼠标右键唤起上下文菜单, 可以将选中的内容发到闪存
151+
152+
<kbd><img src="https://img2022.cnblogs.com/blog/35695/202211/35695-20221115143017664-1504226894.png" alt="" height="550"></kbd>
153+
154+
可以通过博客园导航视图中有 `发闪存` 按钮调用发闪存命令, 点击后会先要求编辑闪存, 编辑后可以发布
155+
156+
<kbd><img src="https://img2022.cnblogs.com/blog/35695/202211/35695-20221115143240134-832955354.png" alt="" height="550"></kbd>
157+
158+
<kbd><img src="https://img2022.cnblogs.com/blog/35695/202211/35695-20221115144008924-1453379990.png" alt=""></kbd>
159+
160+
编辑完成后回车会弹出确认框, 此时可以通过 `编辑内容` `编辑访问权限` `编辑标签` 按钮二次编辑
161+
162+
<kbd><img src="https://img2022.cnblogs.com/blog/35695/202211/35695-20221115143733447-1514251853.png" height="550"></kbd>
163+
164+
也可以通过VSCode命令面板(`ctrl/cmd + p`唤起命令面板)调用发闪存命令
165+
166+
<kbd><img src="https://img2022.cnblogs.com/blog/35695/202211/35695-20221115144307251-1543702626.png" height="550"></kbd>
167+
168+
通过本插件发布的闪存, 在尾部会显示一个vscode图标
169+
147170
## vscode 版本要求
148171

149172
\>=1.62.0

package.json

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"onView:vscode-cnb-workspace",
3030
"onCommand:vscode-cnb.open-workspace",
3131
"onCommand:vscode-cnb.login",
32-
"onCommand:vscode-cnb.logout"
32+
"onCommand:vscode-cnb.logout",
33+
"onCommand:vscode-cnb.ing-publish-selection"
3334
],
3435
"main": "./dist/extension.js",
3536
"contributes": {
@@ -289,6 +290,18 @@
289290
"category": "Cnblogs",
290291
"icon": "$(refresh)",
291292
"enablement": "vscode-cnb.isAuthorized && !vscode-cnb.posts-list-refreshing"
293+
},
294+
{
295+
"command": "vscode-cnb.ing.publish",
296+
"title": "发闪存",
297+
"category": "Cnblogs",
298+
"enablement": "vscode-cnb.isAuthorized"
299+
},
300+
{
301+
"command": "vscode-cnb.ing.publish-selection",
302+
"title": "将选中内容发到闪存",
303+
"category": "Cnblogs",
304+
"enablement": "vscode-cnb.isAuthorized && editorHasSelection == true && isInDiffEditor == false && isInEmbeddedEditor == false"
292305
}
293306
],
294307
"configuration": [
@@ -532,6 +545,10 @@
532545
{
533546
"command": "vscode-cnb.export-post-to-pdf",
534547
"when": "false"
548+
},
549+
{
550+
"command": "vscode-cnb.ing.publish-selection",
551+
"when": "false"
535552
}
536553
],
537554
"view/item/context": [
@@ -693,6 +710,10 @@
693710
"command": "vscode-cnb.extract-images",
694711
"when": "resourceLangId == markdown",
695712
"group": "cnblogs@9"
713+
},
714+
{
715+
"command": "vscode-cnb.ing.publish-selection",
716+
"group": "cnblogs@10"
696717
}
697718
],
698719
"editor/title": [
@@ -757,6 +778,11 @@
757778
"key": "ctrl+alt+f",
758779
"mac": "cmd+alt+f",
759780
"when": "editorTextFocus && resourceLangId == markdown"
781+
},
782+
{
783+
"command": "vscode-cnb.ing.publish",
784+
"key": "ctrl+s ctrl+c",
785+
"mac": "cmd+s cmd+c"
760786
}
761787
],
762788
"viewsContainers": {
@@ -781,6 +807,11 @@
781807
"view": "cnblogs-navigation",
782808
"contents": "[首页](https://www.cnblogs.com)\n[新闻](https://news.cnblogs.com/)\n[博问](https://q.cnblogs.com/)\n[闪存](https://ing.cnblogs.com/)"
783809
},
810+
{
811+
"view": "cnblogs-navigation",
812+
"contents": "[发闪存](command:vscode-cnb.ing.publish)",
813+
"when": "vscode-cnb.isAuthorized"
814+
},
784815
{
785816
"view": "vscode-cnb-workspace",
786817
"contents": "[在vscode中打开工作空间](command:vscode-cnb.open-workspace)",

src/commands/command-handler.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
export abstract class TreeViewCommandHandler<TData> {
2-
readonly input: unknown;
3-
1+
export abstract class CommandHandler {
42
abstract handle(): Promise<void> | void;
3+
}
4+
5+
export abstract class TreeViewCommandHandler<TData> extends CommandHandler {
6+
readonly input: unknown;
57

68
abstract parseInput(): TData | null;
79
}

src/commands/commands-registration.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { pullPostRemoteUpdates } from './pull-post-remote-updates';
3434
import { extractImages } from './extract-images';
3535
import { clearPostsSearchResults, refreshPostsSearchResults, searchPosts } from './posts-list/search';
3636
import { handleDeletePostCategories } from './post-category/delete-selected-categories';
37+
import { PublishIngCommandHandler } from '@/commands/ing/publish-ing';
3738

3839
export const registerCommands = () => {
3940
const context = globalState.extensionContext;
@@ -77,6 +78,10 @@ export const registerCommands = () => {
7778
commands.registerCommand(`${appName}.search-posts`, searchPosts),
7879
commands.registerCommand(`${appName}.clear-posts-search-results`, clearPostsSearchResults),
7980
commands.registerCommand(`${appName}.refresh-posts-search-results`, refreshPostsSearchResults),
81+
commands.registerCommand(`${appName}.ing.publish`, () => new PublishIngCommandHandler('input').handle()),
82+
commands.registerCommand(`${appName}.ing.publish-selection`, () =>
83+
new PublishIngCommandHandler('selection').handle()
84+
),
8085
];
8186
context?.subscriptions.push(...disposables);
8287
};

src/commands/ing/publish-ing.ts

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { CommandHandler } from '@/commands/command-handler';
2+
import { IngPublishModel } from '@/models/ing';
3+
import { AlertService } from '@/services/alert.service';
4+
import { globalState } from '@/services/global-state';
5+
import { IngApi } from '@/services/ing.api';
6+
import { InputStep, MultiStepInput, QuickPickParameters } from '@/services/multi-step-input';
7+
import { commands, MessageOptions, ProgressLocation, QuickPickItem, Uri, window } from 'vscode';
8+
9+
export class PublishIngCommandHandler extends CommandHandler {
10+
readonly maxLength = 0;
11+
readonly operation = '发布闪存';
12+
readonly editingText = '编辑闪存';
13+
readonly inputStep: Record<'content' | 'access' | 'tags', InputStep> = {
14+
content: async input => {
15+
this.currentStep = 1;
16+
this.inputContent = await input.showInputBox({
17+
title: this.editingText + ' - 内容',
18+
value: this.inputContent,
19+
prompt: '你在做什么? 你在想什么?',
20+
totalSteps: Object.keys(this.inputStep).length,
21+
step: this.currentStep,
22+
validateInput: v => Promise.resolve(v.length > 2000 ? '最多输入2000个字符' : undefined),
23+
shouldResume: () => Promise.resolve(false),
24+
});
25+
return this.inputContent ? this.inputStep.access : undefined;
26+
},
27+
access: async input => {
28+
this.currentStep = 2;
29+
const items = [
30+
{ label: '公开', value: false },
31+
{ label: '仅自己', value: true },
32+
];
33+
const activeItem = items.filter(x => x.value === this.inputIsPrivate);
34+
const result = <QuickPickItem & { value: boolean }>await input.showQuickPick<
35+
QuickPickItem & { value: boolean },
36+
QuickPickParameters<QuickPickItem & { value: boolean }>
37+
>({
38+
title: this.editingText + ' - 访问权限',
39+
placeholder: '',
40+
step: this.currentStep,
41+
totalSteps: Object.keys(this.inputStep).length,
42+
items: items,
43+
activeItems: activeItem,
44+
canSelectMany: false,
45+
shouldResume: () => Promise.resolve(false),
46+
});
47+
if (result && result.value != null) {
48+
this.inputIsPrivate = result.value;
49+
return this.inputStep.tags;
50+
}
51+
},
52+
tags: async input => {
53+
this.currentStep = 3;
54+
const value = await input.showInputBox({
55+
title: this.editingText + '标签(非必填)',
56+
step: this.currentStep,
57+
totalSteps: Object.keys(this.inputStep).length,
58+
placeHolder: '在此输入标签',
59+
shouldResume: () => Promise.resolve(false),
60+
prompt: '输入标签, 以 "," 分隔',
61+
validateInput: () => Promise.resolve(undefined),
62+
value: this.inputTags.join(', '),
63+
});
64+
this.inputTags = value
65+
.split(/, ?/)
66+
.map(x => x.trim())
67+
.filter(x => !!x);
68+
},
69+
};
70+
inputTags: string[] = [];
71+
inputContent = '';
72+
inputIsPrivate = false;
73+
currentStep = 0;
74+
75+
constructor(public readonly contentSource: 'selection' | 'input' = 'selection') {
76+
super();
77+
}
78+
79+
private get formattedIngContent() {
80+
return `${this.inputTags.map(x => `[${x}]`).join('')}${this.inputContent}`;
81+
}
82+
83+
async handle() {
84+
const content = await this.getContent();
85+
if (!content) return;
86+
const api = new IngApi();
87+
await this.onPublished(
88+
await window.withProgress({ location: ProgressLocation.Notification, title: '正在发闪, 请稍候...' }, p => {
89+
p.report({ increment: 30 });
90+
return api.publishIng(content).then(isPublished => {
91+
p.report({ increment: 70 });
92+
return isPublished;
93+
});
94+
})
95+
);
96+
}
97+
98+
private getContent(): Promise<IngPublishModel | false> {
99+
switch (this.contentSource) {
100+
case 'selection':
101+
return this.getContentFromSelection();
102+
case 'input':
103+
return this.acquireInputContent();
104+
}
105+
}
106+
107+
private getContentFromSelection(): Promise<IngPublishModel | false> {
108+
const text = window.activeTextEditor?.document.getText(window.activeTextEditor?.selection);
109+
if (!text) {
110+
this.warnNoSelection();
111+
return Promise.resolve(false);
112+
}
113+
this.inputContent = text;
114+
return this.acquireInputContent();
115+
}
116+
117+
private async acquireInputContent(step = this.inputStep.content): Promise<IngPublishModel | false> {
118+
await MultiStepInput.run(step);
119+
return this.inputContent &&
120+
this.currentStep === Object.keys(this.inputStep).length &&
121+
(await this.confirmPublish())
122+
? {
123+
content: this.formattedIngContent,
124+
isPrivate: this.inputIsPrivate,
125+
}
126+
: false;
127+
}
128+
129+
private async confirmPublish(): Promise<boolean> {
130+
const items = [
131+
['确定', () => Promise.resolve(true)],
132+
['编辑内容', async () => (await this.acquireInputContent(this.inputStep.content)) !== false],
133+
['编辑访问权限', async () => (await this.acquireInputContent(this.inputStep.access)) !== false],
134+
['编辑标签', async () => (await this.acquireInputContent(this.inputStep.tags)) !== false],
135+
] as const;
136+
const selected = await window.showInformationMessage(
137+
'确定要发布闪存吗?',
138+
{
139+
modal: true,
140+
detail: '📝' + this.formattedIngContent + (this.inputIsPrivate ? '\n\n🔒仅自己可见' : ''),
141+
} as MessageOptions,
142+
...items.map(([title]) => title)
143+
);
144+
return (await items.find(x => x[0] === selected)?.[1].call(null)) ?? false;
145+
}
146+
147+
private warnNoSelection() {
148+
AlertService.warning(`无法${this.operation}, 当前没有选中的内容`);
149+
}
150+
151+
private async onPublished(isPublished: boolean): Promise<void> {
152+
if (isPublished) {
153+
const options = [
154+
[
155+
'打开闪存',
156+
(): Thenable<void> => commands.executeCommand('vscode.open', Uri.parse(globalState.config.ingSite)),
157+
],
158+
[
159+
'我的闪存',
160+
(): Thenable<void> =>
161+
commands.executeCommand('vscode.open', Uri.parse(globalState.config.ingSite + '/#my')),
162+
],
163+
[
164+
'新回应',
165+
(): Thenable<void> =>
166+
commands.executeCommand(
167+
'vscode.open',
168+
Uri.parse(globalState.config.ingSite + '/#recentcomment')
169+
),
170+
],
171+
[
172+
'提到我',
173+
(): Thenable<void> =>
174+
commands.executeCommand('vscode.open', Uri.parse(globalState.config.ingSite + '/#mention')),
175+
],
176+
] as const;
177+
const option = await window.showInformationMessage(
178+
'闪存已发布, 快去看看吧',
179+
{ modal: false },
180+
...options.map(v => ({ title: v[0], id: v[0] }))
181+
);
182+
if (option) return options.find(x => x[0] === option.id)?.[1].call(null);
183+
}
184+
}
185+
}

src/models/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export interface IConfig {
1313
revocationEndpoint: string;
1414
};
1515
apiBaseUrl: string;
16+
ingSite: string;
17+
cnblogsOpenApiUrl: string;
1618
}
1719

1820
export const isDev = () => process.env.NODE_ENV === 'Development';
@@ -30,6 +32,8 @@ export const defaultConfig: IConfig = {
3032
revocationEndpoint: '/connection/revocation',
3133
},
3234
apiBaseUrl: 'https://i.cnblogs.com',
35+
ingSite: 'https://ing.cnblogs.com',
36+
cnblogsOpenApiUrl: 'https://api.cnblogs.com',
3337
};
3438

3539
export const devConfig = Object.assign({}, defaultConfig, {
@@ -39,4 +43,6 @@ export const devConfig = Object.assign({}, defaultConfig, {
3943
clientSecret: env.ClientSecret ? env.ClientSecret : '',
4044
}),
4145
apiBaseUrl: 'https://admin.cnblogs.com',
46+
ingSite: 'https://my-ing.cnblogs.com',
47+
cnblogsOpenApiUrl: 'https://my-api.cnblogs.com',
4248
});

src/models/ing.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export class Ing {
2+
id = -1;
3+
content = '';
4+
isPrivate = false;
5+
6+
static parse(this: void, value: unknown): Ing {
7+
return Object.assign(new Ing(), typeof value === 'object' ? value : {});
8+
}
9+
}
10+
11+
export type IngPublishModel = Pick<Ing, 'content' | 'isPrivate'>;

src/services/ing.api.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { IngPublishModel } from '@/models/ing';
2+
import { accountService } from '@/services/account.service';
3+
import { AlertService } from '@/services/alert.service';
4+
import { globalState } from '@/services/global-state';
5+
import fetch from 'node-fetch';
6+
7+
export class IngApi {
8+
async publishIng(ing: IngPublishModel): Promise<boolean> {
9+
const resp = await fetch(`${globalState.config.cnblogsOpenApiUrl}/api/statuses`, {
10+
method: 'POST',
11+
body: JSON.stringify(ing),
12+
headers: [accountService.buildBearerAuthorizationHeader(), ['Content-Type', 'application/json']],
13+
}).catch(reason => void AlertService.warning(JSON.stringify(reason)));
14+
if (!resp || !resp.ok)
15+
AlertService.error(`闪存发布失败, ${resp?.statusText ?? ''} ${JSON.stringify((await resp?.text()) ?? '')}`);
16+
17+
return resp != null && resp.ok;
18+
}
19+
}

0 commit comments

Comments
 (0)