Skip to content

Commit 709d317

Browse files
authored
feat: copy link of a post (#103)
1 parent 6b85274 commit 709d317

File tree

10 files changed

+142
-20
lines changed

10 files changed

+142
-20
lines changed

README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
[![Current Version](https://vsmarketplacebadge.apphb.com/version-short/cnblogs.vscode-cnb.svg)](https://marketplace.visualstudio.com/items?itemName=cnblogs.vscode-cnb&ssr=false#overview)
2-
[![](https://vsmarketplacebadge.apphb.com/downloads-short/cnblogs.vscode-cnb.svg)](https://marketplace.visualstudio.com/items?itemName=cnblogs.vscode-cnb&ssr=false#overview)
3-
[![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/cnblogs/vscode-cnb/Build%20and%20check%20the%20code%20format/main)](https://github.com/cnblogs/vscode-cnb)
1+
[![Current Version](https://vsmarketplacebadges.dev/version-short/cnblogs.vscode-cnb.svg)](https://marketplace.visualstudio.com/items?itemName=cnblogs.vscode-cnb&ssr=false#overview)
2+
[![](https://vsmarketplacebadges.dev/installs-short/cnblogs.vscode-cnb.svg)](https://marketplace.visualstudio.com/items?itemName=cnblogs.vscode-cnb&ssr=false#overview)
3+
[![](https://vsmarketplacebadges.dev/rating-short/cnblogs.vscode-cnb.svg
4+
)]((https://marketplace.visualstudio.com/items?itemName=cnblogs.vscode-cnb&ssr=false#overview))
5+
[![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/cnblogs/vscode-cnb/build-check.yml)](https://github.com/cnblogs/vscode-cnb)
46
[![GitHub](https://img.shields.io/github/license/cnblogs/vscode-cnb)](https://github.com/cnblogs/vscode-cnb/blob/main/LICENSE.txt)
57
[![GitHub issues](https://img.shields.io/github/issues-raw/cnblogs/vscode-cnb)](https://github.com/cnblogs/vscode-cnb/issues)
68

@@ -21,6 +23,7 @@
2123
- [博文设置面板](#博文设置面板)
2224
- [闪存](#闪存)
2325
- [markdown语法扩展](#markdown语法扩展)
26+
- [复制博文链接](#复制博文链接)
2427
- [vscode 版本要求](#vscode-版本要求)
2528
- [插件设置](#插件设置)
2629

@@ -181,6 +184,18 @@
181184

182185
<kbd><img height="550" src="https://img2023.cnblogs.com/blog/35695/202211/35695-20221129171115024-35740390.png"></kbd>
183186

187+
### 复制博文链接
188+
189+
文件浏览器和随笔列表中的上下文菜单里有`复制博文链接`选项, 点击后可以复制不同格式的博文链接
190+
191+
<kbd><img height="500" alt="https://img2023.cnblogs.com/blog/35695/202301/35695-20230130155516202-1979736560.png" src="https://img2023.cnblogs.com/blog/35695/202301/35695-20230130155516202-1979736560.png"><kbd>
192+
193+
194+
默认的的链接形如: `https://www.cnblogs.com/cmt/p/47365.html`
195+
markdown格式链接形如: `[博文标题](https://www.cnblogs.com/cmt/p/47365.html)`
196+
也可以选择仅复制博文的Id
197+
198+
184199
## vscode 版本要求
185200

186201
\>=1.62.0

package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,12 @@
358358
"title": "在浏览器中打开闪存",
359359
"icon": "$(globe)",
360360
"enablement": "vscode-cnb.isAuthorized"
361+
},
362+
{
363+
"command": "vscode-cnb.copy-post-link",
364+
"title": "复制博文链接",
365+
"icon": "$(link)",
366+
"enablement": "vscode-cnb.isAuthorized"
361367
}
362368
],
363369
"configuration": [
@@ -703,6 +709,10 @@
703709
{
704710
"command": "vscode-cnb.ings-list.open-in-browser",
705711
"when": "false"
712+
},
713+
{
714+
"command": "vscode-cnb.copy-post-link",
715+
"when": "false"
706716
}
707717
],
708718
"view/item/context": [
@@ -775,6 +785,11 @@
775785
"when": "viewItem =~ /^cnb-post/ && viewItem != cnb-post-category",
776786
"group": "0@5"
777787
},
788+
{
789+
"command": "vscode-cnb.copy-post-link",
790+
"when": "viewItem =~ /^cnb-post/ && viewItem != cnb-post-category",
791+
"group": "0@6"
792+
},
778793
{
779794
"command": "vscode-cnb.delete-selected-post-categories",
780795
"when": "viewItem == cnb-post-category"
@@ -918,6 +933,11 @@
918933
"command": "vscode-cnb.export-post-to-pdf",
919934
"when": "resourceLangId == markdown",
920935
"group": "cnblogs@7"
936+
},
937+
{
938+
"command": "vscode-cnb.copy-post-link",
939+
"when": "resourceLangId == markdown",
940+
"group": "cnblogs@8"
921941
}
922942
]
923943
},

src/commands/command-handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export abstract class CommandHandler {
55
export abstract class TreeViewCommandHandler<TData> extends CommandHandler {
66
readonly input: unknown;
77

8-
abstract parseInput(): TData | null;
8+
abstract parseInput(): TData | null | undefined;
99
}
1010

1111
export abstract class MultiSelectableTreeViewCommandHandler<TArgument, TData>

src/commands/commands-registration.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { clearPostsSearchResults, refreshPostsSearchResults, searchPosts } from
3636
import { handleDeletePostCategories } from './post-category/delete-selected-categories';
3737
import { PublishIngCommandHandler } from '@/commands/ing/publish-ing';
3838
import { registerCommandsForIngsList } from 'src/commands/ing/ings-list-commands-registration';
39+
import { CopyPostLinkCommandHandler } from '@/commands/posts-list/copy-link';
3940

4041
export const registerCommands = () => {
4142
const context = globalState.extensionContext;
@@ -79,6 +80,7 @@ export const registerCommands = () => {
7980
commands.registerCommand(`${appName}.search-posts`, searchPosts),
8081
commands.registerCommand(`${appName}.clear-posts-search-results`, clearPostsSearchResults),
8182
commands.registerCommand(`${appName}.refresh-posts-search-results`, refreshPostsSearchResults),
83+
commands.registerCommand(`${appName}.copy-post-link`, input => new CopyPostLinkCommandHandler(input).handle()),
8284
commands.registerCommand(`${appName}.ing.publish`, () => new PublishIngCommandHandler('input').handle()),
8385
commands.registerCommand(`${appName}.ing.publish-selection`, () =>
8486
new PublishIngCommandHandler('selection').handle()
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { TreeViewCommandHandler } from '@/commands/command-handler';
2+
import { Post } from '@/models/post';
3+
import { AlertService } from '@/services/alert.service';
4+
import { PostFileMapManager } from '@/services/post-file-map';
5+
import { postService } from '@/services/post.service';
6+
import { PostTreeItem } from '@/tree-view-providers/models/post-tree-item';
7+
import { env, MessageItem, Uri, window } from 'vscode';
8+
9+
type LinkFormat = 'markdown' | 'raw' | 'id';
10+
interface CopyStrategy {
11+
name: string;
12+
provideContent: (post: Post) => Thenable<string>;
13+
}
14+
15+
export class CopyPostLinkCommandHandler extends TreeViewCommandHandler<Thenable<Post | null | undefined>> {
16+
private readonly _strategies: { [key in LinkFormat]: CopyStrategy } = {
17+
raw: {
18+
name: '复制链接',
19+
provideContent: ({ url }) => Promise.resolve(url),
20+
},
21+
markdown: {
22+
name: '复制markdown格式链接',
23+
provideContent: ({ url, title }) => Promise.resolve(`[${title}](${url})`),
24+
},
25+
id: {
26+
name: '复制Id',
27+
provideContent: ({ id }) => Promise.resolve(`${id}`),
28+
},
29+
};
30+
31+
constructor(public readonly input: unknown) {
32+
super();
33+
}
34+
35+
async handle(): Promise<void> {
36+
const post = await this.parseInput();
37+
38+
if (post == null) return;
39+
40+
const linkFormat = await this.askFormat();
41+
if (linkFormat == null) return;
42+
43+
const contentToCopy = await this._strategies[linkFormat].provideContent(post);
44+
if (contentToCopy.length > 0) await env.clipboard.writeText(contentToCopy);
45+
}
46+
47+
parseInput(): Thenable<Post | null | undefined> {
48+
const { input } = this;
49+
if (input instanceof Post) {
50+
return Promise.resolve(input);
51+
} else if (input instanceof PostTreeItem) {
52+
return Promise.resolve(input.post);
53+
} else if (input instanceof Uri) {
54+
const postId = PostFileMapManager.findByFilePath(input.fsPath)?.[0];
55+
return postId == null || postId <= 0
56+
? Promise.resolve(undefined).then(() => void AlertService.fileNotLinkedToPost(input))
57+
: postService.fetchPostEditDto(postId).then(v => v?.post);
58+
}
59+
60+
return Promise.resolve(undefined);
61+
}
62+
63+
private askFormat(): Thenable<LinkFormat | undefined | null> {
64+
return window
65+
.showInformationMessage(
66+
'选择链接格式',
67+
{ modal: true },
68+
...(Object.keys(this._strategies) as LinkFormat[]).map<MessageItem & { format: LinkFormat }>(f => ({
69+
title: this._strategies[f].name,
70+
format: f,
71+
isCloseAffordance: false,
72+
}))
73+
)
74+
.then(x => x?.format);
75+
}
76+
}

src/commands/posts-list/modify-post-settings.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { postService } from '../../services/post.service';
55
import { PostFileMapManager } from '../../services/post-file-map';
66
import { revealPostsListItem } from '../../services/posts-list-view';
77
import { postConfigurationPanel } from '../../services/post-configuration-panel.service';
8-
import path from 'path';
98
import fs from 'fs';
109
import { LocalDraft } from '../../services/local-draft.service';
1110
import { saveFilePendingChanges } from '../../utils/save-file-pending-changes';
@@ -23,11 +22,7 @@ export const modifyPostSettings = async (input: Post | PostTreeItem | Uri) => {
2322
postId = input.id;
2423
} else if (input instanceof Uri) {
2524
postId = PostFileMapManager.getPostId(input.fsPath) ?? -1;
26-
const filename = path.basename(input.fsPath, path.extname(input.fsPath));
27-
if (postId < 0) {
28-
AlertService.warning(`本地文件 "${filename}" 未关联博客园博文`);
29-
return;
30-
}
25+
if (postId < 0) return AlertService.fileNotLinkedToPost(input);
3126
}
3227

3328
if (!(postId >= 0)) return;

src/commands/pull-post-remote-updates.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,8 @@ const parseUriInput = (input: InputType): Uri | undefined => {
5757

5858
const handleUriInput = (fileUri: Uri, contexts: CommandContext[]): Promise<void> => {
5959
const postId = PostFileMapManager.getPostId(fileUri.fsPath);
60-
if (!postId) {
61-
AlertService.warning(
62-
`本地文件"${path.basename(fileUri.fsPath, path.extname(fileUri.fsPath))}"未关联博客园博文`
63-
);
64-
return Promise.resolve();
65-
}
60+
if (!postId) return Promise.resolve().then(() => AlertService.fileNotLinkedToPost(fileUri));
61+
6662
contexts.push({ postId, fileUri });
6763
return Promise.resolve();
6864
};

src/commands/view-post-online.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,5 @@ export const viewPostOnline = async (input?: Post | PostTreeItem | Uri) => {
1515

1616
if (!post) return;
1717

18-
const url = post.url.startsWith('//') ? `https:${post.url}` : post.url;
19-
await commands.executeCommand('vscode.open', Uri.parse(url));
18+
await commands.executeCommand('vscode.open', Uri.parse(post.url));
2019
};

src/models/post.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ export class Post {
3434
siteCategoryId?: number;
3535
tags?: string[];
3636
title = '';
37-
url = '';
3837

38+
private _url = '';
3939
private _dateUpdated?: Date | undefined;
4040
private _datePublished = new Date();
4141

@@ -53,6 +53,14 @@ export class Post {
5353
this._dateUpdated = typeof value === 'string' ? parseISO(value) : value;
5454
}
5555

56+
get url() {
57+
const { _url } = this;
58+
return _url.startsWith('//') ? (this._url = `https:${_url}`) : _url;
59+
}
60+
set url(value) {
61+
this._url = value;
62+
}
63+
5664
get accessPermissionDesc(): string {
5765
switch (this.accessPermission) {
5866
case AccessPermission.authenticated:

src/services/alert.service.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import vscode from 'vscode';
1+
import path from 'path';
2+
import vscode, { Uri } from 'vscode';
23

34
export class AlertService {
45
static error(message: string) {
@@ -12,4 +13,14 @@ export class AlertService {
1213
static warning(message: string) {
1314
vscode.window.showWarningMessage(message).then(undefined, undefined);
1415
}
16+
17+
/**
18+
* alert that file not linked to the post
19+
* @param file the file path, could be a string or {@link Uri} object
20+
*/
21+
static fileNotLinkedToPost(file: string | Uri, { trimExt = true } = {}) {
22+
file = file instanceof Uri ? file.fsPath : file;
23+
file = trimExt ? path.basename(file, path.extname(file)) : file;
24+
this.warning(`本地文件"${file}"未关联博客园博文`);
25+
}
1526
}

0 commit comments

Comments
 (0)