├── images └── whats-new-bookmarks.png ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── question.md │ └── bug_report.md ├── CONTRIBUTING.md ├── LICENSE.md ├── src ├── ContentProvider.ts ├── Manager.ts └── PageBuilder.ts ├── ui ├── whats-new.html └── main.css └── README.md /images/whats-new-bookmarks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alefragnani/vscode-whats-new/HEAD/images/whats-new-bookmarks.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: alefragnani 2 | patreon: alefragnani 3 | custom: https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=EP57F3B6FXKTU&lc=US&item_name=Alessandro%20Fragnani&item_number=vscode%20extensions¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donate_SM%2egif%3aNonHosted 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for the module 4 | title: "[FEATURE] - " 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question about the module 4 | title: "[QUESTION] - " 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help the module improve 4 | title: "[BUG] - " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | 13 | **Environment/version** 14 | 15 | - Extension version: 16 | - VSCode version: 17 | - OS version: 18 | 19 | **Steps to reproduce** 20 | 21 | 1. 22 | 2. 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First off all, thank you for taking the time to contribute! 4 | 5 | When contributing to this project, please first discuss the changes you wish to make via an issue before making changes. 6 | 7 | ## Your First Code Contribution 8 | 9 | Unsure where to begin contributing? You can start by looking through the [`help wanted`](https://github.com/alefragnani/vscode-whats-new/labels/help%20wanted) issues. 10 | 11 | ### Getting the code 12 | 13 | This repo is intended to be used as a `git submodule`. So, the best approach is to use it this way. 14 | 15 | Follow the [README](./README.md) instructions on how to use this submodule on your extension. 16 | 17 | ## Submitting a Pull Request 18 | 19 | Be sure your branch is up to date (relative to `master`) and submit your PR. Also add reference to the issue the PR refers to. -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Alessandro Fragnani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/ContentProvider.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Alessandro Fragnani. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | // common 7 | export interface Image { 8 | src: string; 9 | width: number; 10 | height: number; 11 | } 12 | 13 | // header 14 | export interface Header { 15 | logo: Image; 16 | message: string; 17 | } 18 | 19 | // changelog 20 | export enum ChangeLogKind { 21 | NEW = "NEW", 22 | CHANGED = "CHANGED", 23 | FIXED = "FIXED", 24 | VERSION = "VERSION", 25 | INTERNAL = "INTERNAL" 26 | } 27 | 28 | export enum IssueKind { 29 | Issue = "Issue", 30 | PR = "PR" 31 | } 32 | 33 | export interface ChangeLogIssue { 34 | message: string; 35 | id: number; 36 | kind: IssueKind; 37 | kudos?: string; 38 | } 39 | 40 | export interface ChangeLogVersion { 41 | releaseNumber: string; 42 | releaseDate: string; 43 | } 44 | 45 | export interface ChangeLogItem { 46 | kind: ChangeLogKind; 47 | detail: ChangeLogIssue | ChangeLogVersion | string ; 48 | } 49 | 50 | export interface Image { 51 | light: string; 52 | dark: string 53 | } 54 | 55 | // sponsor 56 | export interface Sponsor { 57 | title: string; 58 | link: string; 59 | image: Image; 60 | width: number; 61 | message: string; 62 | extra: string; 63 | } 64 | 65 | export interface SupportChannel { 66 | title: string; 67 | link: string; 68 | message: string; 69 | } 70 | 71 | export interface ContentProvider { 72 | provideHeader(logoUrl: string): Header; 73 | provideChangeLog(): ChangeLogItem[]; 74 | provideSupportChannels(): SupportChannel[]; 75 | } 76 | 77 | export interface SponsorProvider { 78 | provideSponsors(): Sponsor[]; 79 | } 80 | 81 | export interface SocialMedia { 82 | link: string; 83 | title: string; 84 | } 85 | 86 | export interface SocialMediaProvider { 87 | provideSocialMedias(): SocialMedia[]; 88 | } -------------------------------------------------------------------------------- /ui/whats-new.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 | 23 | 24 | 25 |

26 | ${headerMessage} 27 |

28 |
29 | 30 |
31 |
32 | 33 |
34 |

What's New in ${extensionDisplayName} ${extensionVersion}

35 |
36 |
    37 | ${changeLog} 38 |
39 |
40 |
41 | 42 |

Need Help?

