UNPKG

chrome-devtools-frontend

Version:
1,242 lines (1,188 loc) 73 kB
// 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