ソースを参照

Updates home webview content

main
Keith Daulton 2年前
コミット
921b369feb
15個のファイルの変更1062行の追加157行の削除
  1. +6
    -0
      src/storage.ts
  2. +110
    -0
      src/webviews/apps/home/components/card-section.ts
  3. +108
    -0
      src/webviews/apps/home/components/stepped-section.ts
  4. +303
    -27
      src/webviews/apps/home/home.html
  5. +249
    -7
      src/webviews/apps/home/home.scss
  6. +181
    -117
      src/webviews/apps/home/home.ts
  7. バイナリ
      src/webviews/apps/media/getting-started.png
  8. バイナリ
      src/webviews/apps/media/gitlens-backdrop.png
  9. バイナリ
      src/webviews/apps/media/gitlens-logo.png
  10. バイナリ
      src/webviews/apps/media/play-button-dark.png
  11. バイナリ
      src/webviews/apps/media/play-button.png
  12. +4
    -0
      src/webviews/apps/play-button.svg
  13. +75
    -5
      src/webviews/home/homeWebviewView.ts
  14. +17
    -1
      src/webviews/home/protocol.ts
  15. +9
    -0
      src/webviews/webviewViewBase.ts

+ 6
- 0
src/storage.ts ファイルの表示

@ -123,6 +123,12 @@ export interface GlobalStorage {
actions: {
completed?: CompletedActions[];
};
steps: {
completed?: string[];
};
sections: {
dismissed?: string[];
};
};
pendingWelcomeOnFocus?: boolean;
pendingWhatsNewOnFocus?: boolean;

+ 110
- 0
src/webviews/apps/home/components/card-section.ts ファイルの表示

@ -0,0 +1,110 @@
import { attr, css, customElement, FASTElement, html, volatile, when } from '@microsoft/fast-element';
import { numberConverter } from '../../shared/components/converters/number-converter';
import '../../shared/components/codicon';
const template = html<CardSection>`<template
role="region"
style="${x => (x.backdrop !== '' ? `background-image: url(${x.backdrop})` : '')}"
>
${when(
x => x.noHeading === false,
html<CardSection>`<header>
<div class="heading" role="heading" aria-level="${x => x.headingLevel}">
<slot name="heading"></slot>
<small class="description"><slot name="description"></slot></small>
</div>
${when(
x => x.dismissable,
html<CardSection>`<button
class="dismiss"
type="button"
@click="${(x, c) => x.handleDismiss(c.event)}"
title="dismiss"
aria-label="dismiss"
>
<code-icon icon="close"></code-icon>
</button>`,
)}
</header>`,
)}
<div class="content"><slot></slot></div>
</template>`;
const styles = css`
* {
box-sizing: border-box;
}
:host {
display: block;
padding: 1.2rem;
background-color: #aaaaaa10;
margin-bottom: 1rem;
border-radius: 0.4rem;
background-repeat: no-repeat;
background-size: cover;
}
header {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 0.4rem;
margin-bottom: 1rem;
}
.dismiss {
width: 2rem;
height: 2rem;
padding: 0;
font-size: var(--vscode-editor-font-size);
line-height: 2rem;
font-family: inherit;
border: none;
color: inherit;
background: none;
text-align: left;
cursor: pointer;
opacity: 0.5;
flex: none;
text-align: center;
}
.dismiss:focus {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: 0.2rem;
}
.heading {
text-transform: uppercase;
}
.description {
margin-left: 0.2rem;
text-transform: none;
/* color needs to come from some sort property */
color: #b68cd8;
}
`;
@customElement({ name: 'card-section', template: template, styles: styles })
export class CardSection extends FASTElement {
@attr
backdrop = '';
@attr({ attribute: 'no-heading', mode: 'boolean' })
noHeading = false;
@attr({ attribute: 'heading-level', converter: numberConverter })
headingLevel = 2;
@attr({ mode: 'boolean' })
dismissable = false;
@attr({ mode: 'boolean' })
expanded = true;
handleDismiss(e: Event) {
this.$emit('dismiss');
}
}

+ 108
- 0
src/webviews/apps/home/components/stepped-section.ts ファイルの表示

@ -0,0 +1,108 @@
import { attr, css, customElement, FASTElement, html, volatile, when } from '@microsoft/fast-element';
import { numberConverter } from '../../shared/components/converters/number-converter';
import '../../shared/components/codicon';
const template = html<SteppedSection>`<template role="region">
<header class="heading" role="heading" aria-level="${x => x.headingLevel}">
<button
id="button"
class="button"
type="button"
aria-expanded="${x => !x.completed}"
aria-controls="content"
@click="${(x, c) => x.handleClick(c.event)}"
>
<slot name="heading"></slot>
<small class="description"><slot name="description"></slot></small>
</button>
</header>
<div class="content${x => (x.completed ? ' is-hidden' : '')}" id="content" aria-labelledby="button">
<slot></slot>
</div>
<span class="checkbox"
><code-icon icon="${x => (x.completed ? 'pass-filled' : 'circle-large-outline')}"></code-icon
></span>
</template>`;
const styles = css`
* {
box-sizing: border-box;
}
:host {
display: grid;
gap: 0 0.8rem;
grid-template-columns: 16px auto;
grid-auto-flow: column;
margin-bottom: 2.4rem;
}
.button {
width: 100%;
padding: 0.1rem 0 0 0;
font-size: var(--vscode-editor-font-size);
line-height: 1.6rem;
font-family: inherit;
border: none;
color: inherit;
background: none;
text-align: left;
text-transform: uppercase;
cursor: pointer;
}
.button:focus {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: 0.2rem;
}
.checkbox {
position: relative;
grid-column: 1;
grid-row: 1 / span 2;
color: var(--vscode-textLink-foreground);
}
:host(:not(:last-of-type)) .checkbox:after {
content: '';
position: absolute;
border-left: 0.1rem solid currentColor;
width: 0;
top: 1.6rem;
bottom: -2.4rem;
left: 50%;
transform: translateX(-50%);
opacity: 0.3;
}
.content {
margin-top: 1rem;
}
.content.is-hidden {
display: none;
}
.description {
margin-left: 0.2rem;
text-transform: none;
/* color needs to come from some sort property */
color: #b68cd8;
opacity: 0.6;
font-style: italic;
}
`;
@customElement({ name: 'stepped-section', template: template, styles: styles })
export class SteppedSection extends FASTElement {
@attr({ attribute: 'heading-level', converter: numberConverter })
headingLevel = 2;
@attr({ mode: 'boolean' })
completed = false;
handleClick(e: Event) {
this.completed = !this.completed;
this.$emit('complete', this.completed);
}
}

