| // Copyright (c) 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /* eslint-disable rulesdir/no_underscored_properties */ |
| |
| import * as Common from '../../core/common/common.js'; |
| import * as i18n from '../../core/i18n/i18n.js'; |
| import * as SDK from '../../core/sdk/sdk.js'; |
| import * as InlineEditor from '../../ui/legacy/components/inline_editor/inline_editor.js'; |
| import * as Components from '../../ui/legacy/components/utils/utils.js'; |
| import * as UI from '../../ui/legacy/legacy.js'; |
| import type * as Protocol from '../../generated/protocol.js'; |
| |
| const UIStrings = { |
| /** |
| *@description Text in App Manifest View of the Application panel |
| */ |
| noManifestDetected: 'No manifest detected', |
| /** |
| *@description Text in App Manifest View of the Application panel |
| */ |
| appManifest: 'App 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 |
| */ |
| icons: 'Icons', |
| /** |
| *@description Text for the name of something |
| */ |
| name: 'Name', |
| /** |
| *@description Text in App Manifest View of the Application panel |
| */ |
| shortName: 'Short name', |
| /** |
| *@description Text for the description of something |
| */ |
| 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 Text in App Manifest View of the Application panel |
| */ |
| descriptionMayBeTruncated: 'Description may be truncated.', |
| /** |
| *@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 {PH1}.', |
| /** |
| *@description Label for the primary icon loaded from the manifest |
| *@example {https://example.com/} PH1 |
| */ |
| primaryManifestIconFromS: 'Primary manifest icon from {PH1}', |
| /** |
| *@description Label for the primary icon loaded from the manifest |
| */ |
| primaryIconasUsedByChrome: 'Primary icon as used by `Chrome`', |
| /** |
| *@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 96x96 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"` and should not include `"maskable"`.', |
| /** |
| *@description Manifest installability error in the Application panel |
| */ |
| noMatchingServiceWorkerDetected: |
| 'No matching `service worker` detected. You may need to reload the page, or check that the scope of the `service worker` for the current page encloses the scope and start URL from the manifest.', |
| /** |
| *@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 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 {Image} 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', |
| }; |
| const str_ = i18n.i18n.registerUIStrings('panels/application/AppManifestView.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| export class AppManifestView extends UI.Widget.VBox implements SDK.TargetManager.Observer { |
| _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; |
| _shortcutSections: UI.ReportView.Section[]; |
| _screenshotsSections: UI.ReportView.Section[]; |
| _nameField: HTMLElement; |
| _shortNameField: HTMLElement; |
| _descriptionField: Element; |
| _startURLField: HTMLElement; |
| _themeColorSwatch: InlineEditor.ColorSwatch.ColorSwatch; |
| _backgroundColorSwatch: InlineEditor.ColorSwatch.ColorSwatch; |
| _orientationField: HTMLElement; |
| _displayField: HTMLElement; |
| _throttler: Common.Throttler.Throttler; |
| _registeredListeners: Common.EventTarget.EventDescriptor[]; |
| _target?: SDK.Target.Target; |
| _resourceTreeModel?: SDK.ResourceTreeModel.ResourceTreeModel|null; |
| _serviceWorkerManager?: SDK.ServiceWorkerManager.ServiceWorkerManager|null; |
| constructor() { |
| super(true); |
| this.registerRequiredCSS('panels/application/appManifestView.css', {enableLegacyPatching: false}); |
| this.contentElement.classList.add('manifest-container'); |
| |
| Common.Settings.Settings.instance() |
| .moduleSetting('colorFormat') |
| .addChangeListener(this._updateManifest.bind(this, true)); |
| |
| this._emptyView = new UI.EmptyWidget.EmptyWidget(i18nString(UIStrings.noManifestDetected)); |
| this._emptyView.appendLink('https://web.dev/add-manifest/'); |
| |
| this._emptyView.show(this.contentElement); |
| this._emptyView.hideWidget(); |
| |
| // TODO(crbug.com/1156978): Replace UI.ReportView.ReportView with ReportView.ts web component. |
| this._reportView = new UI.ReportView.ReportView(i18nString(UIStrings.appManifest)); |
| this._reportView.registerRequiredCSS('panels/application/appManifestView.css', {enableLegacyPatching: false}); |
| this._reportView.element.classList.add('manifest-view-header'); |
| this._reportView.show(this.contentElement); |
| this._reportView.hideWidget(); |
| |
| this._errorsSection = this._reportView.appendSection(i18nString(UIStrings.errorsAndWarnings)); |
| this._installabilitySection = this._reportView.appendSection(i18nString(UIStrings.installability)); |
| this._identitySection = this._reportView.appendSection(i18nString(UIStrings.identity)); |
| |
| this._presentationSection = this._reportView.appendSection(i18nString(UIStrings.presentation)); |
| this._iconsSection = this._reportView.appendSection(i18nString(UIStrings.icons), 'report-section-icons'); |
| this._shortcutSections = []; |
| this._screenshotsSections = []; |
| |
| this._nameField = this._identitySection.appendField(i18nString(UIStrings.name)); |
| this._shortNameField = this._identitySection.appendField(i18nString(UIStrings.shortName)); |
| this._descriptionField = this._identitySection.appendFlexedField(i18nString(UIStrings.description)); |
| |
| this._startURLField = this._presentationSection.appendField(i18nString(UIStrings.startUrl)); |
| |
| const themeColorField = this._presentationSection.appendField(i18nString(UIStrings.themeColor)); |
| this._themeColorSwatch = new InlineEditor.ColorSwatch.ColorSwatch(); |
| themeColorField.appendChild(this._themeColorSwatch); |
| |
| const backgroundColorField = this._presentationSection.appendField(i18nString(UIStrings.backgroundColor)); |
| this._backgroundColorSwatch = new InlineEditor.ColorSwatch.ColorSwatch(); |
| backgroundColorField.appendChild(this._backgroundColorSwatch); |
| |
| this._orientationField = this._presentationSection.appendField(i18nString(UIStrings.orientation)); |
| this._displayField = this._presentationSection.appendField(i18nString(UIStrings.display)); |
| |
| this._throttler = new Common.Throttler.Throttler(1000); |
| SDK.TargetManager.TargetManager.instance().observeTargets(this); |
| this._registeredListeners = []; |
| } |
| |
| targetAdded(target: SDK.Target.Target): void { |
| if (this._target) { |
| return; |
| } |
| this._target = target; |
| this._resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel); |
| this._serviceWorkerManager = target.model(SDK.ServiceWorkerManager.ServiceWorkerManager); |
| if (!this._resourceTreeModel || !this._serviceWorkerManager) { |
| return; |
| } |
| |
| this._updateManifest(true); |
| |
| this._registeredListeners = [ |
| this._resourceTreeModel.addEventListener( |
| SDK.ResourceTreeModel.Events.DOMContentLoaded, |
| _event => { |
| this._updateManifest(true); |
| }), |
| this._serviceWorkerManager.addEventListener( |
| SDK.ServiceWorkerManager.Events.RegistrationUpdated, |
| _event => { |
| this._updateManifest(false); |
| }), |
| ]; |
| } |
| |
| targetRemoved(target: SDK.Target.Target): void { |
| if (this._target !== target) { |
| return; |
| } |
| if (!this._resourceTreeModel || !this._serviceWorkerManager) { |
| return; |
| } |
| delete this._resourceTreeModel; |
| delete this._serviceWorkerManager; |
| Common.EventTarget.EventTarget.removeEventListeners(this._registeredListeners); |
| } |
| |
| async _updateManifest(immediately: boolean): Promise<void> { |
| if (!this._resourceTreeModel) { |
| return; |
| } |
| const {url, data, errors} = await this._resourceTreeModel.fetchAppManifest(); |
| const installabilityErrors = await this._resourceTreeModel.getInstallabilityErrors(); |
| const manifestIcons = await this._resourceTreeModel.getManifestIcons(); |
| |
| this._throttler.schedule( |
| () => this._renderManifest(url, data, errors, installabilityErrors, manifestIcons), immediately); |
| } |
| |
| async _renderManifest( |
| url: string, data: string|null, errors: Protocol.Page.AppManifestError[], |
| installabilityErrors: Protocol.Page.InstallabilityError[], manifestIcons: { |
| primaryIcon: string|null, |
| }): Promise<void> { |
| if (!data && !errors.length) { |
| this._emptyView.showWidget(); |
| this._reportView.hideWidget(); |
| return; |
| } |
| this._emptyView.hideWidget(); |
| this._reportView.showWidget(); |
| |
| const link = Components.Linkifier.Linkifier.linkifyURL(url); |
| link.tabIndex = 0; |
| this._reportView.setURL(link); |
| this._errorsSection.clearContent(); |
| this._errorsSection.element.classList.toggle('hidden', !errors.length); |
| for (const error of errors) { |
| this._errorsSection.appendRow().appendChild( |
| UI.UIUtils.createIconLabel(error.message, error.critical ? 'smallicon-error' : 'smallicon-warning')); |
| } |
| |
| if (!data) { |
| return; |
| } |
| |
| if (data.charCodeAt(0) === 0xFEFF) { |
| data = data.slice(1); |
| } // Trim the BOM as per https://tools.ietf.org/html/rfc7159#section-8.1. |
| |
| const parsedManifest = JSON.parse(data); |
| this._nameField.textContent = stringProperty('name'); |
| this._shortNameField.textContent = stringProperty('short_name'); |
| |
| const warnings = []; |
| |
| const description = stringProperty('description'); |
| this._descriptionField.textContent = description; |
| if (description.length > 324) { |
| warnings.push(i18nString(UIStrings.descriptionMayBeTruncated)); |
| } |
| |
| this._startURLField.removeChildren(); |
| const startURL = stringProperty('start_url'); |
| if (startURL) { |
| const completeURL = (Common.ParsedURL.ParsedURL.completeURL(url, startURL) as string); |
| const link = Components.Linkifier.Linkifier.linkifyURL( |
| completeURL, ({text: startURL} as Components.Linkifier.LinkifyURLOptions)); |
| link.tabIndex = 0; |
| this._startURLField.appendChild(link); |
| } |
| |
| this._themeColorSwatch.classList.toggle('hidden', !stringProperty('theme_color')); |
| const themeColor = |
| Common.Color.Color.parse(stringProperty('theme_color') || 'white') || Common.Color.Color.parse('white'); |
| if (themeColor) { |
| this._themeColorSwatch.renderColor(themeColor, true); |
| } |
| this._backgroundColorSwatch.classList.toggle('hidden', !stringProperty('background_color')); |
| const backgroundColor = |
| Common.Color.Color.parse(stringProperty('background_color') || 'white') || Common.Color.Color.parse('white'); |
| if (backgroundColor) { |
| this._backgroundColorSwatch.renderColor(backgroundColor, true); |
| } |
| |
| this._orientationField.textContent = stringProperty('orientation'); |
| const displayType = stringProperty('display'); |
| this._displayField.textContent = displayType; |
| |
| const icons = parsedManifest['icons'] || []; |
| this._iconsSection.clearContent(); |
| |
| const shortcuts = parsedManifest['shortcuts'] || []; |
| for (const shortcutsSection of this._shortcutSections) { |
| shortcutsSection.detach(/** overrideHideOnDetach= */ true); |
| } |
| |
| const screenshots = parsedManifest['screenshots'] || []; |
| for (const screenshotSection of this._screenshotsSections) { |
| screenshotSection.detach(/** overrideHideOnDetach= */ true); |
| } |
| |
| const imageErrors = []; |
| |
| const setIconMaskedCheckbox = UI.UIUtils.CheckboxLabel.create(i18nString(UIStrings.showOnlyTheMinimumSafeAreaFor)); |
| setIconMaskedCheckbox.classList.add('mask-checkbox'); |
| setIconMaskedCheckbox.addEventListener('click', () => { |
| this._iconsSection.setIconMasked(setIconMaskedCheckbox.checkboxElement.checked); |
| }); |
| this._iconsSection.appendRow().appendChild(setIconMaskedCheckbox); |
| const documentationLink = |
| UI.XLink.XLink.create('https://web.dev/maskable-icon/', i18nString(UIStrings.documentationOnMaskableIcons)); |
| this._iconsSection.appendRow().appendChild( |
| i18n.i18n.getFormatLocalizedString(str_, UIStrings.needHelpReadOurS, {PH1: documentationLink})); |
| |
| if (manifestIcons && manifestIcons.primaryIcon) { |
| const wrapper = document.createElement('div'); |
| wrapper.classList.add('image-wrapper'); |
| const image = document.createElement('img'); |
| image.style.maxWidth = '200px'; |
| image.style.maxHeight = '200px'; |
| image.src = 'data:image/png;base64,' + manifestIcons.primaryIcon; |
| image.alt = i18nString(UIStrings.primaryManifestIconFromS, {PH1: url}); |
| const title = i18nString(UIStrings.primaryIconasUsedByChrome); |
| const field = this._iconsSection.appendFlexedField(title); |
| wrapper.appendChild(image); |
| field.appendChild(wrapper); |
| } |
| |
| let hasSquareIcon = false; |
| for (const icon of icons) { |
| const iconErrors = |
| await this._appendImageResourceToSection(url, icon, this._iconsSection, /** isScreenshot= */ false); |
| imageErrors.push(...iconErrors); |
| |
| if (!hasSquareIcon) { |
| const [width, height] = icon.sizes.split('x').map((x: string) => parseInt(x, 10)); |
| hasSquareIcon = width === height; |
| } |
| } |
| if (!hasSquareIcon) { |
| imageErrors.push(i18nString(UIStrings.sSShouldHaveSquareIcon)); |
| } |
| |
| let shortcutIndex = 1; |
| for (const shortcut of shortcuts) { |
| const shortcutSection = this._reportView.appendSection(i18nString(UIStrings.shortcutS, {PH1: shortcutIndex})); |
| this._shortcutSections.push(shortcutSection); |
| |
| shortcutSection.appendFlexedField('Name', shortcut.name); |
| if (shortcut.short_name) { |
| shortcutSection.appendFlexedField('Short name', shortcut.short_name); |
| } |
| if (shortcut.description) { |
| shortcutSection.appendFlexedField('Description', shortcut.description); |
| } |
| const urlField = shortcutSection.appendFlexedField('URL'); |
| const shortcutUrl = (Common.ParsedURL.ParsedURL.completeURL(url, shortcut.url) as string); |
| const link = Components.Linkifier.Linkifier.linkifyURL( |
| shortcutUrl, ({text: shortcut.url} as Components.Linkifier.LinkifyURLOptions)); |
| link.tabIndex = 0; |
| urlField.appendChild(link); |
| |
| const shortcutIcons = shortcut.icons || []; |
| let hasShorcutIconLargeEnough = false; |
| for (const shortcutIcon of shortcutIcons) { |
| const shortcutIconErrors = |
| await this._appendImageResourceToSection(url, shortcutIcon, shortcutSection, /** isScreenshot= */ false); |
| imageErrors.push(...shortcutIconErrors); |
| if (!hasShorcutIconLargeEnough && shortcutIcon.sizes) { |
| const shortcutIconSize = shortcutIcon.sizes.match(/^(\d+)x(\d+)$/); |
| if (shortcutIconSize && shortcutIconSize[1] >= 96 && shortcutIconSize[2] >= 96) { |
| hasShorcutIconLargeEnough = true; |
| } |
| } |
| } |
| if (!hasShorcutIconLargeEnough) { |
| imageErrors.push(i18nString(UIStrings.shortcutSShouldIncludeAXPixel, {PH1: shortcutIndex})); |
| } |
| shortcutIndex++; |
| } |
| |
| let screenshotIndex = 1; |
| for (const screenshot of screenshots) { |
| const screenshotSection = |
| this._reportView.appendSection(i18nString(UIStrings.screenshotS, {PH1: screenshotIndex})); |
| this._screenshotsSections.push(screenshotSection); |
| const screenshotErrors = |
| await this._appendImageResourceToSection(url, screenshot, screenshotSection, /** isScreenshot= */ true); |
| imageErrors.push(...screenshotErrors); |
| screenshotIndex++; |
| } |
| |
| this._installabilitySection.clearContent(); |
| this._installabilitySection.element.classList.toggle('hidden', !installabilityErrors.length); |
| const errorMessages = this.getInstallabilityErrorMessages(installabilityErrors); |
| for (const error of errorMessages) { |
| this._installabilitySection.appendRow().appendChild(UI.UIUtils.createIconLabel(error, 'smallicon-warning')); |
| } |
| |
| this._errorsSection.element.classList.toggle('hidden', !errors.length && !imageErrors.length && !warnings.length); |
| for (const warning of warnings) { |
| this._errorsSection.appendRow().appendChild(UI.UIUtils.createIconLabel(warning, 'smallicon-warning')); |
| } |
| for (const error of imageErrors) { |
| this._errorsSection.appendRow().appendChild(UI.UIUtils.createIconLabel(error, 'smallicon-warning')); |
| } |
| |
| function stringProperty(name: string): string { |
| const value = parsedManifest[name]; |
| if (typeof value !== 'string') { |
| return ''; |
| } |
| return value; |
| } |
| } |
| |
| 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-matching-service-worker': |
| errorMessage = i18nString(UIStrings.noMatchingServiceWorkerDetected); |
| 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; |
| } |
| |
| async _loadImage(url: string): Promise<{ |
| image: HTMLImageElement, |
| wrapper: Element, |
| }|null> { |
| const wrapper = document.createElement('div'); |
| wrapper.classList.add('image-wrapper'); |
| const image = (document.createElement('img') as HTMLImageElement); |
| const result = new Promise((resolve, reject) => { |
| image.onload = resolve; |
| image.onerror = reject; |
| }); |
| image.src = url; |
| image.alt = i18nString(UIStrings.imageFromS, {PH1: url}); |
| wrapper.appendChild(image); |
| try { |
| await result; |
| return {wrapper, image}; |
| } catch (e) { |
| } |
| return null; |
| } |
| |
| async _appendImageResourceToSection( |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| baseUrl: string, imageResource: any, section: UI.ReportView.Section, isScreenshot: boolean): Promise<string[]> { |
| const imageResourceErrors = []; |
| const resourceName = isScreenshot ? i18nString(UIStrings.screenshot) : i18nString(UIStrings.icon); |
| if (!imageResource.src) { |
| imageResourceErrors.push(i18nString(UIStrings.sSrcIsNotSet, {PH1: resourceName})); |
| return imageResourceErrors; |
| } |
| const imageUrl = Common.ParsedURL.ParsedURL.completeURL(baseUrl, imageResource['src']); |
| if (!imageUrl) { |
| imageResourceErrors.push( |
| i18nString(UIStrings.sUrlSFailedToParse, {PH1: resourceName, PH2: imageResource['src']})); |
| return imageResourceErrors; |
| } |
| const result = await this._loadImage(imageUrl); |
| if (!result) { |
| imageResourceErrors.push(i18nString(UIStrings.sSFailedToLoad, {PH1: resourceName, PH2: imageUrl})); |
| return imageResourceErrors; |
| } |
| const {wrapper, image} = result; |
| const sizes = imageResource['sizes'] ? imageResource['sizes'].replace('x', '×') + 'px' : ''; |
| const title = sizes + '\n' + (imageResource['type'] || ''); |
| const field = section.appendFlexedField(title); |
| if (!imageResource.sizes) { |
| imageResourceErrors.push(i18nString(UIStrings.sSDoesNotSpecifyItsSizeInThe, {PH1: resourceName, PH2: imageUrl})); |
| } else if (!/^\d+x\d+$/.test(imageResource.sizes)) { |
| imageResourceErrors.push(i18nString(UIStrings.sSShouldSpecifyItsSizeAs, {PH1: resourceName, PH2: imageUrl})); |
| } else { |
| const [width, height] = imageResource.sizes.split('x').map((x: string) => parseInt(x, 10)); |
| if (image.naturalWidth !== width && image.naturalHeight !== height) { |
| imageResourceErrors.push(i18nString(UIStrings.actualSizeSspxOfSSDoesNotMatch, { |
| PH1: image.naturalWidth, |
| PH2: image.naturalHeight, |
| PH3: resourceName, |
| PH4: imageUrl, |
| PH5: width, |
| PH6: height, |
| })); |
| } else if (image.naturalWidth !== width) { |
| imageResourceErrors.push(i18nString( |
| UIStrings.actualWidthSpxOfSSDoesNotMatch, |
| {PH1: image.naturalWidth, PH2: resourceName, PH3: imageUrl, PH4: width})); |
| } else if (image.naturalHeight !== height) { |
| imageResourceErrors.push(i18nString( |
| UIStrings.actualHeightSpxOfSSDoesNotMatch, |
| {PH1: image.naturalHeight, PH2: resourceName, PH3: imageUrl, PH4: height})); |
| } else if (isScreenshot) { |
| if (width < 320 || height < 320) { |
| imageResourceErrors.push(i18nString(UIStrings.sSSizeShouldBeAtLeast320, {PH1: resourceName, PH2: imageUrl})); |
| } else if (width > 3840 || height > 3840) { |
| imageResourceErrors.push(i18nString(UIStrings.sSSizeShouldBeAtMost3840, {PH1: resourceName, PH2: imageUrl})); |
| } else if (width > (height * 2.3)) { |
| imageResourceErrors.push( |
| i18nString(UIStrings.sSWidthDoesNotComplyWithRatioRequirement, {PH1: resourceName, PH2: imageUrl})); |
| } else if (height > (width * 2.3)) { |
| imageResourceErrors.push( |
| i18nString(UIStrings.sSHeightDoesNotComplyWithRatioRequirement, {PH1: resourceName, PH2: imageUrl})); |
| } |
| } |
| } |
| field.appendChild(wrapper); |
| return imageResourceErrors; |
| } |
| } |