chrome-devtools-frontend
Version:
Chrome DevTools UI
1,242 lines (1,188 loc) • 73 kB
text/typescript
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable @devtools/no-imperative-dom-api, @devtools/no-lit-render-outside-of-view */
import '../../ui/legacy/components/inline_editor/inline_editor.js';
import '../../ui/components/report_view/report_view.js';
import * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import * as Buttons from '../../ui/components/buttons/buttons.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import {Directives, html, i18nTemplate, type LitTemplate, nothing, render} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import appManifestViewStyles from './appManifestView.css.js';
import * as ApplicationComponents from './components/components.js';
const {classMap, ref} = Directives;
const {linkifyURL} = Components.Linkifier.Linkifier;
const {widgetConfig} = UI.Widget;
const UIStrings = {
/**
* @description Text in App Manifest View of the Application panel
*/
noManifestDetected: 'No manifest detected',
/**
* @description Description text on manifests in App Manifest View of the Application panel which describes the app manifest view tab
*/
manifestDescription:
'A manifest defines how your app appears on phone’s home screens and what the app looks like on launch.',
/**
* @description Text in App Manifest View of the Application panel
*/
appManifest: 'Manifest',
/**
* @description Text in App Manifest View of the Application panel
*/
errorsAndWarnings: 'Errors and warnings',
/**
* @description Text in App Manifest View of the Application panel
*/
installability: 'Installability',
/**
* @description Text in App Manifest View of the Application panel
*/
identity: 'Identity',
/**
* @description Text in App Manifest View of the Application panel
*/
presentation: 'Presentation',
/**
* @description Text in App Manifest View of the Application panel
*/
protocolHandlers: 'Protocol Handlers',
/**
* @description Text in App Manifest View of the Application panel
*/
icons: 'Icons',
/**
* @description Text in App Manifest View of the Application panel
*/
windowControlsOverlay: 'Window Controls Overlay',
/**
* @description Label in the App Manifest View for the "name" property of web app or shortcut item
*/
name: 'Name',
/**
* @description Label in the App Manifest View for the "short_name" property of web app or shortcut item
*/
shortName: 'Short name',
/**
* @description Label in the App Manifest View for the "url" property of shortcut item
*/
url: 'URL',
/**
* @description Label in the App Manifest View for the Computed App Id
*/
computedAppId: 'Computed App ID',
/**
* @description Popup-text explaining what the App Id is used for.
*/
appIdExplainer:
'This is used by the browser to know whether the manifest should be updating an existing application, or whether it refers to a new web app that can be installed.',
/**
* @description Text which is a hyperlink to more documentation
*/
learnMore: 'Learn more',
/**
* @description Explanation why it is advisable to specify an 'id' field in the manifest.
* @example {/index.html} PH1
* @example {(button for copying suggested value into clipboard)} PH2
*/
appIdNote:
'Note: `id` is not specified in the manifest, `start_url` is used instead. To specify an App ID that matches the current identity, set the `id` field to {PH1} {PH2}.',
/**
* @description Tooltip text that appears when hovering over a button which copies the previous text to the clipboard.
*/
copyToClipboard: 'Copy suggested ID to clipboard',
/**
* @description Screen reader announcement string when the user clicks the copy to clipboard button.
* @example {/index.html} PH1
*/
copiedToClipboard: 'Copied suggested ID {PH1} to clipboard',
/**
* @description Label in the App Manifest View for the "description" property of web app or shortcut item
*/
description: 'Description',
/**
* @description Text in App Manifest View of the Application panel
*/
startUrl: 'Start URL',
/**
* @description Text in App Manifest View of the Application panel
*/
themeColor: 'Theme color',
/**
* @description Text in App Manifest View of the Application panel
*/
backgroundColor: 'Background color',
/**
* @description Text for the orientation of something
*/
orientation: 'Orientation',
/**
* @description Title of the display attribute in App Manifest View of the Application panel
* The display attribute defines the preferred display mode for the app such fullscreen or
* standalone.
* For more details see https://www.w3.org/TR/appmanifest/#display-member.
*/
display: 'Display',
/**
* @description Title of the new_note_url attribute in the Application panel
*/
newNoteUrl: 'New note URL',
/**
* @description Text in App Manifest View of the Application panel
*/
descriptionMayBeTruncated: 'Description may be truncated.',
/**
* @description Warning text about too many shortcuts
*/
shortcutsMayBeNotAvailable:
'The maximum number of shortcuts is platform dependent. Some shortcuts may be not available.',
/**
* @description Text in App Manifest View of the Application panel
*/
showOnlyTheMinimumSafeAreaFor: 'Show only the minimum safe area for maskable icons',
/**
* @description Link text for more information on maskable icons in App Manifest view of the Application panel
*/
documentationOnMaskableIcons: 'documentation on maskable icons',
/**
* @description Text wrapping a link pointing to more information on maskable icons in App Manifest view of the Application panel
* @example {https://web.dev/maskable-icon/} PH1
*/
needHelpReadOurS: 'Need help? Read the {PH1}.',
/**
* @description Text in App Manifest View of the Application panel
* @example {1} PH1
*/
shortcutS: 'Shortcut #{PH1}',
/**
* @description Text in App Manifest View of the Application panel
* @example {1} PH1
*/
shortcutSShouldIncludeAXPixel: 'Shortcut #{PH1} should include a 96×96 pixel icon',
/**
* @description Text in App Manifest View of the Application panel
* @example {1} PH1
*/
screenshotS: 'Screenshot #{PH1}',
/**
* @description Manifest installability error in the Application panel
*/
pageIsNotLoadedInTheMainFrame: 'Page is not loaded in the main frame',
/**
* @description Manifest installability error in the Application panel
*/
pageIsNotServedFromASecureOrigin: 'Page is not served from a secure origin',
/**
* @description Manifest installability error in the Application panel
*/
pageHasNoManifestLinkUrl: 'Page has no manifest <link> `URL`',
/**
* @description Manifest installability error in the Application panel
*/
manifestCouldNotBeFetchedIsEmpty: 'Manifest could not be fetched, is empty, or could not be parsed',
/**
* @description Manifest installability error in the Application panel
*/
manifestStartUrlIsNotValid: 'Manifest \'`start_url`\' is not valid',
/**
* @description Manifest installability error in the Application panel
*/
manifestDoesNotContainANameOr: 'Manifest does not contain a \'`name`\' or \'`short_name`\' field',
/**
* @description Manifest installability error in the Application panel
*/
manifestDisplayPropertyMustBeOne:
'Manifest \'`display`\' property must be one of \'`standalone`\', \'`fullscreen`\', or \'`minimal-ui`\'',
/**
* @description Manifest installability error in the Application panel
* @example {100} PH1
*/
manifestDoesNotContainASuitable:
'Manifest does not contain a suitable icon—PNG, SVG, or WebP format of at least {PH1}px is required, the \'`sizes`\' attribute must be set, and the \'`purpose`\' attribute, if set, must include \'`any`\'.',
/**
* @description Manifest installability error in the Application panel
*/
avoidPurposeAnyAndMaskable:
'Declaring an icon with \'`purpose`\' of \'`any maskable`\' is discouraged. It is likely to look incorrect on some platforms due to too much or too little padding.',
/**
* @description Manifest installability error in the Application panel
* @example {100} PH1
*/
noSuppliedIconIsAtLeastSpxSquare:
'No supplied icon is at least {PH1} pixels square in `PNG`, `SVG`, or `WebP` format, with the purpose attribute unset or set to \'`any`\'.',
/**
* @description Manifest installability error in the Application panel
*/
couldNotDownloadARequiredIcon: 'Could not download a required icon from the manifest',
/**
* @description Manifest installability error in the Application panel
*/
downloadedIconWasEmptyOr: 'Downloaded icon was empty or corrupted',
/**
* @description Manifest installability error in the Application panel
*/
theSpecifiedApplicationPlatform: 'The specified application platform is not supported on Android',
/**
* @description Manifest installability error in the Application panel
*/
noPlayStoreIdProvided: 'No Play store ID provided',
/**
* @description Manifest installability error in the Application panel
*/
thePlayStoreAppUrlAndPlayStoreId: 'The Play Store app URL and Play Store ID do not match',
/**
* @description Manifest installability error in the Application panel
*/
theAppIsAlreadyInstalled: 'The app is already installed',
/**
* @description Manifest installability error in the Application panel
*/
aUrlInTheManifestContainsA: 'A URL in the manifest contains a username, password, or port',
/**
* @description Manifest installability error in the Application panel
*/
pageIsLoadedInAnIncognitoWindow: 'Page is loaded in an incognito window',
/**
* @description Manifest installability error in the Application panel
*/
pageDoesNotWorkOffline: 'Page does not work offline',
/**
* @description Manifest installability error in the Application panel
*/
couldNotCheckServiceWorker: 'Could not check `service worker` without a \'`start_url`\' field in the manifest',
/**
* @description Manifest installability error in the Application panel
*/
manifestSpecifies: 'Manifest specifies \'`prefer_related_applications`: true\'',
/**
* @description Manifest installability error in the Application panel
*/
preferrelatedapplicationsIsOnly:
'\'`prefer_related_applications`\' is only supported on `Chrome` Beta and Stable channels on `Android`.',
/**
* @description Manifest installability error in the Application panel
*/
manifestContainsDisplayoverride:
'Manifest contains \'`display_override`\' field, and the first supported display mode must be one of \'`standalone`\', \'`fullscreen`\', or \'`minimal-ui`\'',
/**
* @description Warning message for offline capability check
* @example {https://developer.chrome.com/blog/improved-pwa-offline-detection} PH1
*/
pageDoesNotWorkOfflineThePage:
'Page does not work offline. Starting in Chrome 93, the installability criteria are changing, and this site will not be installable. See {PH1} for more information.',
/**
* @description Text to indicate the source of an image
* @example {example.com} PH1
*/
imageFromS: 'Image from {PH1}',
/**
* @description Text for one or a group of screenshots
*/
screenshot: 'Screenshot',
/**
* @description Label in the App Manifest View for the "form_factor" property of screenshot
*/
formFactor: 'Form factor',
/**
* @description Label in the App Manifest View for the "label" property of screenshot
*/
label: 'Label',
/**
* @description Label in the App Manifest View for the "platform" property of screenshot
*/
platform: 'Platform',
/**
* @description Text in App Manifest View of the Application panel
*/
icon: 'Icon',
/**
* @description This is a warning message telling the user about a problem where the src attribute
* of an image has not be entered/provided correctly. 'src' is part of the DOM API and should not
* be translated.
* @example {ImageName} PH1
*/
sSrcIsNotSet: '{PH1} \'`src`\' is not set',
/**
* @description Warning message for image resources from the manifest
* @example {Screenshot} PH1
* @example {https://example.com/image.png} PH2
*/
sUrlSFailedToParse: '{PH1} URL \'\'{PH2}\'\' failed to parse',
/**
* @description Warning message for image resources from the manifest
* @example {Image} PH1
* @example {https://example.com/image.png} PH2
*/
sSFailedToLoad: '{PH1} {PH2} failed to load',
/**
* @description Warning message for image resources from the manifest
* @example {Image} PH1
* @example {https://example.com/image.png} PH2
*/
sSDoesNotSpecifyItsSizeInThe: '{PH1} {PH2} does not specify its size in the manifest',
/**
* @description Warning message for image resources from the manifest
* @example {Image} PH1
* @example {https://example.com/image.png} PH2
*/
sSShouldSpecifyItsSizeAs: '{PH1} {PH2} should specify its size as `[width]x[height]`',
/**
* @description Warning message for image resources from the manifest
*/
sSShouldHaveSquareIcon:
'Most operating systems require square icons. Please include at least one square icon in the array.',
/**
* @description Warning message for image resources from the manifest
* @example {100} PH1
* @example {100} PH2
* @example {Image} PH3
* @example {https://example.com/image.png} PH4
* @example {200} PH5
* @example {200} PH6
*/
actualSizeSspxOfSSDoesNotMatch:
'Actual size ({PH1}×{PH2})px of {PH3} {PH4} does not match specified size ({PH5}×{PH6}px)',
/**
* @description Warning message for image resources from the manifest
* @example {100} PH1
* @example {Image} PH2
* @example {https://example.com/image.png} PH3
* @example {200} PH4
*/
actualWidthSpxOfSSDoesNotMatch: 'Actual width ({PH1}px) of {PH2} {PH3} does not match specified width ({PH4}px)',
/**
* @description Warning message for image resources from the manifest
* @example {100} PH1
* @example {Image} PH2
* @example {https://example.com/image.png} PH3
* @example {100} PH4
*/
actualHeightSpxOfSSDoesNotMatch: 'Actual height ({PH1}px) of {PH2} {PH3} does not match specified height ({PH4}px)',
/**
* @description Warning message for image resources from the manifest
* @example {Image} PH1
* @example {https://example.com/image.png} PH2
*/
sSSizeShouldBeAtLeast320: '{PH1} {PH2} size should be at least 320×320',
/**
* @description Warning message for image resources from the manifest
* @example {Image} PH1
* @example {https://example.com/image.png} PH2
*/
sSSizeShouldBeAtMost3840: '{PH1} {PH2} size should be at most 3840×3840',
/**
* @description Warning message for image resources from the manifest
* @example {Image} PH1
* @example {https://example.com/image.png} PH2
*/
sSWidthDoesNotComplyWithRatioRequirement: '{PH1} {PH2} width can\'t be more than 2.3 times as long as the height',
/**
* @description Warning message for image resources from the manifest
* @example {Image} PH1
* @example {https://example.com/image.png} PH2
*/
sSHeightDoesNotComplyWithRatioRequirement: '{PH1} {PH2} height can\'t be more than 2.3 times as long as the width',
/**
* @description Manifest installability error in the Application panel
* @example {https://example.com/image.png} url
*/
screenshotPixelSize:
'Screenshot {url} should specify a pixel size `[width]x[height]` instead of `any` as first size.',
/**
* @description Warning text about screenshots for Richer PWA Install UI on desktop
*/
noScreenshotsForRicherPWAInstallOnDesktop:
'Richer PWA Install UI won’t be available on desktop. Please add at least one screenshot with the `form_factor` set to `wide`.',
/**
* @description Warning text about screenshots for Richer PWA Install UI on mobile
*/
noScreenshotsForRicherPWAInstallOnMobile:
'Richer PWA Install UI won’t be available on mobile. Please add at least one screenshot for which `form_factor` is not set or set to a value other than `wide`.',
/**
* @description Warning text about too many screenshots for desktop
*/
tooManyScreenshotsForDesktop: 'No more than 8 screenshots will be displayed on desktop. The rest will be ignored.',
/**
* @description Warning text about too many screenshots for mobile
*/
tooManyScreenshotsForMobile: 'No more than 5 screenshots will be displayed on mobile. The rest will be ignored.',
/**
* @description Warning text about not all screenshots matching the appropriate form factor have the same aspect ratio
*/
screenshotsMustHaveSameAspectRatio:
'All screenshots with the same `form_factor` must have the same aspect ratio as the first screenshot with that `form_factor`. Some screenshots will be ignored.',
/**
* @description Message for Window Controls Overlay value succsessfully found with links to documnetation
* @example {window-controls-overlay} PH1
* @example {https://developer.mozilla.org/en-US/docs/Web/Manifest/display_override} PH2
* @example {https://developer.mozilla.org/en-US/docs/Web/Manifest} PH3
*/
wcoFound: 'Chrome has successfully found the {PH1} value for the {PH2} field in the {PH3}.',
/**
* @description Message for Windows Control Overlay value not found with link to documentation
* @example {https://developer.mozilla.org/en-US/docs/Web/Manifest/display_override} PH1
*/
wcoNotFound:
'Define {PH1} in the manifest to use the Window Controls Overlay API and customize your app\'s title bar.',
/**
* @description Link text for more information on customizing Window Controls Overlay title bar in the Application panel
*/
customizePwaTitleBar: 'Customize the window controls overlay of your PWA\'s title bar',
/**
* @description Text wrapping link to documentation on how to customize WCO title bar
* @example {https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/how-to/window-controls-overlay} PH1
*/
wcoNeedHelpReadMore: 'Need help? Read {PH1}.',
/**
* @description Text for emulation OS selection dropdown
*/
selectWindowControlsOverlayEmulationOs: 'Emulate the Window Controls Overlay on',
/**
* @description Alert message for screen reader to announce which subsection is being scrolled to
* @example {"Identity"} PH1
*/
onInvokeAlert: 'Scrolled to {PH1}',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/application/AppManifestView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export type ParsedSize = {
any: 'any',
formatted: string,
}|{
width: number,
height: number,
formatted: string,
};
interface Screenshot {
src: string;
type?: string;
sizes?: string;
label?: string;
form_factor?: string; // eslint-disable-line @typescript-eslint/naming-convention
platform?: string;
}
/* eslint-disable @typescript-eslint/naming-convention */
interface Manifest {
background_color?: string;
description?: string;
display?: string;
display_override?: string[];
icons?: Array<{
src: string,
sizes?: string,
type?: string,
purpose?: string,
}>;
id?: string;
name?: string;
note_taking?: {
new_note_url?: string,
};
orientation?: string;
protocol_handlers?: Protocol.Page.ProtocolHandler[];
screenshots?: Screenshot[];
short_name?: string;
shortcuts?: Array<{
name: string,
url: string,
description?: string,
short_name?: string,
icons?: Array<{
src: string,
sizes?: string,
type?: string,
purpose?: string,
}>,
}>;
start_url?: string;
theme_color?: string;
}
/* eslint-enable @typescript-eslint/naming-convention */
interface ReportSectionItem {
content: LitTemplate|LitTemplate[]|string|HTMLElement;
title?: string;
label?: string;
flexed?: boolean;
}
interface IdentitySectionData {
name: string;
shortName: string;
description: string;
appId: string|null;
recommendedId: string|null;
hasId: boolean;
warnings: Platform.UIString.LocalizedString[];
}
interface PresentationSectionData {
startUrl: string;
completeStartUrl: Platform.DevToolsPath.UrlString|null;
themeColor: Common.Color.Color|null;
backgroundColor: Common.Color.Color|null;
orientation: string;
display: string;
newNoteUrl?: string;
hasNewNoteUrl: boolean;
completeNewNoteUrl: Platform.DevToolsPath.UrlString|null;
}
interface ProtocolHandlersSectionData {
protocolHandlers: Protocol.Page.ProtocolHandler[];
manifestLink: Platform.DevToolsPath.UrlString;
}
interface IconsSectionData {
icons: Map<string, ProcessedImageResource[]>;
imageResourceErrors: Platform.UIString.LocalizedString[];
}
interface ProcessedShortcut {
name: string;
shortName?: string;
description?: string;
url: string;
shortcutUrl: Platform.DevToolsPath.UrlString;
icons: Map<string, ProcessedImageResource[]>;
}
interface ShortcutsSectionData {
shortcuts: ProcessedShortcut[];
warnings: Platform.UIString.LocalizedString[];
imageResourceErrors: Platform.UIString.LocalizedString[];
}
interface ProcessedScreenshot {
screenshot: Screenshot;
processedImage: ProcessedImageResource;
}
interface ScreenshotsSectionData {
screenshots: ProcessedScreenshot[];
warnings: Platform.UIString.LocalizedString[];
imageResourceErrors: Platform.UIString.LocalizedString[];
}
interface WindowControlsSectionData {
hasWco: boolean;
themeColor: string;
wcoStyleSheetText: boolean;
url: Platform.DevToolsPath.UrlString;
}
type ProcessedImageResource = {
imageResourceErrors: Platform.UIString.LocalizedString[],
imageUrl?: string,
squareSizedIconAvailable?: boolean,
}|{
imageResourceErrors: Platform.UIString.LocalizedString[],
imageUrl: string,
squareSizedIconAvailable: boolean,
title: string,
naturalWidth: number,
naturalHeight: number,
imageSrc: string,
};
function renderSectionHeader(text: Platform.UIString.LocalizedString, output?: ViewOutput): LitTemplate {
// clang-format off
return html`
<devtools-report-section-header
${ref(e => { if (output && e instanceof HTMLElement) {
output.scrollToSection.set(text, () => { e.scrollIntoView(); });
}})}>
${text}
</devtools-report-section-header>`;
// clang-format on
}
function renderErrors(
errorsSection: UI.ReportView.Section, warnings?: Platform.UIString.LocalizedString[],
manifestErrors?: Protocol.Page.AppManifestError[], imageErrors?: Platform.UIString.LocalizedString[]): void {
errorsSection.clearContent();
errorsSection.element.classList.toggle(
'hidden', !manifestErrors?.length && !warnings?.length && !imageErrors?.length);
for (const error of manifestErrors ?? []) {
const icon = UI.UIUtils.createIconLabel({
title: error.message,
iconName: error.critical ? 'cross-circle-filled' : 'warning-filled',
color: error.critical ? 'var(--icon-error)' : 'var(--icon-warning)',
});
errorsSection.appendRow().appendChild(icon);
}
for (const warning of warnings ?? []) {
const msgElement = document.createTextNode(warning);
errorsSection.appendRow().appendChild(msgElement);
}
for (const error of imageErrors ?? []) {
const msgElement = document.createTextNode(error);
errorsSection.appendRow().appendChild(msgElement);
}
}
function renderIdentity(identitySection: UI.ReportView.Section, identityData: IdentitySectionData): void {
const {name, shortName, description, appId, recommendedId, hasId} = identityData;
const fields: ReportSectionItem[] = [];
fields.push({title: i18nString(UIStrings.name), content: name});
fields.push({title: i18nString(UIStrings.shortName), content: shortName});
fields.push({title: i18nString(UIStrings.description), content: description});
if (appId && recommendedId) {
const onCopy = (): void => {
UI.ARIAUtils.LiveAnnouncer.alert(i18nString(UIStrings.copiedToClipboard, {PH1: recommendedId}));
Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(recommendedId);
};
// clang-format off
fields.push({title: i18nString(UIStrings.computedAppId), label: 'App Id', content: html`
${appId}
<devtools-icon class="inline-icon" name="help" title=${i18nString(UIStrings.appIdExplainer)}
jslog=${VisualLogging.action('help').track({hover: true})}>
</devtools-icon>
<devtools-link href="https://developer.chrome.com/blog/pwa-manifest-id/"
.jslogContext=${'learn-more'}>
${i18nString(UIStrings.learnMore)}
</devtools-link>
${!hasId ? html`
<div class="multiline-value">
${i18nTemplate(str_, UIStrings.appIdNote, {
PH1: html`<code>${recommendedId}</code>`,
PH2: html`<devtools-button class="inline-button" @click=${onCopy}
.iconName=${'copy'}
.variant=${Buttons.Button.Variant.ICON}
.size=${Buttons.Button.Size.SMALL}
.jslogContext=${'manifest.copy-id'}
.title=${i18nString(UIStrings.copyToClipboard)}>
</devtools-button>`,
})}
</div>` : nothing}`});
// clang-format on
} else {
identitySection.removeField(i18nString(UIStrings.computedAppId));
}
setSectionContents(fields, identitySection);
}
function renderPresentation(
presentationSection: UI.ReportView.Section, presentationData: PresentationSectionData): void {
const {
startUrl,
completeStartUrl,
themeColor,
backgroundColor,
orientation,
display,
newNoteUrl,
hasNewNoteUrl,
completeNewNoteUrl,
} = presentationData;
const fields: ReportSectionItem[] = [
{
title: i18nString(UIStrings.startUrl),
label: i18nString(UIStrings.startUrl),
content: completeStartUrl ? Components.Linkifier.Linkifier.linkifyURL(
completeStartUrl, ({text: startUrl, tabStop: true, jslogContext: 'start-url'})) :
nothing,
},
{
title: i18nString(UIStrings.themeColor),
content: themeColor ? html`<devtools-color-swatch .color=${themeColor}></devtools-color-swatch>` : nothing,
},
{
title: i18nString(UIStrings.backgroundColor),
content: backgroundColor ? html`<devtools-color-swatch .color=${backgroundColor}></devtools-color-swatch>` :
nothing,
},
{title: i18nString(UIStrings.orientation), content: orientation},
{title: i18nString(UIStrings.display), content: display},
];
if (completeNewNoteUrl) {
fields.push({
title: i18nString(UIStrings.newNoteUrl),
content: hasNewNoteUrl ?
Components.Linkifier.Linkifier.linkifyURL(completeNewNoteUrl, ({text: newNoteUrl, tabStop: true})) :
nothing,
});
}
setSectionContents(fields, presentationSection);
}
function renderProtocolHandlers(data: ProtocolHandlersSectionData, output: ViewOutput): LitTemplate {
// clang-format off
return html`${renderSectionHeader(i18nString(UIStrings.protocolHandlers), output)}
<div class="report-row">
<devtools-widget .widgetConfig=${widgetConfig(
ApplicationComponents.ProtocolHandlersView.ProtocolHandlersView,
{protocolHandlers: data.protocolHandlers, manifestLink: data.manifestLink})}
${ref(setFocusOnSection(i18nString(UIStrings.protocolHandlers), output))}>
</devtools-widget>
</div>
<devtools-report-divider></devtools-report-divider>`;
// clang-format on
}
function renderImage(imageSrc: string, imageUrl: string, naturalWidth: number): LitTemplate {
// clang-format off
return html`
<div class="image-wrapper">
<img src=${imageSrc} alt=${i18nString(UIStrings.imageFromS, {PH1: imageUrl})}
width=${naturalWidth}>
</div>`;
// clang-format on
}
function setFocusOnSection(section: Platform.UIString.LocalizedString, output: ViewOutput): (e: Element|undefined) =>
void {
return (e: Element|undefined) => {
if (e instanceof HTMLElement) {
output.focusOnSection.set(section, () => e.focus());
}
};
}
function renderIcons(
data: IconsSectionData, maskedIcons: boolean, onToggleIconMasked: (value: boolean) => void,
output: ViewOutput): LitTemplate {
// clang-format off
return html`${renderSectionHeader(i18nString(UIStrings.icons), output)}
<div class="report-section" jslog=${VisualLogging.section('icons')}>
<div class="report-row">
<devtools-checkbox class="mask-checkbox"
jslog=${VisualLogging.toggle('show-minimal-safe-area-for-maskable-icons')
.track({change: true})}
@click=${(event: Event) => onToggleIconMasked((event.target as HTMLInputElement).checked)}
${ref(setFocusOnSection(i18nString(UIStrings.icons), output))}>
${i18nString(UIStrings.showOnlyTheMinimumSafeAreaFor)}
</devtools-checkbox>
</div>
<div class="report-row">
${i18nTemplate(str_, UIStrings.needHelpReadOurS, {
PH1: html`
<devtools-link href="https://web.dev/maskable-icon/" .jslogContext=${'learn-more'}>
${i18nString(UIStrings.documentationOnMaskableIcons)}
</devtools-link>`,
})}
</div>
${Array.from(data.icons).map(([title, images]: [string, ProcessedImageResource[]]) => {
return html`
<devtools-report-key>${title}</devtools-report-key>
<devtools-report-value class=${classMap({'show-mask': Boolean(maskedIcons)})}>
${images.filter(icon => 'imageSrc' in icon)
.map(icon => renderImage(icon.imageSrc, icon.imageUrl, icon.naturalWidth))}
</devtools-report-value>
`;})}
</div>`;
// clang-format on
}
function renderShortcuts(data: ShortcutsSectionData): LitTemplate {
// clang-format off
return html`${data.shortcuts.map((shortcut, index) => html`
${renderSectionHeader(i18nString(UIStrings.shortcutS, {PH1: index + 1}))}
<div class="report-section" jslog=${VisualLogging.section('shortcuts')}>
<devtools-report-key>${i18nString(UIStrings.name)}</devtools-report-key>
<devtools-report-value>${shortcut.name}</devtools-report-value>
${shortcut.shortName ? html`
<devtools-report-key>${i18nString(UIStrings.shortName)}</devtools-report-key>
<devtools-report-value>${shortcut.shortName}</devtools-report-value>
` : nothing}
${shortcut.description ? html`
<devtools-report-key>${i18nString(UIStrings.description)}</devtools-report-key>
<devtools-report-value>${shortcut.description}</devtools-report-value>
` : nothing}
<devtools-report-key>${i18nString(UIStrings.url)}</devtools-report-key>
<devtools-report-value>
${linkifyURL(shortcut.shortcutUrl, {text: shortcut.url, tabStop: true, jslogContext: 'shortcut'})}
</devtools-report-value>
${Array.from(shortcut.icons).map(([title, images]) => html`
<devtools-report-key>${title}</devtools-report-key>
<devtools-report-value>
${images.filter(icon => 'imageSrc' in icon)
.map(icon => renderImage(icon.imageSrc, icon.imageUrl, icon.naturalWidth))}
</devtools-report-value>
`)}
</div>`)}`;
// clang-format on
}
function renderScreenshots(data: ScreenshotsSectionData): LitTemplate {
// clang-format off
return html`${data.screenshots.map(({screenshot, processedImage}, index) => html`
${renderSectionHeader(i18nString(UIStrings.screenshotS, {PH1: index + 1}))}
<div class="report-section" jslog=${VisualLogging.section('screenshots')}>
${screenshot.form_factor
? html`<devtools-report-key>${i18nString(UIStrings.formFactor)}</devtools-report-key>
<devtools-report-value>${screenshot.form_factor}</devtools-report-value>`
: nothing}
${screenshot.label
? html`<devtools-report-key>${i18nString(UIStrings.label)}</devtools-report-key>
<devtools-report-value>${screenshot.label}</devtools-report-value>`
: nothing}
${screenshot.platform
? html`<devtools-report-key>${i18nString(UIStrings.platform)}</devtools-report-key>
<devtools-report-value>${screenshot.platform}</devtools-report-value>`
: nothing}
${'imageSrc' in processedImage ? html`
<devtools-report-key>${processedImage.title}</devtools-report-key>
<devtools-report-value>
${renderImage(processedImage.imageSrc, processedImage.imageUrl, processedImage.naturalWidth)}
</devtools-report-value>`
: nothing}
</div>
`)}`;
// clang-format on
}
function renderInstallability(
installabilitySection: UI.ReportView.Section, installabilityErrors: Protocol.Page.InstallabilityError[]): void {
installabilitySection.clearContent();
installabilitySection.element.classList.toggle('hidden', !installabilityErrors.length);
const errorMessages = getInstallabilityErrorMessages(installabilityErrors);
setSectionContents(errorMessages.map(content => ({content})), installabilitySection);
}
function renderWindowControlsSection(
data: WindowControlsSectionData, selectedPlatform: string|undefined,
onSelectOs: ((selectedOS: SDK.OverlayModel.EmulatedOSType) => Promise<void>)|undefined,
onToggleWcoToolbar: ((enabled: boolean) => Promise<void>)|undefined, output: ViewOutput): LitTemplate {
// clang-format off
return html`
${renderSectionHeader(i18nString(UIStrings.windowControlsOverlay), output)}
<div class="report-section" jslog=${VisualLogging.section('window-controls-overlay')}>
${data?.hasWco && output ? html`
<div class="report-row">
<devtools-icon class="inline-icon" name="check-circle"></devtools-icon>
${i18nTemplate(str_, UIStrings.wcoFound, {
PH1: html`<code class="wco">window-controls-overlay</code>`,
PH2: html`<code>
<devtools-link
href="https://developer.mozilla.org/en-US/docs/Web/Manifest/display_override"
.jslogContext=${'display-override'}
${ref(setFocusOnSection(i18nString(UIStrings.windowControlsOverlay), output))}>
display-override
</devtools-link>
</code>`,
PH3: html`${Components.Linkifier.Linkifier.linkifyURL(data.url)}`,
})}
</div>
${selectedPlatform && onSelectOs && onToggleWcoToolbar ?
renderWindowControls(selectedPlatform, onSelectOs, onToggleWcoToolbar) :
nothing}` : html`
<div class="report-row">
<devtools-icon class="inline-icon" name="info"></devtools-icon>
${i18nTemplate(str_, UIStrings.wcoNotFound, {PH1: html`<code>
<devtools-link
href="https://developer.mozilla.org/en-US/docs/Web/Manifest/display_override"
.jslogContext=${'display-override'}
${ref(setFocusOnSection(i18nString(UIStrings.windowControlsOverlay), output))}>
display-override
</devtools-link>
</code>`})}
</div>`}
<div class="report-row">
${i18nTemplate(str_, UIStrings.wcoNeedHelpReadMore, {PH1: html`<devtools-link
href="https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/how-to/window-controls-overlay"
.jslogContext=${'customize-pwa-tittle-bar'}>
${i18nString(UIStrings.customizePwaTitleBar)}
</devtools-link>`})}
</div>
</div>`;
// clang-format on
}
function getInstallabilityErrorMessages(installabilityErrors: Protocol.Page.InstallabilityError[]): string[] {
const errorMessages = [];
for (const installabilityError of installabilityErrors) {
let errorMessage;
switch (installabilityError.errorId) {
case 'not-in-main-frame':
errorMessage = i18nString(UIStrings.pageIsNotLoadedInTheMainFrame);
break;
case 'not-from-secure-origin':
errorMessage = i18nString(UIStrings.pageIsNotServedFromASecureOrigin);
break;
case 'no-manifest':
errorMessage = i18nString(UIStrings.pageHasNoManifestLinkUrl);
break;
case 'manifest-empty':
errorMessage = i18nString(UIStrings.manifestCouldNotBeFetchedIsEmpty);
break;
case 'start-url-not-valid':
errorMessage = i18nString(UIStrings.manifestStartUrlIsNotValid);
break;
case 'manifest-missing-name-or-short-name':
errorMessage = i18nString(UIStrings.manifestDoesNotContainANameOr);
break;
case 'manifest-display-not-supported':
errorMessage = i18nString(UIStrings.manifestDisplayPropertyMustBeOne);
break;
case 'manifest-missing-suitable-icon':
if (installabilityError.errorArguments.length !== 1 ||
installabilityError.errorArguments[0].name !== 'minimum-icon-size-in-pixels') {
console.error('Installability error does not have the correct errorArguments');
break;
}
errorMessage =
i18nString(UIStrings.manifestDoesNotContainASuitable, {PH1: installabilityError.errorArguments[0].value});
break;
case 'no-acceptable-icon':
if (installabilityError.errorArguments.length !== 1 ||
installabilityError.errorArguments[0].name !== 'minimum-icon-size-in-pixels') {
console.error('Installability error does not have the correct errorArguments');
break;
}
errorMessage =
i18nString(UIStrings.noSuppliedIconIsAtLeastSpxSquare, {PH1: installabilityError.errorArguments[0].value});
break;
case 'cannot-download-icon':
errorMessage = i18nString(UIStrings.couldNotDownloadARequiredIcon);
break;
case 'no-icon-available':
errorMessage = i18nString(UIStrings.downloadedIconWasEmptyOr);
break;
case 'platform-not-supported-on-android':
errorMessage = i18nString(UIStrings.theSpecifiedApplicationPlatform);
break;
case 'no-id-specified':
errorMessage = i18nString(UIStrings.noPlayStoreIdProvided);
break;
case 'ids-do-not-match':
errorMessage = i18nString(UIStrings.thePlayStoreAppUrlAndPlayStoreId);
break;
case 'already-installed':
errorMessage = i18nString(UIStrings.theAppIsAlreadyInstalled);
break;
case 'url-not-supported-for-webapk':
errorMessage = i18nString(UIStrings.aUrlInTheManifestContainsA);
break;
case 'in-incognito':
errorMessage = i18nString(UIStrings.pageIsLoadedInAnIncognitoWindow);
break;
case 'not-offline-capable':
errorMessage = i18nString(UIStrings.pageDoesNotWorkOffline);
break;
case 'no-url-for-service-worker':
errorMessage = i18nString(UIStrings.couldNotCheckServiceWorker);
break;
case 'prefer-related-applications':
errorMessage = i18nString(UIStrings.manifestSpecifies);
break;
case 'prefer-related-applications-only-beta-stable':
errorMessage = i18nString(UIStrings.preferrelatedapplicationsIsOnly);
break;
case 'manifest-display-override-not-supported':
errorMessage = i18nString(UIStrings.manifestContainsDisplayoverride);
break;
case 'warn-not-offline-capable':
errorMessage = i18nString(
UIStrings.pageDoesNotWorkOfflineThePage,
{PH1: 'https://developer.chrome.com/blog/improved-pwa-offline-detection/'});
break;
default:
console.error(`Installability error id '${installabilityError.errorId}' is not recognized`);
break;
}
if (errorMessage) {
errorMessages.push(errorMessage);
}
}
return errorMessages;
}
function renderWindowControls(
selectedPlatform: string, onSelectOs: (selectedOS: SDK.OverlayModel.EmulatedOSType) => Promise<void>,
onToggleWcoToolbar: (enabled: boolean) => Promise<void>): LitTemplate {
// clang-format off
return html`<div class="report-row">
<devtools-checkbox @click=${(event: Event) => onToggleWcoToolbar((event.target as HTMLInputElement).checked)}
title=${i18nString(UIStrings.selectWindowControlsOverlayEmulationOs)}>
${i18nString(UIStrings.selectWindowControlsOverlayEmulationOs)}
</devtools-checkbox>
<select value=${selectedPlatform}
@change=${(event: Event): void => {
const target = event.target as HTMLSelectElement;
const selectedOS = target.options[target.selectedIndex].value;
void onSelectOs(selectedOS as SDK.OverlayModel.EmulatedOSType);
}}
.selectedIndex=${0}>
<option value=${SDK.OverlayModel.EmulatedOSType.WINDOWS}
jslog=${VisualLogging.item('windows').track({click: true})}>
Windows
</option>
<option value=${SDK.OverlayModel.EmulatedOSType.MAC}
jslog=${VisualLogging.item('macos').track({click: true })}>
macOS
</option>
<option value=${SDK.OverlayModel.EmulatedOSType.LINUX}
jslog=${VisualLogging.item('linux').track({click: true})}>
Linux
</option>
</select>
</div>`;
// clang-format on
}
function setSectionContents(items: ReportSectionItem[], section: UI.ReportView.Section): void {
for (const item of items) {
if (!item.title) {
render(item.content, section.appendRow());
continue;
}
const element = item.flexed ? section.appendFlexedField(item.title) : section.appendField(item.title);
if (item.label) {
UI.ARIAUtils.setLabel(element, item.label);
}
render(item.content, element);
}
}
interface ViewInput {
emptyView: UI.EmptyWidget.EmptyWidget;
reportView: UI.ReportView.ReportView;
errorsSection?: UI.ReportView.Section;
installabilitySection?: UI.ReportView.Section;
identitySection?: UI.ReportView.Section;
presentationSection?: UI.ReportView.Section;
iconsSection?: UI.ReportView.Section;
maskedIcons?: boolean;
windowControlsSection?: UI.ReportView.Section;
shortcutSections?: UI.ReportView.Section[];
screenshotsSections?: UI.ReportView.Section[];
parsedManifest?: Manifest;
url?: Platform.DevToolsPath.UrlString;
identityData?: IdentitySectionData;
presentationData?: PresentationSectionData;
protocolHandlersData?: ProtocolHandlersSectionData;
iconsData?: IconsSectionData;
shortcutsData?: ShortcutsSectionData;
screenshotsData?: ScreenshotsSectionData;
installabilityErrors?: Protocol.Page.InstallabilityError[];
warnings?: Platform.UIString.LocalizedString[];
errors?: Protocol.Page.AppManifestError[];
imageErrors?: Platform.UIString.LocalizedString[];
windowControlsData?: WindowControlsSectionData;
selectedPlatform?: string;
onSelectOs?: (selectedOS: SDK.OverlayModel.EmulatedOSType) => Promise<void>;
onToggleWcoToolbar?: (enabled: boolean) => Promise<void>;
onToggleIconMasked?: (masked: boolean) => void;
}
interface ViewOutput {
scrollToSection: Map<string, () => void>;
focusOnSection: Map<string, () => void>;
}
type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void;
export const DEFAULT_VIEW: View = (input, output, target) => {
const {
errorsSection,
installabilitySection,
identitySection,
presentationSection,
identityData,
presentationData,
protocolHandlersData,
iconsData,
shortcutsData,
screenshotsData,
installabilityErrors,
warnings,
errors,
imageErrors,
maskedIcons,
windowControlsData,
selectedPlatform,
onSelectOs,
onToggleWcoToolbar,
onToggleIconMasked,
} = input;
if (identitySection && identityData) {
renderIdentity(identitySection, identityData);
}
if (presentationSection && presentationData) {
renderPresentation(presentationSection, presentationData);
}
if (installabilitySection && installabilityErrors) {
renderInstallability(installabilitySection, installabilityErrors);
}
if (errorsSection) {
renderErrors(errorsSection, warnings, errors, imageErrors);
}
// clang-format off
render(html`
<style>${appManifestViewStyles}</style>
<devtools-report>
${protocolHandlersData ? renderProtocolHandlers(protocolHandlersData, output) : nothing}
${iconsData && onToggleIconMasked && maskedIcons ?
renderIcons(iconsData, maskedIcons, onToggleIconMasked, output) : nothing}
${windowControlsData && output ? renderWindowControlsSection(
windowControlsData, selectedPlatform, onSelectOs, onToggleWcoToolbar, output) : nothing}
${shortcutsData ? renderShortcuts(shortcutsData) : nothing}
${screenshotsData ? renderScreenshots(screenshotsData) : nothing}
</devtools-report>`, target);
// clang-format on
};
export class AppManifestView extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.Widget.VBox>(UI.Widget.VBox)
implements SDK.TargetManager.Observer {
private readonly emptyView: UI.EmptyWidget.EmptyWidget;
private readonly reportView: UI.ReportView.ReportView;
private readonly errorsSection: UI.ReportView.Section;
private readonly installabilitySection: UI.ReportView.Section;
private readonly identitySection: UI.ReportView.Section;
private readonly presentationSection: UI.ReportView.Section;
private registeredListeners: Common.EventTarget.EventDescriptor[];
private target?: SDK.Target.Target;
private resourceTreeModel?: SDK.ResourceTreeModel.ResourceTreeModel|null;
private serviceWorkerManager?: SDK.ServiceWorkerManager.ServiceWorkerManager|null;
private overlayModel?: SDK.OverlayModel.OverlayModel|null;
private manifestUrl: Platform.DevToolsPath.UrlString;
private manifestData: string|null;
private manifestErrors: Protocol.Page.AppManifestError[];
private installabilityErrors: Protocol.Page.InstallabilityError[];
private appIdResponse: Protocol.Page.GetAppIdResponse|null;
private wcoToolbarEnabled = false;
private maskedIcons = false;
private readonly view: View;
private readonly output: ViewOutput = {scrollToSection: new Map(), focusOnSection: new Map()};
constructor(view: View = DEFAULT_VIEW) {
super({
jslog: `${VisualLogging.pane('manifest')}`,
useShadowDom: true,
});
this.view = view;
this.contentElement.classList.add('manifest-container');
this.emptyView = new UI.EmptyWidget.EmptyWidget(
i18nString(UIStrings.noManifestDetected), i18nString(UIStrings.manifestDescription));
this.emptyView.link = 'https://web.dev/add-manifest/' as Platform.DevToolsPath.UrlString;
this.emptyView.show(this.contentElement);
this.emptyView.hideWidget();
this.reportView = new UI.ReportView.ReportView(i18nString(UIStrings.appManifest));
this.reportView.registerRequiredCSS(appManifestViewStyles);
this.reportView.element.classList.add('manifest-view-header');
this.reportView.show(this.contentElement);
this.reportView.hideWidget();
this.errorsSection =
this.reportView.appendSection(i18nString(UIStrings.errorsAndWarnings), undefined, 'errors-and-warnings');
this.installabilitySection =
this.reportView.appendSection(i18nString(UIStrings.installability), undefined, 'installability');
this.identitySection = this.reportView.appendSection(i18nString(UIStrings.identity), 'undefined,identity');
this.presentationSection =
this.reportView.appendSection(i18nString(UIStrings.presentation), 'undefined,presentation');
SDK.TargetManager.TargetManager.instance().observeTargets(this);
this.registeredListeners = [];
this.manifestUrl = Platform.DevToolsPath.EmptyUrlString;
this.manifestData = null;
this.manifestErrors = [];
this.installabilityErrors = [];
this.appIdResponse = null;
}
scrollToSection(sectionTitle: string): void {
const handler = this.output.scrollToSection.get(sectionTitle);
if (handler) {
handler();
} else {
const section = this.getManifestSections().find(s => s.title() === sectionTitle);
if (section) {
section.getTitleElement().scrollIntoView();
}
}
UI.ARIAUtils.LiveAnnouncer.alert(i18nString(UIStrings.onInvokeAlert, {PH1: sectionTitle}));
}
getFieldElementForSection(sectionTitle: string): HTMLElement|null {
const section = this.getManifestSections().find(s => s.title() === sectionTitle);
return section ? section.getFieldElement() : null;
}
focusOnSection(sectionTitle: string): boolean {
const handler = this.output.focusOnSection.get(sectionTitle);
if (handler) {
handler();
return true;
}
const sectionFieldElement = this.getFieldElementForSection(sectionTitle);
if (!sectionFieldElement) {
return false;
}
const checkBoxElement = sectionFieldElement.querySelector('.mask-checkbox');
let focusableElement: HTMLElement|null = sectionFieldElement.querySelector('[tabindex="0"]');
if (checkBoxElement?.shadowRoot) {
focusableElement = checkBoxElement.shadowRoot.querySelector('input') || null;
}
if (focusableElement) {
focu