43 | 51 | 52 |
53 |
54 | 68 | 71 |
72 |
73 |
74 |
75 | 76 | 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What's New submodule 2 | 3 | This submodule is used to display a **What's New** page on VS Code extensions. It has a simple (but yet effective) UI, optimized to display all the information in a single page. 4 | 5 | ![whats-new-bookmarks](images/whats-new-bookmarks.png) 6 | 7 | ## Usage 8 | 9 | ### Adding 10 | 11 | You just need to add a `submodule` reference to your Git repo 12 | 13 | ``` 14 | git submodule add https://github.com/alefragnani/vscode-whats-new.git 15 | 16 | ``` 17 | 18 | ### Coding 19 | 20 | This submodule is based on a `Provider` pattern (similar to some pieces of VS Code), and you need to implement at least the `ContentProvider` in order to display the main content of the page, like the Header (logo and message) and Changelog. 21 | 22 | There are other two providers available, for specific contents: 23 | 24 | * `SocialMediaProvider`": Add links to your social medias 25 | * `SponsorProvider`: Add references (logos and messages) to your sponsors. 26 | 27 | ```ts 28 | 29 | // provide the data 30 | import { ChangeLogItem, 31 | ChangeLogKind, 32 | ContentProvider, 33 | SocialMediaProvider, 34 | SponsorProvider, 35 | Header, 36 | Image 37 | } from "../../vscode-whats-new/src/ContentProvider"; 38 | 39 | export class BookmarksContentProvider implements ContentProvider { 40 | 41 | provideHeader(logoUrl: string): Header { 42 | return
{logo: {src: logoUrl, height: 50, width: 50}, 43 | message: `Bookmarks helps you to navigate in your code, moving 44 | between important positions easily and quickly. No more need 45 | to search for code. It also supports a set of selection 46 | commands, which allows you to select bookmarked lines and regions between 47 | lines.`}; 48 | } 49 | 50 | provideChangeLog(): ChangeLogItem[] { 51 | let changeLog: ChangeLogItem[] = []; 52 | changeLog.push({ kind: ChangeLogKind.VERSION, detail: { releaseNumber: "12.1.0", releaseDate: "December 2020" } }); 53 | changeLog.push({ 54 | kind: ChangeLogKind.NEW, 55 | detail: { 56 | message: "Support submenu for editor commands", 57 | id: 351, 58 | kind: IssueKind.Issue 59 | } 60 | }); 61 | changeLog.push({ 62 | kind: ChangeLogKind.CHANGED, 63 | detail: { 64 | message: "Setting bookmarks.navigateThroughAllFiles is now true by default", 65 | id: 102, 66 | kind: IssueKind.Issue 67 | } 68 | }); 69 | changeLog.push({ 70 | kind: ChangeLogKind.INTERNAL, 71 | detail: { 72 | message: "Remove unnecessary files from extension package", 73 | id: 355, 74 | kind: IssueKind.Issue 75 | } 76 | }); 77 | } 78 | } 79 | 80 | export class BookmarksSocialMediaProvider implements SocialMediaProvider { 81 | public provideSocialMedias() { 82 | return [{ 83 | title: "Follow me on Twitter", 84 | link: "https://www.twitter.com/alefragnani" 85 | }]; 86 | } 87 | } 88 | 89 | export class BookmarksSponsorProvider implements SponsorProvider { 90 | public provideSponsors(): Sponsor[] { 91 | const sponsors: Sponsor[] = []; 92 | const sponsorCodeStream: Sponsor = { 93 | title: "Learn more about Codestream", 94 | link: "https://sponsorlink.codestream.com/?utm_source=vscmarket&utm_campaign=bookmarks&utm_medium=banner", 95 | image: { 96 | light: "https://alt-images.codestream.com/codestream_logo_bookmarks.png", 97 | dark: "https://alt-images.codestream.com/codestream_logo_bookmarks.png" 98 | }, 99 | width: 52, 100 | message: `

Eliminate context switching and costly distractions. 101 | Create and merge PRs and perform code reviews from inside your 102 | IDE while using jump-to-definition, your keybindings, and other IDE favorites.

`, 103 | extra: 104 | ` 105 | Learn more` 106 | }; 107 | sponsors.push(sponsorCodeStream); 108 | return sponsors; 109 | } 110 | } 111 | 112 | // register the providers 113 | const provider = new BookmarksContentProvider(); 114 | const viewer = new WhatsNewManager(context) 115 | .registerContentProvider("alefragnani", "bookmarks", provider) 116 | .registerSocialMediaProvider(new BookmarksSocialMediaProvider()) 117 | .registerSponsorProvider(new BookmarksSponsorProvider()); 118 | 119 | // show the page (if necessary) 120 | await viewer.showPageInActivation(); 121 | 122 | // register the additional command (not really necessary, unless you want a command registered in your extension) 123 | context.subscriptions.push(vscode.commands.registerCommand("bookmarks.whatsNew", () => viewer.showPage())); 124 | ``` 125 | ## Features 126 | 127 | ### Detects version updates 128 | 129 | It follows [SEMVER - Semantic Versioning](https://www.semver.org) to detect **Major**, **Minor** and **Patch** versions. The **What's New** page will only be displayed when a **Major** or **Minor** update occurs. **Patches** are updated silently. 130 | 131 | ### Template Based 132 | 133 | I don't have to deal with HTML or CSS on my extensions anymore. I just have to _provide_ the relevant information and the HTML page is automatically generated/updated. 134 | 135 | ## Inspiration 136 | 137 | The idea came from the [GitLens extension](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens) by @eamodio (big thanks to Eric Amodio :claps:). Based on pieces of its welcome page, I created this template based engine, to be able to use in any of my extensions, with minimum effort. 138 | 139 | # License 140 | 141 | [MIT](LICENSE.md) © Alessandro Fragnani -------------------------------------------------------------------------------- /src/Manager.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Alessandro Fragnani. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as semver from "semver"; 7 | import * as vscode from "vscode"; 8 | import { Uri, Webview } from "vscode"; 9 | import { ContentProvider, SocialMediaProvider, SponsorProvider } from "./ContentProvider"; 10 | import { WhatsNewPageBuilder } from "./PageBuilder"; 11 | 12 | export class WhatsNewManager { 13 | 14 | private publisher!: string; 15 | private extensionName!: string; 16 | private context: vscode.ExtensionContext; 17 | private contentProvider!: ContentProvider; 18 | private socialMediaProvider!: SocialMediaProvider | undefined; 19 | private sponsorProvider: SponsorProvider | undefined; 20 | 21 | private extension!: vscode.Extension; 22 | private versionKey!: string; 23 | 24 | constructor(context: vscode.ExtensionContext) { 25 | this.context = context; 26 | } 27 | 28 | private isRunningOnCodespaces(): boolean { 29 | return vscode.env.remoteName?.toLocaleLowerCase() === 'codespaces'; 30 | } 31 | 32 | private isRunningOnGitpod(): boolean { 33 | return !!process.env.GITPOD_WORKSPACE_ID; 34 | } 35 | 36 | public registerContentProvider(publisher: string, extensionName: string, contentProvider: ContentProvider): WhatsNewManager { 37 | this.publisher = publisher; 38 | this.extensionName = extensionName 39 | this.contentProvider = contentProvider; 40 | this.versionKey = `${this.extensionName}.version`; 41 | 42 | this.context.globalState.setKeysForSync([this.versionKey]); 43 | 44 | return this; 45 | } 46 | 47 | public registerSocialMediaProvider(socialMediaProvider: SocialMediaProvider): WhatsNewManager { 48 | this.socialMediaProvider = socialMediaProvider; 49 | return this; 50 | } 51 | 52 | public registerSponsorProvider(sponsorProvider: SponsorProvider): WhatsNewManager { 53 | this.sponsorProvider = sponsorProvider; 54 | return this; 55 | } 56 | 57 | public async showPageInActivation() { 58 | // load data from extension manifest 59 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 60 | this.extension = vscode.extensions.getExtension(`${this.publisher}.${this.extensionName}`)!; 61 | 62 | const previousExtensionVersion = this.context.globalState.get(this.versionKey); 63 | 64 | await this.showPageIfCurrentVersionIsGreaterThanPrevisouVersion(this.extension.packageJSON.version, previousExtensionVersion); 65 | } 66 | 67 | public async showPage() { 68 | 69 | // Create and show panel 70 | const panel = vscode.window.createWebviewPanel(`${this.extensionName}.whatsNew`, 71 | `What's New in ${this.extension.packageJSON.displayName}`, vscode.ViewColumn.One, { enableScripts: true }); 72 | 73 | // Path to HTML 74 | const onDiskPath = vscode.Uri.joinPath(this.context.extensionUri, "vscode-whats-new", "ui", "whats-new.html"); 75 | 76 | // Path to CSS 77 | const cssPathOnDisk = vscode.Uri.joinPath(this.context.extensionUri, "vscode-whats-new", "ui", "main.css"); 78 | 79 | // Path to Logo 80 | const logoPathOnDisk = vscode.Uri.joinPath(this.context.extensionUri, "images", `vscode-${this.extensionName.toLowerCase()}-logo-readme.png`); 81 | 82 | panel.webview.html = await this.getWebviewContentLocal(panel.webview, onDiskPath, cssPathOnDisk, logoPathOnDisk); 83 | } 84 | 85 | public async showPageIfVersionDiffers(currentVersion: string, previousVersion: string | undefined) { 86 | 87 | if (previousVersion) { 88 | const differs: semver.ReleaseType | null = semver.diff(currentVersion, previousVersion); 89 | 90 | // only "patch" should be suppressed 91 | if (!differs || differs === "patch") { 92 | return; 93 | } 94 | } 95 | 96 | // "major", "minor" 97 | this.context.globalState.update(this.versionKey, currentVersion); 98 | 99 | // 100 | if (this.isRunningOnCodespaces() || this.isRunningOnGitpod()) { 101 | return; 102 | } 103 | 104 | await this.showPage(); 105 | } 106 | 107 | public async showPageIfCurrentVersionIsGreaterThanPrevisouVersion(currentVersion: string, previousVersion: string | undefined) { 108 | if (previousVersion) { 109 | const differs: semver.ReleaseType | null = semver.diff(currentVersion, previousVersion); 110 | const isGreaterThanPreviousVersion = semver.gt(currentVersion, previousVersion); 111 | 112 | // only "patch" should be suppressed 113 | if (!differs || differs === "patch" || !isGreaterThanPreviousVersion) { 114 | return; 115 | } 116 | } 117 | 118 | // "major", "minor" 119 | this.context.globalState.update(this.versionKey, currentVersion); 120 | 121 | // 122 | if (this.isRunningOnCodespaces() || this.isRunningOnGitpod()) { 123 | return; 124 | } 125 | 126 | await this.showPage(); 127 | } 128 | 129 | private async getWebviewContentLocal(webview: Webview, htmlFile: Uri, cssUrl: Uri, logoUrl: Uri): Promise { 130 | const pageBuilder = await WhatsNewPageBuilder.newBuilder(webview, htmlFile); 131 | let html = pageBuilder.updateExtensionPublisher(this.publisher) 132 | .updateExtensionDisplayName(this.extension.packageJSON.displayName) 133 | .updateExtensionName(this.extensionName) 134 | .updateExtensionVersion(this.extension.packageJSON.version) 135 | .updateRepositoryUrl(this.extension.packageJSON.repository.url.slice( 136 | 0, this.extension.packageJSON.repository.url.length - 4)) 137 | .updateRepositoryIssues(this.extension.packageJSON.bugs.url) 138 | .updateRepositoryHomepage(this.extension.packageJSON.homepage) 139 | .updateCSS(webview.asWebviewUri(cssUrl).toString()) 140 | .updateHeader(this.contentProvider.provideHeader(webview.asWebviewUri(logoUrl).toString())) 141 | .updateChangeLog(this.contentProvider.provideChangeLog()) 142 | .updateSponsors(this.sponsorProvider?.provideSponsors()) 143 | .updateSupportChannels(this.contentProvider.provideSupportChannels()) 144 | .updateSocialMedias(this.socialMediaProvider?.provideSocialMedias()) 145 | .build(); 146 | 147 | html = html 148 | .replace(/#{cspSource}/g, webview.cspSource) 149 | .replace(/#{root}/g, webview.asWebviewUri(this.context.extensionUri).toString()); 150 | 151 | return html; 152 | } 153 | } -------------------------------------------------------------------------------- /src/PageBuilder.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Alessandro Fragnani. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | // import * as fs from "fs"; 7 | import * as semver from "semver"; 8 | import { Uri, Webview, workspace } from "vscode"; 9 | import { ChangeLogItem, ChangeLogIssue, ChangeLogVersion, ChangeLogKind, Header, Sponsor, IssueKind, SupportChannel, SocialMedia } from "./ContentProvider"; 10 | 11 | export async function readRAWFileUri(uri: Uri): Promise { 12 | const bytes = await workspace.fs.readFile(uri); 13 | return new TextDecoder('utf-8').decode(bytes); 14 | } 15 | 16 | export class WhatsNewPageBuilder { 17 | 18 | public static async newBuilder(webview: Webview, htmlFile: Uri): Promise { 19 | const html = await readRAWFileUri(htmlFile); 20 | return new WhatsNewPageBuilder(webview, html); 21 | } 22 | 23 | private htmlFile: string; 24 | private repositoryUrl!: string; 25 | private webview: Webview; 26 | 27 | constructor(webview: Webview, htmlFile: string) { 28 | this.webview = webview; 29 | this.htmlFile = htmlFile; 30 | } 31 | 32 | public updateExtensionPublisher(publisher: string) { 33 | this.htmlFile = this.htmlFile.replace(/\$\{publisher\}/g, publisher); 34 | return this; 35 | } 36 | 37 | public updateExtensionDisplayName(extensionDisplayName: string) { 38 | this.htmlFile = this.htmlFile.replace(/\$\{extensionDisplayName\}/g, extensionDisplayName); 39 | return this; 40 | } 41 | 42 | public updateExtensionName(extensionName: string) { 43 | this.htmlFile = this.htmlFile.replace(/\$\{extensionName\}/g, extensionName); 44 | return this; 45 | } 46 | 47 | public updateExtensionVersion(extensionVersion: string) { 48 | this.htmlFile = this.htmlFile.replace("${extensionVersion}", 49 | `${semver.major(extensionVersion)}.${semver.minor(extensionVersion)}`); 50 | return this; 51 | } 52 | 53 | public updateRepositoryUrl(repositoryUrl: string) { 54 | this.htmlFile = this.htmlFile.replace(/\$\{repositoryUrl\}/g, repositoryUrl); 55 | this.repositoryUrl = repositoryUrl; 56 | return this; 57 | } 58 | 59 | public updateRepositoryIssues(repositoryIssues: string) { 60 | this.htmlFile = this.htmlFile.replace("${repositoryIssues}", repositoryIssues); 61 | return this; 62 | } 63 | 64 | public updateRepositoryHomepage(repositoryHomepage: string) { 65 | this.htmlFile = this.htmlFile.replace("${repositoryHomepage}", repositoryHomepage); 66 | return this; 67 | } 68 | 69 | public updateCSS(cssUrl: string): WhatsNewPageBuilder { 70 | this.htmlFile = this.htmlFile.replace("${cssUrl}", cssUrl); 71 | return this; 72 | } 73 | 74 | public updateHeader(header: Header): WhatsNewPageBuilder { 75 | this.htmlFile = this.htmlFile.replace("${headerLogo}", header.logo.src); 76 | this.htmlFile = this.htmlFile.replace("${headerWidth}", header.logo.width.toString()); 77 | this.htmlFile = this.htmlFile.replace("${headerHeight}", header.logo.height.toString()); 78 | this.htmlFile = this.htmlFile.replace("${headerMessage}", header.message); 79 | return this; 80 | } 81 | 82 | public updateChangeLog(changeLog: ChangeLogItem[]): WhatsNewPageBuilder { 83 | let changeLogString = ""; 84 | 85 | for (const cl of changeLog) { 86 | if (cl.kind === ChangeLogKind.VERSION) { 87 | const cc: ChangeLogVersion = cl.detail; 88 | const borderTop = changeLogString === "" ? "" : "changelog__version__borders__top"; 89 | changeLogString = changeLogString.concat( 90 | `
  • 91 | ${cc.releaseNumber} 92 | ${cc.releaseDate} 93 |
  • `); 94 | } else { 95 | const badge: string = this.getBadgeFromChangeLogKind(cl.kind); 96 | let message: string; 97 | 98 | if (typeof cl.detail === "string") { 99 | message = cl.detail 100 | } else { 101 | const cc: ChangeLogIssue = cl.detail; 102 | if (cc.kind === IssueKind.Issue) { 103 | message = `${cc.message} 104 | (Issue #${cc.id})` 106 | } else { 107 | message = `${cc.message} 108 | (Thanks to ${cc.kudos} - PR #${cc.id})` 110 | } 111 | } 112 | 113 | changeLogString = changeLogString.concat( 114 | `
  • ${cl.kind} 115 | ${message} 116 |
  • ` 117 | ); 118 | } 119 | } 120 | this.htmlFile = this.htmlFile.replace("${changeLog}", changeLogString); 121 | return this; 122 | } 123 | 124 | public updateSponsors(sponsors: Sponsor[] | undefined): WhatsNewPageBuilder { 125 | if (!sponsors || sponsors.length === 0) { 126 | this.htmlFile = this.htmlFile.replace("${sponsors}", ""); 127 | return this; 128 | } 129 | 130 | let sponsorsString = `

    131 |

    Sponsors

    `; 132 | 133 | for (const sp of sponsors) { 134 | if (sp.message) { 135 | sponsorsString = sponsorsString.concat( 136 | ` 137 | 138 | 139 | 140 | ${sp.message} 141 | ${sp.extra}

    ` 142 | ); 143 | } else { 144 | sponsorsString = sponsorsString.concat( 145 | `
    ` 149 | ); 150 | } 151 | } 152 | sponsorsString = sponsorsString.concat("

    "); 153 | this.htmlFile = this.htmlFile.replace("${sponsors}", sponsorsString); 154 | return this; 155 | } 156 | 157 | public updateSupportChannels(supportChannels: SupportChannel[]): WhatsNewPageBuilder { 158 | if (supportChannels.length === 0) { 159 | this.htmlFile = this.htmlFile.replace("${supportChannels}", ""); 160 | return this; 161 | } 162 | 163 | let supportChannelsString = `
    `; 164 | 165 | for (const sc of supportChannels) { 166 | supportChannelsString = supportChannelsString.concat( 167 | ` 168 | ${sc.message} 169 | ` 170 | ) 171 | } 172 | supportChannelsString = supportChannelsString.concat("
    "); 173 | this.htmlFile = this.htmlFile.replace("${supportChannels}", supportChannelsString); 174 | return this; 175 | } 176 | 177 | public updateSocialMedias(socialMedias: SocialMedia[] | undefined): WhatsNewPageBuilder { 178 | if (!socialMedias || socialMedias.length === 0) { 179 | this.htmlFile = this.htmlFile.replace("${socialMedias}", ""); 180 | return this; 181 | } 182 | 183 | let socialMediasString = ''; 184 | 185 | for (const sm of socialMedias) { 186 | socialMediasString = socialMediasString.concat( 187 | `
  • ${sm.title}
  • ` 188 | ); 189 | } 190 | this.htmlFile = this.htmlFile.replace("${socialMedias}", socialMediasString); 191 | return this; 192 | } 193 | 194 | public build(): string { 195 | return this.htmlFile.toString(); 196 | } 197 | 198 | private getBadgeFromChangeLogKind(kind: ChangeLogKind): string { 199 | switch (kind) { 200 | case ChangeLogKind.NEW: 201 | return "added"; 202 | 203 | case ChangeLogKind.CHANGED: 204 | return "changed"; 205 | 206 | case ChangeLogKind.FIXED: 207 | return "fixed"; 208 | 209 | case ChangeLogKind.INTERNAL: 210 | return "internal"; 211 | 212 | default: 213 | return "internal"; 214 | } 215 | } 216 | } -------------------------------------------------------------------------------- /ui/main.css: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Eric Amodio & Alessandro Fragnani. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | * 5 | * Original Author: Eric Amodio @ GitLens 6 | * Modifications by: Alessandro Fragnani 7 | *--------------------------------------------------------------------------------------------*/ 8 | 9 | html { 10 | height: 100%; } 11 | 12 | body { 13 | background-color: var(--vscode-editor-background); 14 | color: var(--vscode-editor-foreground); 15 | font-family: var(--vscode-font-family); 16 | height: 100%; 17 | opacity: 0.85; 18 | line-height: 1.4; } 19 | 20 | a { 21 | border: 0; 22 | color: var(--vscode-textLink-foreground); 23 | font-weight: 400; 24 | outline: none; 25 | text-decoration: none; } 26 | a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover { 27 | color: inherit; 28 | text-decoration: none; } 29 | .vscode-light a:focus { 30 | outline-color: var(--vscode-textLink-foreground); } 31 | .vscode-dark a:focus { 32 | outline-color: var(--vscode-textLink-foreground); } 33 | 34 | b { 35 | font-weight: 600; } 36 | 37 | h1 { 38 | border: none; 39 | font-size: 2.77em; 40 | font-weight: 400; 41 | margin: 0; 42 | padding: 0; 43 | white-space: nowrap; } 44 | 45 | h2 { 46 | font-size: 1.5em; 47 | font-weight: 200; 48 | line-height: normal; 49 | /* margin: 1em 0 0.3em 0; */ 50 | white-space: nowrap; } 51 | 52 | h3 { 53 | font-size: 1.17em; 54 | font-weight: 200; 55 | line-height: normal; 56 | margin: 1em 0 0.3em 0; 57 | white-space: nowrap; } 58 | 59 | header { 60 | align-items: center; 61 | display: flex; 62 | flex-wrap: wrap; 63 | justify-content: center; 64 | margin-bottom: 1em; } 65 | 66 | input, select, button { 67 | font-family: var(--vscode-font-family); 68 | font-size: inherit; 69 | margin: 0; } 70 | 71 | input { 72 | background: none; 73 | border: none; 74 | cursor: pointer; 75 | margin: 0; 76 | padding: 0 10px; } 77 | input:focus { 78 | background: rgba(0, 0, 0, 0.1); 79 | border-radius: 5px; 80 | outline: none; } 81 | input[disabled] { 82 | color: var(--vscode-input-foreground); 83 | cursor: default; } 84 | 85 | label { 86 | cursor: pointer; } 87 | 88 | section { 89 | display: flex; 90 | flex-wrap: wrap; 91 | margin-bottom: 1em; 92 | /* padding: 1rem; */ 93 | } 94 | .vscode-light section { 95 | background-color: var(--vscode-editor-background); } 96 | .vscode-dark section { 97 | background-color: var(--vscode-editor-background); } 98 | 99 | ul { 100 | font-size: 1em; 101 | list-style: none; 102 | margin: 0; 103 | padding: 0; } 104 | 105 | .button { 106 | background: none; 107 | border: none; 108 | border-radius: 3px; 109 | cursor: pointer; 110 | display: inline-block; 111 | font-size: 0.8em; 112 | letter-spacing: 0.25em; 113 | margin: 1em 0.5em; 114 | padding: 1em 1.75em; 115 | text-decoration: none; 116 | text-transform: uppercase; 117 | user-select: none; 118 | white-space: nowrap; } 119 | .button:focus { 120 | outline: none; } 121 | 122 | .button--big { 123 | font-size: 1.25em; } 124 | 125 | .button--flat { 126 | border: 1px solid rgba(255, 255, 255, 0.6); 127 | color: white !important; 128 | transition: background-color 250ms, border-color 250ms, color 250ms; } 129 | .button--flat:hover { 130 | background-color: white; 131 | color: black !important; } 132 | .preload .button--flat { 133 | transition-duration: 0s !important; } 134 | 135 | .button--flat-inverse { 136 | background-color: white; 137 | border: 1px solid white; 138 | color: black !important; 139 | font-weight: 600; 140 | transition: background-color 250ms, border-color 250ms, color 250ms; } 141 | .button--flat-inverse:hover { 142 | background: rgba(0, 0, 0, 0.2); 143 | border-color: rgba(255, 255, 255, 0.6); 144 | color: white !important; } 145 | .preload .button--flat-inverse { 146 | transition-duration: 0s !important; } 147 | 148 | .button--flat-primary { 149 | background-color: var(--vscode-button-background); 150 | border: 1px solid var(--vscode-button-background); 151 | color: white !important; 152 | font-weight: 600; 153 | transition: background-color 250ms, border-color 250ms, color 250ms; } 154 | .button--flat-primary:hover { 155 | background-color: white; 156 | border-color: white; 157 | color: black !important; } 158 | .preload .button--flat-primary { 159 | transition-duration: 0s !important; } 160 | 161 | .button-group { 162 | display: flex; 163 | flex-wrap: wrap; } 164 | .button-group .button { 165 | white-space: nowrap; } 166 | 167 | .button-group--support-alefragnani { 168 | justify-content: center; 169 | margin: 1.5em 0 1em 0; } 170 | .button-group--support-alefragnani .button { 171 | margin-left: 1.5em; 172 | margin-right: 1.5em; 173 | white-space: nowrap; } 174 | 175 | .button-group--support-alefragnani-sidebar { 176 | font-size: 11px; } 177 | .button-group--support-alefragnani-sidebar .button { 178 | font-weight: 400; 179 | margin: 0.75em 0; } 180 | 181 | .changelog { 182 | /* border-radius: 5em;*/ 183 | margin: 0 2em 1em 0; } 184 | .vscode-light .changelog { 185 | background-color: var(--vscode-editor-background); 186 | /* box-shadow: inset 0 0 6px 0px rgba(0, 0, 0, 0.05); */ 187 | } 188 | .vscode-dark .changelog { 189 | background-color: var(--vscode-editor-background); 190 | /* box-shadow: inset 0 0 6px 0px rgba(255, 255, 255, 0.075); */ 191 | } 192 | 193 | .changelog__badge { 194 | background-color: var(--vscode-activityBarBadge-background); 195 | border-radius: 3px; 196 | box-shadow: 0 0 2px 0px rgba(0, 0, 0, 0.5); 197 | color: #e6e6e6; 198 | display: inline-block; 199 | font-size: 11px; 200 | font-weight: 600; 201 | margin-right: 1.25em; 202 | padding: 2px 5px; 203 | text-align: center; 204 | text-transform: uppercase; 205 | vertical-align: text-top; 206 | width: 65px; } 207 | 208 | .changelog__badge--added { 209 | background-color: var(--vscode-problemsInfoIcon-foreground); } 210 | 211 | .changelog__badge--changed { 212 | background-color: var(--vscode-problemsWarningIcon-foreground); } 213 | 214 | .changelog__badge--fixed { 215 | background-color: var(--vscode-problemsErrorIcon-foreground); } 216 | 217 | .changelog__badge--internal { 218 | background-color: var(--vscode-problemsWarningIcon-foreground); } 219 | 220 | .changelog__badge--version { 221 | background-color: var(--vscode-activityBarBadge-background); 222 | height: 24px; 223 | vertical-align: middle; 224 | font-size: 15px; } 225 | 226 | .uppercase { 227 | text-transform: uppercase; 228 | } 229 | 230 | .changelog__version__borders { 231 | border-color: rgba(125, 125, 125, .15); 232 | border-spacing: 50px; 233 | border-width: thin; 234 | padding-top: 10px; 235 | padding-bottom: 10px; 236 | } 237 | 238 | .changelog__version__borders__top { 239 | border-top-style: solid; 240 | } 241 | 242 | .changelog__date { 243 | font-size: 0.8em; 244 | font-weight: 600; 245 | font-variant: small-caps; 246 | opacity: 0.9; 247 | margin-left: -7px; 248 | vertical-align: middle; } 249 | 250 | .changelog__details { 251 | align-items: center; 252 | display: flex; 253 | justify-content: center; 254 | position: relative; } 255 | .changelog__details:before { 256 | border-left: 1px solid rgba(122, 122, 122, 0.15); 257 | content: " "; 258 | height: calc(100% + 7px); 259 | left: 37px; 260 | position: absolute; 261 | top: 1px; } 262 | 263 | .changelog__image { 264 | margin: 1em 0; 265 | max-width: 65%; } 266 | 267 | .changelog__list { 268 | flex: 100% 0 1; 269 | font-size: 1.2em; 270 | font-weight: 200; 271 | line-height: 1.7; 272 | list-style-type: none; 273 | margin: 1em; } 274 | .changelog__list li { 275 | margin-bottom: 0.5em; } 276 | 277 | .changelog__list-item--version { 278 | margin: 2em 0 0.5em 0; } 279 | 280 | .changelog__scroller { 281 | flex: 100% 0 0; 282 | /* margin: 1.5em 0 0 0; */ 283 | max-height: 400px; 284 | overflow-y: auto; } 285 | /* .vscode-dark .changelog__scroller { 286 | border-bottom: 1px solid rgba(255, 255, 255, 0.04); } 287 | .vscode-light .changelog__scroller { 288 | border-bottom: 1px solid rgba(0, 0, 0, 0.05); } */ 289 | 290 | .changelog__title { 291 | flex: 100% 0 0; 292 | text-align: center; } 293 | 294 | .changelog__version { 295 | background-color: yellow; /* var(--vscode-activityBarBadge-background);*/ 296 | /* border-bottom: 2px solid #0bb892; */ 297 | border-radius: 3px; 298 | /* color: var(--vscode-activityBarBadge-foreground); */ 299 | margin: 0 0.25em; 300 | padding: 2px 10px; 301 | text-align: center; 302 | 303 | color: black; 304 | border-color: blue; 305 | border-width: 2px; 306 | font-weight: 600; 307 | 308 | vertical-align: bottom; } 309 | 310 | .command { 311 | font-weight: 600; 312 | padding: 1px 3px; } 313 | 314 | .container { 315 | align-items: center; 316 | display: flex; 317 | justify-content: center; 318 | min-height: 100%; 319 | min-width: 100%; } 320 | 321 | .content { 322 | margin-top: 1em; 323 | max-width: 1200px; 324 | min-width: 450px; 325 | width: 90%; } 326 | 327 | .cta__container { 328 | display: flex; 329 | flex-wrap: wrap; 330 | justify-content: center; 331 | /*margin-bottom: 3em;*/ } 332 | .cta__container p { 333 | margin-top: -1em; 334 | opacity: 0.4; } 335 | 336 | .cta__secondary { 337 | margin: 0 1em; } 338 | 339 | .section-list { 340 | font-weight: 200; 341 | list-style: disc; 342 | margin: 0 0 2em 2em; } 343 | .section-list li { 344 | margin-bottom: 0.5em; } 345 | 346 | .header__blurb { 347 | color: var(--vscode-editor-foreground); 348 | /* flex: 55% 2 1; */ 349 | font-size: 1.2em; 350 | font-weight: 200; 351 | margin: 1em 0 1em 1em; 352 | min-width: 345px; } 353 | @media all and (max-width: 880px) { 354 | .header__blurb { 355 | margin: 0 1em; } } 356 | 357 | .header__link { 358 | color: var(--vscode-input-foreground); 359 | outline: none; 360 | text-align: center; } 361 | .header__link:hover, .header__link:active, .header__link:focus { 362 | color: var(--vscode-input-foreground); 363 | outline: none; } 364 | 365 | .header__logo { 366 | display: flex; 367 | flex-wrap: nowrap; } 368 | 369 | .header__subtitle { 370 | color: var(--vscode-input-foreground); 371 | display: block; 372 | font-size: 2em; 373 | font-weight: 100; 374 | margin-top: -0.2em; 375 | white-space: nowrap; } 376 | 377 | .icon { 378 | background-color: var(--vscode-input-background); 379 | display: inline-block; 380 | height: 24px; 381 | margin-bottom: -9px; 382 | width: 19px; } 383 | 384 | .icon__info { 385 | -webkit-mask-image: url('data:image/svg+xml;utf8,'); 386 | -webkit-mask-repeat: no-repeat; 387 | mask-image: url('data:image/svg+xml;utf8,'); 388 | mask-repeat: no-repeat; } 389 | 390 | .icon--lg { 391 | height: 36px; 392 | margin-bottom: -16px; 393 | width: 30px; } 394 | 395 | .image__logo { 396 | margin: 9px 1em 0 0; 397 | max-height: 64px; 398 | max-width: 64px; } 399 | 400 | .image__preview { 401 | border-radius: 8px; 402 | box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.8), 0px 0px 12px 1px rgba(0, 0, 0, 0.5); } 403 | 404 | .image__preview--overlay { 405 | left: 0; 406 | position: absolute; 407 | top: 0; } 408 | 409 | .list-button { 410 | border: none; 411 | color: var(--vscode-button-foreground); 412 | cursor: pointer; 413 | font-size: 1em; 414 | height: 5em; 415 | margin: 5px 1px; 416 | max-width: 475px; 417 | padding: 12px 10px; 418 | text-align: left; 419 | width: calc(100% - 2px); } 420 | .vscode-light .list-button { 421 | background-color: var(--vscode-editor-background); } 422 | .vscode-dark .list-button { 423 | background-color: var(--vscode-editor-background); } 424 | .vscode-light .list-button:hover, .vscode-light .list-button:focus { 425 | background-color: var(--vscode-editor-background); } 426 | .vscode-dark .list-button:hover, .vscode-dark .list-button:focus { 427 | background-color: var(--vscode-editor-background); } 428 | .list-button:focus { 429 | outline: 0; } 430 | .vscode-light .list-button:focus { 431 | border: 1px solid var(--vscode-editor-background); } 432 | .vscode-dark .list-button:focus { 433 | border: 1px solid var(--vscode-editor-background); } 434 | 435 | .list-button__title { 436 | font-size: 1em; 437 | font-weight: 400; 438 | margin: 0; 439 | margin-bottom: .25em; 440 | overflow: hidden; 441 | text-overflow: ellipsis; } 442 | 443 | .list-button__detail { 444 | color: var(--vscode-button-foreground); 445 | display: inline-block; 446 | overflow: hidden; 447 | text-overflow: ellipsis; 448 | white-space: nowrap; 449 | width: 100%; } 450 | 451 | .page-header { 452 | margin: 1em 0 2em 0; 453 | position: relative; } 454 | 455 | .page-header__title { 456 | font-size: 3em; 457 | margin: 0; } 458 | 459 | .page-header__subtitle { 460 | color: var(--vscode-input-foreground); 461 | margin: 0; } 462 | 463 | .section__preview { 464 | flex: 30% 1 1; 465 | margin-left: 2em; 466 | min-width: 400px; 467 | position: relative; } 468 | 469 | .section__settings { 470 | display: flex; 471 | flex: 100% 1 0; 472 | flex-wrap: wrap; 473 | margin-right: 1em; } 474 | 475 | .section__header { 476 | align-items: baseline; 477 | cursor: pointer; 478 | display: flex; 479 | flex: 100% 0 0; 480 | flex-wrap: wrap; 481 | margin-bottom: 2em; 482 | position: relative; } 483 | .section__header:after { 484 | background-color: var(--vscode-editor-background); 485 | content: ''; 486 | height: 40px; 487 | -webkit-mask-image: url('data:image/svg+xml;utf8,'); 488 | -webkit-mask-repeat: no-repeat; 489 | -webkit-mask-size: 32px 40px; 490 | mask-image: url('data:image/svg+xml;utf8,'); 491 | mask-repeat: no-repeat; 492 | mask-size: 32px 40px; 493 | position: absolute; 494 | right: 0; 495 | top: 0; 496 | transition: transform 500ms cubic-bezier(0, 1.5, 1, 1); 497 | width: 32px; } 498 | .section__header.collapsed { 499 | margin-bottom: 0; } 500 | .section__header.collapsed:after { 501 | transform: rotateX(180deg); } 502 | .section__header.collapsed + .section__settings { 503 | display: none; } 504 | 505 | .section__title { 506 | flex: 100% 1 0; 507 | margin: 0; } 508 | 509 | .section-group__content { 510 | flex: auto 1 1; 511 | min-width: 0; 512 | max-width: 60%; } 513 | 514 | .section-group__sidebar { 515 | align-self: flex-start; 516 | margin: 0 0 0 3em; 517 | position: sticky; 518 | top: 0; } 519 | .section-group__sidebar li { 520 | white-space: nowrap; } 521 | 522 | @media all and (max-width: 860px) { 523 | .section-group__sidebar--settings { 524 | display: none; } } 525 | 526 | .section-group__sidebar--welcome { 527 | margin-right: 3em; } 528 | @media all and (max-width: 560px) { 529 | .section-group__sidebar--welcome { 530 | display: none; } } 531 | 532 | .section-group-section p { 533 | font-size: 1.1em; 534 | font-weight: 200; 535 | margin: 1em 0.25em; } 536 | 537 | .section-group-section__cols { 538 | display: flex; 539 | flex-wrap: wrap; } 540 | 541 | .section-group-section__col { 542 | flex: 45% 1 1; 543 | margin: 0 4px; 544 | min-width: 300px; } 545 | 546 | .section-groups { 547 | display: flex; 548 | justify-content: space-around; 549 | margin-bottom: 50%; 550 | margin-top: -1em; } 551 | 552 | .settings-group { 553 | flex: 2 1; 554 | margin-bottom: 1em; 555 | min-width: 300px; } 556 | 557 | .settings-group__hint--more { 558 | display: block; 559 | margin-left: 34px; 560 | margin-top: 0.5em; } 561 | 562 | .settings-group__setting { 563 | align-items: baseline; 564 | display: flex; 565 | flex: 100% 1 1; 566 | flex-wrap: wrap; 567 | margin-bottom: 0.75em; } 568 | .settings-group__setting input[type="checkbox"] { 569 | flex: 16px 0 0; 570 | height: 16px; 571 | margin: 0 0.75em 0 0; 572 | position: relative; 573 | top: 3px; 574 | width: 16px; } 575 | .settings-group__setting label { 576 | flex: auto 0 1; 577 | margin-bottom: 0.75em; } 578 | .settings-group__setting[disabled] label { 579 | color: var(--vscode-input-placeholderForeground); 580 | cursor: default; } 581 | 582 | .setting__hint--indent-2 { 583 | margin-left: 5em; } 584 | 585 | .setting__hint--indent-4 { 586 | margin-left: 7em; } 587 | 588 | .sidebar-group { 589 | margin-top: 0; } 590 | 591 | .bold { 592 | font-weight: 600; } 593 | 594 | .center { 595 | text-align: center; } 596 | 597 | .hidden { 598 | display: none !important; } 599 | 600 | .mb-2 { 601 | margin-bottom: 2em !important; } 602 | 603 | .ml-1 { 604 | margin-left: 1em; } 605 | 606 | .ml-2 { 607 | margin-left: 2em; } 608 | 609 | .ml-4 { 610 | margin-left: 4em; } 611 | 612 | .non-interactive { 613 | cursor: default !important; } 614 | 615 | .nowrap { 616 | flex-wrap: nowrap !important; } 617 | 618 | .vscode-dark .light { 619 | display: none; 620 | } 621 | .vscode-light .dark { 622 | display: none; 623 | } 624 | --------------------------------------------------------------------------------