diff --git a/src/storage.ts b/src/storage.ts index e6f9583..9db08bc 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -123,6 +123,12 @@ export interface GlobalStorage { actions: { completed?: CompletedActions[]; }; + steps: { + completed?: string[]; + }; + sections: { + dismissed?: string[]; + }; }; pendingWelcomeOnFocus?: boolean; pendingWhatsNewOnFocus?: boolean; diff --git a/src/webviews/apps/home/components/card-section.ts b/src/webviews/apps/home/components/card-section.ts new file mode 100644 index 0000000..e69b801 --- /dev/null +++ b/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``; + +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'); + } +} diff --git a/src/webviews/apps/home/components/stepped-section.ts b/src/webviews/apps/home/components/stepped-section.ts new file mode 100644 index 0000000..93da395 --- /dev/null +++ b/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``; + +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); + } +} diff --git a/src/webviews/apps/home/home.html b/src/webviews/apps/home/home.html index 1bf9527..75b20e1 100644 --- a/src/webviews/apps/home/home.html +++ b/src/webviews/apps/home/home.html @@ -4,24 +4,312 @@ - -
-
- GitLens+ - introductory pricing will end with the next release - (late Sept, early Oct). - + + skip to content + skip to footer links +
+
+ +

GitLens 12 Git supercharged

+ +
+
+
-
-
-
-
+
+
+ +
+ + Welcome to GitLens 12 +

+ GitLens supercharges Git inside VS Code and unlocks the untapped knowledge within each + repository. +

+

+ Quick Setup +

+
+ + Getting Started + + Get Started Tutorial Video + + + + + Features + *Always available to you at no cost +
+
    +
  • + Find GitLens features by opening the + Source Control Side Bar +
  • +
  • Keep an eye out for feature updates and new components here.
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + Want even more from GitLens? + +
+

+
+ *optional +

+
+

+ GitLens+ adds all-new, completely optional, features that enhance your current + GitLens experience. +

+

These features are free for local and public repos with no account required.

+ +

+ Try GitLens+ with private repositories +

+

+ Hide GitLens+ features, cannot use them +

+
+
+
+
-
- + +
+ + Introducing the Commit Graph +

+ The + Commit Graph + 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. +

+ Commit Graph illustration +
+ + Introducing Visual File History +

+ The + Visual File History + allows you to quickly see the evolution of a file, including when changes were made, how large + they were, and who made them. +

+ Visual File History illustration +
+ + Introducing Worktrees +

+ Worktrees + allow you to easily work on different branches of a repository simultaneously. +

+ Worktrees illustration +
-
+
+ Restore GitLens+ features + Restore Welcome +
+
+ +
+ +
+ #{endOfBody} - - - <%= 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') %> diff --git a/src/webviews/apps/home/home.scss b/src/webviews/apps/home/home.scss index ff6ca9e..443c4c0 100644 --- a/src/webviews/apps/home/home.scss +++ b/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; } diff --git a/src/webviews/apps/home/home.ts b/src/webviews/apps/home/home.ts index 3ca6e73..b8a7dec 100644 --- a/src/webviews/apps/home/home.ts +++ b/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 { - 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('stepped-section[id]')]; + this.$cards = [...document.querySelectorAll('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('stepped-section', 'complete', (e, target: HTMLElement) => + this.onStepComplete(e, target), + ), + ); + disposables.push( + DOM.on('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, 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, 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 = ` +

GitLens+ Trial

+

+ You have ${timeRemaining} left in your  + + GitLens+ trial . Once your trial ends, you'll need a paid plan to continue to use GitLens+ features on this + and other private repos. +

+ `; + actions = shortTimeRemaining; + break; } + case SubscriptionState.FreePreviewTrialExpired: + forcePlus = true; + plan = 'Free Trial (0 days)'; + content = ` +

Extend Your GitLens+ Trial

+

+ Your free trial has ended, please sign in to extend your trial of GitLens+ features on private + repos by an additional 7-days. +

+

+ Extend Trial +

+ `; + actions = ` + + Extend Trial + + `; + break; + case SubscriptionState.FreePlusTrialExpired: + forcePlus = true; + plan = 'GitLens+ Trial (0 days)'; + content = ` +

GitLens+ Trial Expired

+

+ 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. +

+

+ Upgrade Your Account +

+ `; + actions = ` + + Upgrade Your Account + + `; + break; + case SubscriptionState.VerificationRequired: + forcePlus = true; + plan = 'Unverified'; + content = ` +

Please verify your email

+

Please verify the email for the account you created.

+

+ Resend Verification Email +

+

+ Refresh Verification Status +

+ `; + actions = ` + Verify  + `; + 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(); + } + }); } } diff --git a/src/webviews/apps/media/getting-started.png b/src/webviews/apps/media/getting-started.png new file mode 100644 index 0000000..c515b31 Binary files /dev/null and b/src/webviews/apps/media/getting-started.png differ diff --git a/src/webviews/apps/media/gitlens-backdrop.png b/src/webviews/apps/media/gitlens-backdrop.png new file mode 100644 index 0000000..830fdcb Binary files /dev/null and b/src/webviews/apps/media/gitlens-backdrop.png differ diff --git a/src/webviews/apps/media/gitlens-logo.png b/src/webviews/apps/media/gitlens-logo.png new file mode 100644 index 0000000..9f35e25 Binary files /dev/null and b/src/webviews/apps/media/gitlens-logo.png differ diff --git a/src/webviews/apps/media/play-button-dark.png b/src/webviews/apps/media/play-button-dark.png new file mode 100644 index 0000000..d5a003f Binary files /dev/null and b/src/webviews/apps/media/play-button-dark.png differ diff --git a/src/webviews/apps/media/play-button.png b/src/webviews/apps/media/play-button.png new file mode 100644 index 0000000..e24fe6b Binary files /dev/null and b/src/webviews/apps/media/play-button.png differ diff --git a/src/webviews/apps/play-button.svg b/src/webviews/apps/play-button.svg new file mode 100644 index 0000000..cde16d4 --- /dev/null +++ b/src/webviews/apps/play-button.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/webviews/home/homeWebviewView.ts b/src/webviews/home/homeWebviewView.ts index a15edf1..005fdd8 100644 --- a/src/webviews/home/homeWebviewView.ts +++ b/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 { 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 { 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 { + private async getRepoVisibility(): Promise { + 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 { + 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)), ); } diff --git a/src/webviews/home/protocol.ts b/src/webviews/home/protocol.ts index 5149da6..d09c62b 100644 --- a/src/webviews/home/protocol.ts +++ b/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('home/step/complete'); + +export interface DismissSectionParams { + id: string; +} +export const DismissSectionCommandType = new IpcCommandType('home/section/dismiss'); + export interface DidChangeSubscriptionParams { subscription: Subscription; completedActions: CompletedActions[]; diff --git a/src/webviews/webviewViewBase.ts b/src/webviews/webviewViewBase.ts index 6c13375..a4b7e0c 100644 --- a/src/webviews/webviewViewBase.ts +++ b/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 { const webRootUri = Uri.joinPath(this.container.context.extensionUri, 'dist', 'webviews'); const uri = Uri.joinPath(webRootUri, this.fileName);