+ 303
- 27
src/webviews/apps/home/home.html ファイルの表示

@ -4,24 +4,312 @@
<meta charset="utf-8" />
</head>
<body class="preload">
<div id="container" class="container">
<div class="notice">
<span
><span class="glicon glicon-clock"></span> GitLens+
<a href="command:gitlens.plus.purchase">introductory pricing</a> will end with the next release
(late Sept, early Oct).
</span>
<body class="home preload">
<a href="#main" class="sr-skip sr-only-focusable">skip to content</a>
<a href="#footer" class="sr-skip sr-only-focusable">skip to footer links</a>
<header class="home__header">
<div class="header-card">
<div class="header-card__logo"><img src="#{webroot}/media/gitlens-logo.webp" alt="GitLens Logo" /></div>
<h1 class="header-card__title"><span class="foreground">Git</span>Lens 12 <em>Git supercharged</em></h1>
<p class="header-card__account">
<span id="header-content"></span>
<span id="header-actions"></span>
</p>
<div class="progress header-card__progress" role="progressbar">
<div class="progress__indicator"></div>
</div>
</div>
<div class="content">
<section id="slot1"></section>
<section id="slot2"></section>
<section id="slot3"></section>
</header>
<main class="home__main" id="main" tabindex="-1">
<style>
.video-banner {
--video-banner-bg: url(#{webroot}/media/getting-started.webp);
}
.vscode-dark .video-banner {
--video-banner-play: url(#{webroot}/media/play-button.webp);
}
.vscode-light .video-banner {
--video-banner-play: url(#{webroot}/media/play-button-dark.webp);
}
</style>
<div class="stepped-sections">
<stepped-section id="welcome">
<span slot="heading">Welcome to GitLens 12</span>
<p>
GitLens supercharges Git inside VS Code and unlocks the untapped knowledge within each
repository.
</p>
<p>
<a href="command:gitlens.showWelcomePage?%22quick-setup%22" class="button-link"
><code-icon icon="pass"></code-icon>Quick Setup</a
>
</p>
</stepped-section>
<stepped-section id="getting-started">
<span slot="heading">Getting Started</span>
<a class="video-banner" href="https://www.youtube.com/watch?v=UQPb73Zz9qk">
<span>Get Started</span> <small>Tutorial Video</small>
</a>
<ul>
<li>
The <a href="command:gitlens.showWelcomePage?%22quick-setup%22">Quick Setup</a> helps you
start personalizing GitLens to your needs.
</li>
<li>
The <a href="command:gitlens.getStarted">Feature Walkthrough</a> gets you familiarized with
the rich features GitLens provides.
</li>
<li>
The <a href="command:gitlens.showSettingsPage">Interactive settings editor</a> helps you
fine-tune your GitLens experience.
</li>
</ul>
</stepped-section>
<stepped-section id="features">
<span slot="heading">Features</span>
<span slot="description">*Always available to you at no cost</span>
<div class="activitybar-banner">
<ul>
<li>
Find GitLens features by opening the
<a href="command:workbench.view.scm">Source Control Side Bar</a>
</li>
<li>Keep an eye out for feature updates and new components here.</li>
</ul>
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 83 76">
<rect x="42.5" y=".5" width="40" height="75" rx="16.5" stroke="url(#a)" />
<mask
id="b"
style="mask-type: alpha"
maskUnits="userSpaceOnUse"
x="45"
y="3"
width="35"
height="70"
>
<rect
x="45"
y="3"
width="35"
height="70"
rx="14"
fill="#333"
style="fill: var(--vscode-activityBar-background)"
/>
</mask>
<g mask="url(#b)">
<rect
x="45"
y="3"
width="35"
height="70"
rx="14"
fill="#333"
style="fill: var(--vscode-activityBar-background)"
/>
<path
fill="#fff"
d="M45 38h1v27h-1z"
style="fill: var(--vscode-activityBar-activeBorder)"
/>
</g>
<g clip-path="url(#c)" fill="#fff" style="fill: var(--vscode-activityBar-foreground)">
<path
d="m64.078 54.996-2.138-2.138.752-.751 2.137 2.138-.751.751ZM60.73 58.079V52.92h1.062v5.158h-1.063Z"
/>
<path
d="M61.26 61.167a1.594 1.594 0 1 0 0-3.188 1.594 1.594 0 0 0 0 3.188ZM65.51 57.27a1.594 1.594 0 1 0 0-3.187 1.594 1.594 0 0 0 0 3.188ZM61.26 53.02a1.594 1.594 0 1 0 0-3.187 1.594 1.594 0 0 0 0 3.188Z"
/>
<path
d="M62.5 47.885a7.615 7.615 0 1 0 0 15.23 7.615 7.615 0 0 0 0-15.23ZM54 55.5a8.5 8.5 0 1 1 17 0 8.5 8.5 0 0 1-17 0Z"
/>
</g>
<g clip-path="url(#d)">
<path
d="M68.755 17.166a2.804 2.804 0 0 0-5.406-1.053 2.803 2.803 0 0 0 1.802 3.724 2.241 2.241 0 0 1-2.001 1.253h-2.243a3.342 3.342 0 0 0-2.241.873V16.55a2.803 2.803 0 1 0-1.121 0v6.838a2.832 2.832 0 1 0 1.362.074 2.242 2.242 0 0 1 2.001-1.25h2.242a3.362 3.362 0 0 0 3.168-2.28 2.803 2.803 0 0 0 2.437-2.765Zm-12.331-3.363a1.682 1.682 0 1 1 3.363 0 1.682 1.682 0 0 1-3.363 0Zm3.363 12.331a1.682 1.682 0 1 1-3.364 0 1.682 1.682 0 0 1 3.364 0Zm6.166-7.286a1.682 1.682 0 1 1 0-3.364 1.682 1.682 0 0 1 0 3.364Z"
fill="#858585"
style="fill: var(--vscode-activityBar-inactiveForeground)"
/>
</g>
<path
d="M.333 54a2.667 2.667 0 1 1 5.334 0 2.667 2.667 0 0 1-5.334 0ZM34 54l-5 2.887v-5.774L34 54Zm-31-.5h26.5v1H3v-1Z"
fill="#fff"
style="fill: var(--vscode-foreground)"
/>
<path
d="M.333 22a2.667 2.667 0 1 1 5.334 0 2.667 2.667 0 0 1-5.334 0ZM34 22l-5 2.887v-5.774L34 22Zm-31-.5h26.5v1H3v-1Z"
fill="#007FD5"
style="fill: var(--vscode-textLink-foreground)"
/>
<defs>
<clipPath id="c">
<path
fill="#fff"
transform="translate(54 47)"
d="M0 0h17v17H0z"
style="fill: var(--vscode-textLink-foreground)"
/>
</clipPath>
<clipPath id="d">
<path
fill="#fff"
transform="translate(53 11)"
d="M0 0h18v18H0z"
style="fill: var(--vscode-textLink-foreground)"
/>
</clipPath>
<linearGradient
id="a"
x1="62.5"
y1="-19.365"
x2="62.5"
y2="76"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#333" style="stop-color: var(--vscode-activityBar-background)" />
<stop
offset="1"
stop-color="#333"
stop-opacity="0"
style="stop-color: var(--vscode-activityBar-background)"
/>
<stop
offset="1"
stop-color="#333"
stop-opacity="0"
style="stop-color: var(--vscode-activityBar-background)"
/>
</linearGradient>
</defs>
</svg>
</div>
</stepped-section>
<!-- check for gitlens+ -->
<stepped-section id="plus">
<span slot="heading">Want even more from GitLens?</span>
<card-section no-heading backdrop="#{webroot}/media/gitlens-backdrop.webp">
<div class="centered">
<p class="type-tight">
<span class="logo"><span class="foreground">Git</span>Lens+</span><br />
<em class="description">*optional</em>
</p>
<div id="plus-content">
<p>
GitLens+ adds all-new, completely optional, features that enhance your current
GitLens experience.
</p>
<p>These features are free for local and public repos with no account required.</p>
<!-- free -->
<p class="mb-1">
<vscode-button data-action="command:gitlens.plus.startPreviewTrial"
>Try GitLens+ with private repositories</vscode-button
>
</p>
<p class="mb-0">
<a href="command:gitlens.plus.hide">Hide GitLens+ features, cannot use them</a>
</p>
</div>
</div>
</card-section>
</stepped-section>
</div>
<div>
<section id="slot-footer"></section>
<!-- check for gitlens+ -->
<div id="plus-sections">
<card-section dismissable id="commit-graph">
<span slot="heading">Introducing the Commit Graph</span>
<p>
The
<a
title="Learn more about the Visual File History"
href="command:gitlens.openWalkthrough?%22gitlens.plus%7Cgitlens.plus.commitGraph%22"
>Commit Graph</a
>
helps you to easily visualize branch structure and commit history. Not only does it help you
verify your changes, but also easily see changes made by others and when.
</p>
<img src="#{webroot}/media/plus-commit-graph-illustrated.webp" alt="Commit Graph illustration" />
</card-section>
<card-section dismissable id="visual-file-history">
<span slot="heading">Introducing Visual File History</span>
<p>
The
<a
title="Learn more about the Visual File History"
href="command:gitlens.openWalkthrough?%22gitlens.plus%7Cgitlens.plus.visualFileHistory%22"
>Visual File History</a
>
allows you to quickly see the evolution of a file, including when changes were made, how large
they were, and who made them.
</p>
<img
src="#{webroot}/media/plus-visual-file-history-illustrated.webp"
alt="Visual File History illustration"
/>
</card-section>
<card-section dismissable id="worktrees">
<span slot="heading">Introducing Worktrees</span>
<p>
<a
title="Learn more about worktrees"
href="command:gitlens.openWalkthrough?%22gitlens.plus%7Cgitlens.plus.worktrees%22"
>Worktrees</a
>
allow you to easily work on different branches of a repository simultaneously.
</p>
<img src="#{webroot}/media/plus-worktrees-illustrated.webp" alt="Worktrees illustration" />
</card-section>
</div>
</div>
<div class="button-container">
<vscode-button id="restore-plus" appearance="secondary" data-action="command:gitlens.plus.restore"
>Restore GitLens+ features</vscode-button
>
<vscode-button
id="restore-welcome"
appearance="secondary"
data-action="command:gitlens.home.restoreWelcome"
>Restore Welcome</vscode-button
>
</div>
</main>
<footer class="home__footer" id="footer" tabindex="-1">
<nav class="inline-nav" aria-label="Help and Resources">
<a href="https://gitlens.amod.io/#whats-new" aria-label="What's new" title="What's new"
><span class="codicon codicon-rocket"></span
></a>
<a
href="https://github.com/gitkraken/vscode-gitlens#readme"
aria-label="GitHub Readme"
title="GitHub Readme"
><span class="codicon codicon-book"></span
></a>
<a
href="https://github.com/gitkraken/vscode-gitlens/discussions"
aria-label="GitHub Discussions"
title="GitHub Discussions"
><span class="codicon codicon-comment-discussion"></span
></a>
<a
href="https://github.com/gitkraken/vscode-gitlens/issues"
aria-label="GitHub Issues"
title="GitHub Issues"
><span class="codicon codicon-issues"></span
></a>
<a href="https://gitkraken.com/gitlens-support" aria-label="GitLens Support" title="GitLens Support"
><span class="codicon codicon-account"></span
></a>
<a
href="https://gitkraken.com/gitlens?utm_source=gitlens-extension&utm_medium=in-app-links&utm_campaign=gitlens-logo-links"
aria-label="GitLens Website"
title="GitLens Website"
><span class="codicon codicon-globe"></span
></a>
<a href="https://twitter.com/gitlens" aria-label="GitLens on Twitter" title="GitLens on Twitter"
><span class="codicon codicon-twitter"></span
></a>
</nav>
</footer>
#{endOfBody}
<style nonce="#{cspNonce}">
@font-face {
@ -31,16 +319,4 @@
}
</style>
</body>
<!-- prettier-ignore -->
<%= require('html-loader?{"esModule":false}!./partials/welcome.html') %>
<%= require('html-loader?{"esModule":false}!./partials/views.html') %>
<%= require('html-loader?{"esModule":false}!./partials/links.html') %>
<%= require('html-loader?{"esModule":false}!./partials/state.free.html') %>
<%= require('html-loader?{"esModule":false}!./partials/state.free-preview-trial.html') %>
<%= require('html-loader?{"esModule":false}!./partials/state.free-preview-trial-expired.html') %>
<%= require('html-loader?{"esModule":false}!./partials/state.plus-trial.html') %>
<%= require('html-loader?{"esModule":false}!./partials/state.plus-trial-expired.html') %>
<%= require('html-loader?{"esModule":false}!./partials/state.paid.html') %>
<%= require('html-loader?{"esModule":false}!./partials/state.verify-email.html') %>
</html>

+ 249
- 7
src/webviews/apps/home/home.scss ファイルの表示

@ -1,19 +1,84 @@
:root {
--gitlens-z-inline: 1000;
--gitlens-z-sticky: 1100;
--gitlens-z-popover: 1200;
--gitlens-z-cover: 1300;
--gitlens-z-dialog: 1400;
--gitlens-z-modal: 1500;
--gitlens-brand-color: #914db3;
--gitlens-brand-color-2: #a16dc4;
}
* {
box-sizing: border-box;
}
// avoids FOUC for elements not yet called with `define()`
:not(:defined) {
visibility: hidden;
}
html {
height: 100%;
font-size: 62.5%;
text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
background-color: var(--color-view-background);
color: var(--color-view-foreground);
font-family: var(--font-family);
height: 100%;
min-height: 100%;
line-height: 1.4;
font-size: 100% !important;
font-size: var(--vscode-font-size);
}
:focus {
outline-color: var(--vscode-focusBorder);
}
.sr-skip {
position: fixed;
z-index: var(--gitlens-z-popover);
top: 0.2rem;
left: 0.2rem;
display: inline-block;
padding: 0.2rem 0.4rem;
background-color: var(--color-view-background);
}
.sr-only,
.sr-only-focusable:not(:active):not(:focus) {
clip: rect(0 0 0 0);
clip-path: inset(50%);
width: 1px;
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
}
.home {
padding: 0;
height: 100%;
display: flex;
flex-direction: column;
gap: 0.4rem;
&__header {
flex: none;
padding: 0 2rem;
}
&__main {
flex: 1;
overflow: auto;
padding: 2rem 2rem 0.4rem;
}
&__footer {
flex: none;
padding: 0 2rem;
}
}
.container {
@ -75,13 +140,172 @@ b {
}
p {
margin-bottom: 0;
margin-top: 0;
}
ul {
margin-top: 0;
padding-left: 1.2em;
}
.feature-desc {
margin-bottom: 1rem;
}
.button-container {
display: flex;
flex-direction: column;
margin-bottom: 1rem;
}
.button-link {
code-icon {
margin-right: 0.4rem;
}
}
.centered {
text-align: center;
}
.progress {
width: 100%;
.vscode-dark & {
background-color: var(--color-background--lighten-15);
}
.vscode-light & {
background-color: var(--color-background--darken-15);
}
&__indicator {
height: 4px;
}
}
.header-card {
position: relative;
display: grid;
padding: 1rem 1rem 1.2rem;
background-color: #aaaaaa10;
border-radius: 0.4rem;
gap: 0 0.8rem;
grid-template-columns: 3.4rem auto;
grid-auto-flow: column;
&__logo {
grid-column: 1;
grid-row: 1 / span 2;
}
&__title {
font-size: var(--vscode-font-size);
color: var(--gitlens-brand-color-2);
margin: 0;
em {
font-weight: normal;
color: var(--color-view-foreground);
opacity: 0.4;
}
}
&__account {
margin: 0;
display: flex;
flex-direction: row;
justify-content: space-between;
}
&__progress {
position: absolute;
bottom: 0;
left: 0;
border-bottom-left-radius: 0.4rem;
border-bottom-right-radius: 0.4rem;
}
}
.foreground {
color: var(--color-view-foreground);
}
.inline-nav {
display: flex;
flex-direction: row;
gap: 0.4rem;
a {
display: flex;
justify-content: center;
align-items: center;
width: 2.2rem;
height: 2.2rem;
// line-height: 2.2rem;
.codicon {
line-height: 1.6rem;
}
&:hover {
text-decoration: none;
}
}
}
.logo {
font-size: 1.8rem;
color: var(--gitlens-brand-color-2);
}
.description {
color: #b68cd8;
opacity: 0.6;
}
.activitybar-banner {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 1.6rem;
ul {
margin: {
top: 0.2rem;
bottom: 0;
}
}
svg {
flex: none;
max-width: 10rem;
height: auto;
}
}
.video-banner {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-end;
margin-bottom: 0.8rem;
background: no-repeat var(--video-banner-play) center center, no-repeat var(--video-banner-bg) left center;
background-size: clamp(2.9rem, 8%, 6rem), cover;
aspect-ratio: var(--video-banner-ratio, 354 / 54);
padding: 0.4rem 1.2rem;
color: inherit;
line-height: 1.2;
font-size: clamp(var(--vscode-font-size), 4vw, 2.4rem);
transition: aspect-ratio linear 100ms;
@media (min-width: 564px) {
aspect-ratio: var(--video-banner-ratio, 354 / 40);
}
&:hover {
text-decoration: none;
color: inherit;
}
small {
color: #8d778d;
}
}
.image--preview {
border-radius: 8px;
box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.8), 0px 0px 12px 1px rgba(0, 0, 0, 0.5);
@ -128,9 +352,12 @@ p {
vscode-button {
align-self: center;
margin-top: 1.5rem;
max-width: 300px;
width: 100%;
& + & {
margin-top: 1rem;
}
}
span.button-subaction {
@ -153,7 +380,22 @@ vscode-divider {
@import '../shared/codicons';
.codicon {
position: relative;
top: -2px;
// .codicon {
// position: relative;
// top: -2px;
// }
.type-tight {
line-height: 1.2;
}
.mb-1 {
margin-bottom: 0.4rem;
}
.mb-0 {
margin-bottom: 0;
}
.hide {
display: none;
}

+ 181
- 117
src/webviews/apps/home/home.ts ファイルの表示

@ -2,17 +2,28 @@
import './home.scss';
import { provideVSCodeDesignSystem, vsCodeButton } from '@vscode/webview-ui-toolkit';
import type { Disposable } from 'vscode';
import { getSubscriptionTimeRemaining, SubscriptionState } from '../../../subscription';
// import { RepositoriesVisibility } from '../../../git/gitProviderService';
import { getSubscriptionTimeRemaining, isSubscriptionTrial, SubscriptionState } from '../../../subscription';
import { pluralize } from '../../../system/string';
import type { State } from '../../home/protocol';
import { CompletedActions, DidChangeSubscriptionNotificationType } from '../../home/protocol';
import {
CompleteStepCommandType,
DidChangeSubscriptionNotificationType,
DismissSectionCommandType,
} from '../../home/protocol';
import type { IpcMessage } from '../../protocol';
import { ExecuteCommandType, onIpc } from '../../protocol';
import { App } from '../shared/appBase';
import { DOM } from '../shared/dom';
import type { CardSection } from './components/card-section';
import type { SteppedSection } from './components/stepped-section';
import '../shared/components/codicon';
import './components/card-section';
import './components/stepped-section';
export class HomeApp extends App<State> {
private $slots!: HTMLElement[];
private $footer!: HTMLElement;
private $steps!: SteppedSection[];
private $cards!: CardSection[];
constructor() {
super('HomeApp');
@ -21,12 +32,8 @@ export class HomeApp extends App {
protected override onInitialize() {
provideVSCodeDesignSystem().register(vsCodeButton());
this.$slots = [
document.getElementById('slot1') as HTMLDivElement,
document.getElementById('slot2') as HTMLDivElement,
document.getElementById('slot3') as HTMLDivElement,
];
this.$footer = document.getElementById('slot-footer') as HTMLDivElement;
this.$steps = [...document.querySelectorAll<SteppedSection>('stepped-section[id]')];
this.$cards = [...document.querySelectorAll<CardSection>('card-section[id]')];
this.updateState();
}
@ -35,6 +42,16 @@ export class HomeApp extends App {
const disposables = super.onBind?.() ?? [];
disposables.push(DOM.on('[data-action]', 'click', (e, target: HTMLElement) => this.onActionClicked(e, target)));
disposables.push(
DOM.on<SteppedSection, boolean>('stepped-section', 'complete', (e, target: HTMLElement) =>
this.onStepComplete(e, target),
),
);
disposables.push(
DOM.on<CardSection, undefined>('card-section', 'dismiss', (e, target: HTMLElement) =>
this.onCardDismissed(e, target),
),
);
return disposables;
}
@ -47,7 +64,8 @@ export class HomeApp extends App {
this.log(`${this.appName}.onMessageReceived(${msg.id}): name=${msg.method}`);
onIpc(DidChangeSubscriptionNotificationType, msg, params => {
this.state = params;
this.state.subscription = params.subscription;
this.state.completedActions = params.completedActions;
this.updateState();
});
break;
@ -58,6 +76,19 @@ export class HomeApp extends App {
}
}
private onStepComplete(e: CustomEvent<boolean>, target: HTMLElement) {
const id = target.id;
console.log('onStepComplete', id, e.detail);
this.sendCommand(CompleteStepCommandType, { id: id, completed: e.detail ?? false });
}
private onCardDismissed(e: CustomEvent<undefined>, target: HTMLElement) {
const id = target.id;
console.log('onCardDismissed', id);
this.sendCommand(DismissSectionCommandType, { id: id });
target.remove();
}
private onActionClicked(e: MouseEvent, target: HTMLElement) {
const action = target.dataset.action;
if (action?.startsWith('command:')) {
@ -66,119 +97,152 @@ export class HomeApp extends App {
}
private updateState() {
const { subscription, completedActions } = this.state;
const viewsVisible = !completedActions.includes(CompletedActions.OpenedSCM);
const welcomeVisible = !completedActions.includes(CompletedActions.DismissedWelcome);
let index = 0;
if (subscription.account?.verified === false) {
DOM.insertTemplate('state:verify-email', this.$slots[index++]);
DOM.insertTemplate(welcomeVisible ? 'welcome' : 'links', this.$slots[index++]);
} else {
switch (subscription.state) {
case SubscriptionState.Free:
if (welcomeVisible) {
DOM.insertTemplate('welcome', this.$slots[index++]);
DOM.resetSlot(this.$footer);
} else {
DOM.insertTemplate('links', this.$footer);
}
if (viewsVisible) {
DOM.insertTemplate('views', this.$slots[index++]);
}
DOM.insertTemplate('state:free', this.$slots[index++]);
break;
case SubscriptionState.FreeInPreviewTrial: {
if (viewsVisible) {
DOM.insertTemplate('views', this.$slots[index++]);
}
const remaining = getSubscriptionTimeRemaining(subscription, 'days') ?? 0;
DOM.insertTemplate('state:free-preview-trial', this.$slots[index++], {
bindings: {
previewDays: `${
remaining < 1
? 'less than one day'
: remaining === 1
? `${remaining} day`
: `${remaining} days`
}`,
},
});
break;
}
case SubscriptionState.FreePreviewTrialExpired:
if (viewsVisible) {
DOM.insertTemplate('views', this.$slots[index++]);
}
DOM.insertTemplate('state:free-preview-trial-expired', this.$slots[index++]);
break;
case SubscriptionState.FreePlusInTrial: {
if (viewsVisible) {
DOM.insertTemplate('views', this.$slots[index++]);
}
const remaining = getSubscriptionTimeRemaining(subscription, 'days') ?? 0;
DOM.insertTemplate('state:plus-trial', this.$slots[index++], {
bindings: {
plan: subscription.plan.effective.name,
trialDays: `${
remaining < 1
? 'less than one day'
: remaining === 1
? `${remaining} day`
: `${remaining} days`
}`,
},
});
break;
}
case SubscriptionState.FreePlusTrialExpired:
if (viewsVisible) {
DOM.insertTemplate('views', this.$slots[index++]);
}
DOM.insertTemplate('state:plus-trial-expired', this.$slots[index++]);
break;
case SubscriptionState.Paid:
if (viewsVisible) {
DOM.insertTemplate('views', this.$slots[index++]);
}
DOM.insertTemplate('state:paid', this.$slots[index++], {
bindings: { plan: subscription.plan.effective.name },
});
break;
}
const { subscription, completedSteps, dismissedSections, plusEnabled, visibility } = this.state;
// banner
document.getElementById('plus')?.classList.toggle('hide', !plusEnabled);
document.getElementById('restore-plus')?.classList.toggle('hide', plusEnabled);
document.getElementById('plus-sections')?.classList.toggle('hide', !plusEnabled);
const showRestoreWelcome = completedSteps?.length || dismissedSections?.length;
document.getElementById('restore-welcome')?.classList.toggle('hide', !showRestoreWelcome);
// TODO: RepositoriesVisibility causes errors during the build
// const alwaysFree = [RepositoriesVisibility.Local, RepositoriesVisibility.Public].includes(visibility);
const alwaysFree = ['local', 'public'].includes(visibility);
const needsAccount = ['mixed', 'private'].includes(visibility);
console.log('updateState', alwaysFree, needsAccount, this.state);
let days = 0;
if ([SubscriptionState.FreeInPreviewTrial, SubscriptionState.FreePlusInTrial].includes(subscription.state)) {
days = getSubscriptionTimeRemaining(subscription, 'days') ?? 0;
}
if (subscription.state !== SubscriptionState.Free) {
if (welcomeVisible) {
DOM.insertTemplate('welcome', this.$slots[index++]);
DOM.resetSlot(this.$footer);
} else {
DOM.insertTemplate('links', this.$footer);
}
const timeRemaining = days < 1 ? 'less than one day' : pluralize('day', days);
const shortTimeRemaining = days < 1 ? '<1 day' : pluralize('day', days);
let plan = subscription.plan.effective.name;
let content;
let actions;
let forcePlus = false;
// switch (-1 as SubscriptionState) {
switch (subscription.state) {
case SubscriptionState.Free:
plan = 'Free';
break;
case SubscriptionState.Paid:
break;
case SubscriptionState.FreeInPreviewTrial:
case SubscriptionState.FreePlusInTrial: {
plan = 'Trial';
content = `
<h3>GitLens+ Trial</h3>
<p class="mb-0">
You have ${timeRemaining} left in your&nbsp;
<a title="Learn more about GitLens+ features" href="command:gitlens.plus.learn">
GitLens+ trial </a
>. Once your trial ends, you'll need a paid plan to continue to use GitLens+ features on this
and other private repos.
</p>
`;
actions = shortTimeRemaining;
break;
}
case SubscriptionState.FreePreviewTrialExpired:
forcePlus = true;
plan = 'Free Trial (0 days)';
content = `
<h3>Extend Your GitLens+ Trial</h3>
<p>
Your free trial has ended, please sign in to extend your trial of GitLens+ features on private
repos by an additional 7-days.
</p>
<p class="mb-1">
<vscode-button data-action="command:gitlens.plus.loginOrSignUp">Extend Trial</vscode-button>
</p>
`;
actions = `
<a href="command:gitlens.plus.loginOrSignUp">
Extend Trial
</a>
`;
break;
case SubscriptionState.FreePlusTrialExpired:
forcePlus = true;
plan = 'GitLens+ Trial (0 days)';
content = `
<h3>GitLens+ Trial Expired</h3>
<p>
Your free trial has ended, please upgrade your account to continue to use GitLens+ features,
including the Commit Graph, on this and other private repos.
</p>
<p class="mb-1">
<vscode-button data-action="command:gitlens.plus.purchase">Upgrade Your Account</vscode-button>
</p>
`;
actions = `
<a href="command:gitlens.plus.purchase">
Upgrade Your Account
</a>
`;
break;
case SubscriptionState.VerificationRequired:
forcePlus = true;
plan = 'Unverified';
content = `
<h3>Please verify your email</h3>
<p class="alert__message">Please verify the email for the account you created.</p>
<p class="mb-1">
<vscode-button data-action="command:gitlens.plus.resendVerification"
>Resend Verification Email</vscode-button
>
</p>
<p class="mb-1">
<vscode-button data-action="command:gitlens.plus.validate"
>Refresh Verification Status</vscode-button
>
</p>
`;
actions = `
<a href="command:gitlens.plus.resendVerification" title="Resend Verification Email" aria-label="Resend Verification Email">Verify</a>&nbsp;<a
href="command:gitlens.plus.validate"
title="Refresh Verification Status"
aria-label="Refresh Verification Status"
><span class="codicon codicon-sync"></span
></a>
`;
break;
}
for (let i = 1; i < index; i++) {
this.$slots[i].classList.add('divider');
if (content) {
const $plusContent = document.getElementById('plus-content');
if ($plusContent) {
$plusContent.innerHTML = content;
}
}
for (let i = index; i < this.$slots.length; i++) {
DOM.resetSlot(this.$slots[i]);
const $headerContent = document.getElementById('header-content');
if ($headerContent) {
$headerContent.innerHTML = plan ?? '';
}
const $headerActions = document.getElementById('header-actions');
if ($headerActions) {
$headerActions.innerHTML = actions ?? '';
}
this.$steps?.forEach(el => {
el.setAttribute(
'completed',
(el.id === 'plus' && forcePlus) || completedSteps?.includes(el.id) !== true ? 'false' : 'true',
);
});
this.$cards?.forEach(el => {
if (dismissedSections?.includes(el.id)) {
el.remove();
}
});
}
}

バイナリ
src/webviews/apps/media/getting-started.png ファイルの表示

変更前 変更後
幅: 708  |  高さ: 108  |  サイズ: 50 KiB

バイナリ
src/webviews/apps/media/gitlens-backdrop.png ファイルの表示

変更前 変更後
幅: 386  |  高さ: 242  |  サイズ: 69 KiB

バイナリ
src/webviews/apps/media/gitlens-logo.png ファイルの表示

変更前 変更後
幅: 34  |  高さ: 34  |  サイズ: 2.9 KiB

バイナリ
src/webviews/apps/media/play-button-dark.png ファイルの表示

変更前 変更後
幅: 58  |  高さ: 58  |  サイズ: 1.0 KiB

バイナリ
src/webviews/apps/media/play-button.png ファイルの表示

変更前 変更後
幅: 58  |  高さ: 58  |  サイズ: 1.1 KiB

+ 4
- 0
src/webviews/apps/play-button.svg ファイルの表示

@ -0,0 +1,4 @@
<svg width="29" height="29" viewBox="0 0 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.9532 9.74212C12.9586 9.7458 12.964 9.7495 12.9694 9.75321L18.5016 13.544C18.7263 13.698 18.9163 13.8282 19.0582 13.948C19.2045 14.0715 19.341 14.2169 19.413 14.4175C19.5114 14.6914 19.5114 15.0031 19.413 15.277C19.341 15.4776 19.2045 15.623 19.0582 15.7465C18.9163 15.8663 18.7263 15.9965 18.5017 16.1504L12.953 19.9525C12.7524 20.09 12.5798 20.2083 12.4353 20.2865C12.29 20.3652 12.1102 20.44 11.911 20.407C11.6408 20.3622 11.4197 20.1788 11.2857 19.9367C11.1927 19.7687 11.1628 19.5738 11.149 19.3809C11.1352 19.1878 11.1353 18.9422 11.1353 18.6434V11.0511C11.1353 10.7523 11.1352 10.5067 11.149 10.3136C11.1628 10.1207 11.1927 9.92586 11.2857 9.75778C11.4197 9.51568 11.6408 9.33231 11.911 9.28754C12.1102 9.25454 12.29 9.32935 12.4353 9.40801C12.5798 9.48624 12.7526 9.60462 12.9532 9.74212ZM12.0109 10.0225C11.9761 10.0308 11.9264 10.0605 11.885 10.1352C11.8759 10.1517 11.8548 10.205 11.843 10.3699C11.8315 10.5314 11.8312 10.7485 11.8312 11.0663V18.6281C11.8312 18.946 11.8315 19.163 11.843 19.3246C11.8548 19.4896 11.8759 19.5429 11.885 19.5593C11.9264 19.6341 11.9761 19.6638 12.0108 19.672C12.0203 19.67 12.0525 19.6612 12.1203 19.6245C12.2296 19.5653 12.3725 19.4679 12.5932 19.3167L18.1109 15.5358C18.3542 15.3691 18.5147 15.2586 18.6263 15.1644C18.7366 15.0713 18.7575 15.0273 18.7631 15.0115C18.8001 14.9085 18.8001 14.786 18.7631 14.683C18.7575 14.6672 18.7366 14.6232 18.6263 14.5301C18.5147 14.4359 18.3542 14.3254 18.1109 14.1587L12.5932 10.3778C12.3725 10.2266 12.2296 10.1292 12.1204 10.07C12.0525 10.0333 12.0203 10.0246 12.0109 10.0225Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.3833 28.029C21.9196 28.029 28.029 21.9196 28.029 14.3833C28.029 6.84698 21.9196 0.737604 14.3833 0.737604C6.84698 0.737604 0.737604 6.84698 0.737604 14.3833C0.737604 21.9196 6.84698 28.029 14.3833 28.029ZM14.3833 28.7666C22.327 28.7666 28.7666 22.327 28.7666 14.3833C28.7666 6.43962 22.327 0 14.3833 0C6.43962 0 0 6.43962 0 14.3833C0 22.327 6.43962 28.7666 14.3833 28.7666Z" fill="white"/>
</svg>

+ 75
- 5
src/webviews/home/homeWebviewView.ts ファイルの表示

@ -1,14 +1,23 @@
import type { Disposable } from 'vscode';
import { window } from 'vscode';
import { configuration } from '../../configuration';
import { CoreCommands } from '../../constants';
import type { Container } from '../../container';
import type { RepositoriesVisibility } from '../../git/gitProviderService';
import type { SubscriptionChangeEvent } from '../../plus/subscription/subscriptionService';
import { ensurePlusFeaturesEnabled } from '../../plus/subscription/utils';
import type { Subscription } from '../../subscription';
import { executeCoreCommand, registerCommand } from '../../system/command';
import type { IpcMessage } from '../protocol';
import { onIpc } from '../protocol';
import { WebviewViewBase } from '../webviewViewBase';
import type { State } from './protocol';
import { CompletedActions, DidChangeSubscriptionNotificationType } from './protocol';
import type { CompleteStepParams, DismissSectionParams, State } from './protocol';
import {
CompletedActions,
CompleteStepCommandType,
DidChangeSubscriptionNotificationType,
DismissSectionCommandType,
} from './protocol';
export class HomeWebviewView extends WebviewViewBase<State> {
constructor(container: Container) {
@ -46,9 +55,17 @@ export class HomeWebviewView extends WebviewViewBase {
await this.container.storage.store('views:welcome:visible', welcomeVisible);
if (welcomeVisible) {
await this.container.storage.store('home:actions:completed', []);
await this.container.storage.store('home:steps:completed', []);
await this.container.storage.store('home:sections:dismissed', []);
}
void this.notifyDidChangeData();
void this.refresh();
}),
registerCommand('gitlens.home.restoreWelcome', async () => {
await this.container.storage.store('home:steps:completed', []);
await this.container.storage.store('home:sections:dismissed', []);
void this.refresh();
}),
registerCommand('gitlens.home.showSCM', async () => {
@ -65,6 +82,39 @@ export class HomeWebviewView extends WebviewViewBase {
];
}
protected override onMessageReceived(e: IpcMessage) {
switch (e.method) {
case CompleteStepCommandType.method:
onIpc(CompleteStepCommandType, e, params => this.completeStep(params));
break;
case DismissSectionCommandType.method:
onIpc(DismissSectionCommandType, e, params => this.dismissSection(params));
break;
}
}
private completeStep({ id, completed = false }: CompleteStepParams) {
const steps = this.container.storage.get('home:steps:completed', []);
const hasStep = steps.includes(id);
if (!hasStep && completed) {
steps.push(id);
} else if (hasStep && !completed) {
steps.splice(steps.indexOf(id), 1);
}
void this.container.storage.store('home:steps:completed', steps);
}
private dismissSection(params: DismissSectionParams) {
const sections = this.container.storage.get('home:sections:dismissed', []);
if (!sections.includes(params.id)) {
sections.push(params.id);
}
void this.container.storage.store('home:sections:dismissed', sections);
}
protected override async includeBootstrap(): Promise<State> {
return this.getState();
}
@ -73,7 +123,12 @@ export class HomeWebviewView extends WebviewViewBase {
return this.container.storage.get('views:welcome:visible', true);
}
private async getState(subscription?: Subscription): Promise<State> {
private async getRepoVisibility(): Promise<RepositoriesVisibility> {
const visibility = await this.container.git.visibility();
return visibility;
}
private async getSubscription(subscription?: Subscription) {
// Make sure to make a copy of the array otherwise it will be live to the storage value
const completedActions = [...this.container.storage.get('home:actions:completed', [])];
if (!this.welcomeVisible) {
@ -86,11 +141,26 @@ export class HomeWebviewView extends WebviewViewBase {
};
}
private async getState(subscription?: Subscription): Promise<State> {
const subscriptionState = await this.getSubscription(subscription);
const steps = this.container.storage.get('home:steps:completed', []);
const sections = this.container.storage.get('home:sections:dismissed', []);
return {
subscription: subscriptionState.subscription,
completedActions: subscriptionState.completedActions,
plusEnabled: configuration.get('plusFeatures.enabled'),
visibility: await this.getRepoVisibility(),
completedSteps: steps,
dismissedSections: sections,
};
}
private notifyDidChangeData(subscription?: Subscription) {
if (!this.isReady) return false;
return window.withProgress({ location: { viewId: this.id } }, async () =>
this.notify(DidChangeSubscriptionNotificationType, await this.getState(subscription)),
this.notify(DidChangeSubscriptionNotificationType, await this.getSubscription(subscription)),
);
}

+ 17
- 1
src/webviews/home/protocol.ts ファイルの表示

@ -1,5 +1,6 @@
import type { RepositoriesVisibility } from '../../git/gitProviderService';
import type { Subscription } from '../../subscription';
import { IpcNotificationType } from '../protocol';
import { IpcCommandType, IpcNotificationType } from '../protocol';
export const enum CompletedActions {
DismissedWelcome = 'dismissed:welcome',
@ -9,8 +10,23 @@ export const enum CompletedActions {
export interface State {
subscription: Subscription;
completedActions: CompletedActions[];
completedSteps?: string[];
dismissedSections?: string[];
plusEnabled: boolean;
visibility: RepositoriesVisibility;
}
export interface CompleteStepParams {
id: string;
completed: boolean;
}
export const CompleteStepCommandType = new IpcCommandType<CompleteStepParams>('home/step/complete');
export interface DismissSectionParams {
id: string;
}
export const DismissSectionCommandType = new IpcCommandType<DismissSectionParams>('home/section/dismiss');
export interface DidChangeSubscriptionParams {
subscription: Subscription;
completedActions: CompletedActions[];

+ 9
- 0
src/webviews/webviewViewBase.ts ファイルの表示

@ -189,6 +189,15 @@ export abstract class WebviewViewBase implements
}
}
protected getWebRoot() {
if (this._view == null) return;
const webRootUri = Uri.joinPath(this.container.context.extensionUri, 'dist', 'webviews');
const webRoot = this._view.webview.asWebviewUri(webRootUri).toString();
return webRoot;
}
private async getHtml(webview: Webview): Promise<string> {
const webRootUri = Uri.joinPath(this.container.context.extensionUri, 'dist', 'webviews');
const uri = Uri.joinPath(webRootUri, this.fileName);

読み込み中…
キャンセル
保存