blob: 91385bd698cdeb7447afb476fc82c3c629d709af [file] [log] [blame]
// Copyright 2014 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.
import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Root from '../../core/root/root.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as UI from '../../ui/legacy/legacy.js';
import {MaxDeviceSize, MinDeviceSize} from './DeviceModeModel.js';
const UIStrings = {
/**
*@description Title of the Laptop with touch device
*/
laptopWithTouch: 'Laptop with touch',
/**
*@description Title of the Laptop with HiDPI screen device
*/
laptopWithHiDPIScreen: 'Laptop with HiDPI screen',
/**
*@description Title of the Laptop with MDPI screen device
*/
laptopWithMDPIScreen: 'Laptop with MDPI screen',
};
const str_ = i18n.i18n.registerUIStrings('panels/emulation/EmulatedDevices.js', UIStrings);
const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined, str_);
export class EmulatedDevice {
constructor() {
/** @type {string} */
this.title = '';
/** @type {string} */
this.type = Type.Unknown;
/** @type {number} */
this.order;
/** @type {!Orientation} */
this.vertical = {width: 0, height: 0, outlineInsets: null, outlineImage: null, hinge: null};
/** @type {!Orientation} */
this.horizontal = {width: 0, height: 0, outlineInsets: null, outlineImage: null, hinge: null};
/** @type {number} */
this.deviceScaleFactor = 1;
/** @type {!Array.<string>} */
this.capabilities = [Capability.Touch, Capability.Mobile];
/** @type {string} */
this.userAgent = '';
/** @type {?Protocol.Emulation.UserAgentMetadata} */
this.userAgentMetadata = null;
/** @type {!Array.<!Mode>} */
this.modes = [];
/** @type {boolean} */
this.isDualScreen = false;
/** @type {!Orientation} */
this.verticalSpanned = {width: 0, height: 0, outlineInsets: null, outlineImage: null, hinge: null};
/** @type {!Orientation} */
this.horizontalSpanned = {width: 0, height: 0, outlineInsets: null, outlineImage: null, hinge: null};
/** @type {string} */
this._show = _Show.Default;
/** @type {boolean} */
this._showByDefault = true;
}
/**
* @param {*} json
* @return {?EmulatedDevice}
*/
static fromJSONV1(json) {
try {
/**
* @param {*} object
* @param {string} key
* @param {string} type
* @param {*=} defaultValue
* @return {*}
*/
function parseValue(object, key, type, defaultValue) {
if (typeof object !== 'object' || object === null || !object.hasOwnProperty(key)) {
if (typeof defaultValue !== 'undefined') {
return defaultValue;
}
throw new Error('Emulated device is missing required property \'' + key + '\'');
}
const value = object[key];
if (typeof value !== type || value === null) {
throw new Error('Emulated device property \'' + key + '\' has wrong type \'' + typeof value + '\'');
}
return value;
}
/**
* @param {*} object
* @param {string} key
* @return {number}
*/
function parseIntValue(object, key) {
const value = /** @type {number} */ (parseValue(object, key, 'number'));
if (value !== Math.abs(value)) {
throw new Error('Emulated device value \'' + key + '\' must be integer');
}
return value;
}
/**
* @param {*} json
* @return {!UI.Geometry.Insets}
*/
function parseInsets(json) {
return new UI.Geometry.Insets(
parseIntValue(json, 'left'), parseIntValue(json, 'top'), parseIntValue(json, 'right'),
parseIntValue(json, 'bottom'));
}
/**
* @param {*} json
* @return {!SDK.OverlayModel.HighlightColor}
*/
function parseRGBA(json) {
const result = {};
result.r = parseIntValue(json, 'r');
if (result.r < 0 || result.r > 255) {
throw new Error('color has wrong r value: ' + result.r);
}
result.g = parseIntValue(json, 'g');
if (result.g < 0 || result.g > 255) {
throw new Error('color has wrong g value: ' + result.g);
}
result.b = parseIntValue(json, 'b');
if (result.b < 0 || result.b > 255) {
throw new Error('color has wrong b value: ' + result.b);
}
result.a = /** @type {number} */ (parseValue(json, 'a', 'number'));
if (result.a < 0 || result.a > 1) {
throw new Error('color has wrong a value: ' + result.a);
}
return /** @type {!SDK.OverlayModel.HighlightColor} */ (result);
}
/**
* @param {*} json
* @return {!SDK.OverlayModel.Hinge}
*/
function parseHinge(json) {
const result = {};
result.width = parseIntValue(json, 'width');
if (result.width < 0 || result.width > MaxDeviceSize) {
throw new Error('Emulated device has wrong hinge width: ' + result.width);
}
result.height = parseIntValue(json, 'height');
if (result.height < 0 || result.height > MaxDeviceSize) {
throw new Error('Emulated device has wrong hinge height: ' + result.height);
}
result.x = parseIntValue(json, 'x');
if (result.x < 0 || result.x > MaxDeviceSize) {
throw new Error('Emulated device has wrong x offset: ' + result.height);
}
result.y = parseIntValue(json, 'y');
if (result.x < 0 || result.x > MaxDeviceSize) {
throw new Error('Emulated device has wrong y offset: ' + result.height);
}
if (json['contentColor']) {
result.contentColor = parseRGBA(json['contentColor']);
}
if (json['outlineColor']) {
result.outlineColor = parseRGBA(json['outlineColor']);
}
return /** @type {!SDK.OverlayModel.Hinge} */ (result);
}
/**
* @param {*} json
* @return {!Orientation}
*/
function parseOrientation(json) {
const result = {};
result.width = parseIntValue(json, 'width');
if (result.width < 0 || result.width > MaxDeviceSize || result.width < MinDeviceSize) {
throw new Error('Emulated device has wrong width: ' + result.width);
}
result.height = parseIntValue(json, 'height');
if (result.height < 0 || result.height > MaxDeviceSize || result.height < MinDeviceSize) {
throw new Error('Emulated device has wrong height: ' + result.height);
}
const outlineInsets = parseValue(json['outline'], 'insets', 'object', null);
if (outlineInsets) {
result.outlineInsets = parseInsets(outlineInsets);
if (result.outlineInsets.left < 0 || result.outlineInsets.top < 0) {
throw new Error('Emulated device has wrong outline insets');
}
result.outlineImage = /** @type {string} */ (parseValue(json['outline'], 'image', 'string'));
}
if (json['hinge']) {
result.hinge = parseHinge(parseValue(json, 'hinge', 'object', undefined));
}
return /** @type {!Orientation} */ (result);
}
const result = new EmulatedDevice();
result.title = /** @type {string} */ (parseValue(json, 'title', 'string'));
result.type = /** @type {string} */ (parseValue(json, 'type', 'string'));
result.order = /** @type {number} */ (parseValue(json, 'order', 'number', 0));
const rawUserAgent = /** @type {string} */ (parseValue(json, 'user-agent', 'string'));
result.userAgent = SDK.NetworkManager.MultitargetNetworkManager.patchUserAgentWithChromeVersion(rawUserAgent);
result.userAgentMetadata =
/** @type {?Protocol.Emulation.UserAgentMetadata} */ parseValue(json, 'user-agent-metadata', 'object', null);
const capabilities = parseValue(json, 'capabilities', 'object', []);
if (!Array.isArray(capabilities)) {
throw new Error('Emulated device capabilities must be an array');
}
result.capabilities = [];
for (let i = 0; i < capabilities.length; ++i) {
if (typeof capabilities[i] !== 'string') {
throw new Error('Emulated device capability must be a string');
}
result.capabilities.push(capabilities[i]);
}
result.deviceScaleFactor = /** @type {number} */ (parseValue(json['screen'], 'device-pixel-ratio', 'number'));
if (result.deviceScaleFactor < 0 || result.deviceScaleFactor > 100) {
throw new Error('Emulated device has wrong deviceScaleFactor: ' + result.deviceScaleFactor);
}
result.vertical = parseOrientation(parseValue(json['screen'], 'vertical', 'object'));
result.horizontal = parseOrientation(parseValue(json['screen'], 'horizontal', 'object'));
result.isDualScreen = /** @type {boolean} */ (parseValue(json, 'dual-screen', 'boolean', false));
if (result.isDualScreen) {
result.verticalSpanned = parseOrientation(parseValue(json['screen'], 'vertical-spanned', 'object', null));
result.horizontalSpanned = parseOrientation(parseValue(json['screen'], 'horizontal-spanned', 'object', null));
}
if (result.isDualScreen && (!result.verticalSpanned || !result.horizontalSpanned)) {
throw new Error('Emulated device \'' + result.title + '\'has dual screen without spanned orientations');
}
const modes = parseValue(json, 'modes', 'object', [
{'title': 'default', 'orientation': 'vertical'},
{'title': 'default', 'orientation': 'horizontal'},
]);
if (!Array.isArray(modes)) {
throw new Error('Emulated device modes must be an array');
}
result.modes = [];
for (let i = 0; i < modes.length; ++i) {
const mode = {};
mode.title = /** @type {string} */ (parseValue(modes[i], 'title', 'string'));
mode.orientation = /** @type {string} */ (parseValue(modes[i], 'orientation', 'string'));
if (mode.orientation !== Vertical && mode.orientation !== Horizontal && mode.orientation !== VerticalSpanned &&
mode.orientation !== HorizontalSpanned) {
throw new Error('Emulated device mode has wrong orientation \'' + mode.orientation + '\'');
}
const orientation = result.orientationByName(mode.orientation);
mode.insets = parseInsets(parseValue(modes[i], 'insets', 'object', {left: 0, top: 0, right: 0, bottom: 0}));
if (mode.insets.top < 0 || mode.insets.left < 0 || mode.insets.right < 0 || mode.insets.bottom < 0 ||
mode.insets.top + mode.insets.bottom > orientation.height ||
mode.insets.left + mode.insets.right > orientation.width) {
throw new Error('Emulated device mode \'' + mode.title + '\'has wrong mode insets');
}
mode.image = /** @type {string} */ (parseValue(modes[i], 'image', 'string', null));
result.modes.push(mode);
}
result._showByDefault = /** @type {boolean} */ (parseValue(json, 'show-by-default', 'boolean', undefined));
result._show =
/** @type {string} */ (parseValue(json, 'show', 'string', _Show.Default));
return result;
} catch (e) {
return null;
}
}
/**
* @param {!EmulatedDevice} device1
* @param {!EmulatedDevice} device2
* @return {number}
*/
static deviceComparator(device1, device2) {
const order1 = device1.order || 0;
const order2 = device2.order || 0;
if (order1 > order2) {
return 1;
}
if (order2 > order1) {
return -1;
}
return device1.title < device2.title ? -1 : (device1.title > device2.title ? 1 : 0);
}
/**
* @param {string} orientation
* @return {!Array.<!Mode>}
*/
modesForOrientation(orientation) {
const result = [];
for (let index = 0; index < this.modes.length; index++) {
if (this.modes[index].orientation === orientation) {
result.push(this.modes[index]);
}
}
return result;
}
/**
* @param {!Mode} mode
* @return {(!Mode|undefined)}
*/
getSpanPartner(mode) {
switch (mode.orientation) {
case Vertical:
return this.modesForOrientation(VerticalSpanned)[0];
case Horizontal:
return this.modesForOrientation(HorizontalSpanned)[0];
case VerticalSpanned:
return this.modesForOrientation(Vertical)[0];
default:
return this.modesForOrientation(Horizontal)[0];
}
}
/**
* @param {!Mode} mode
* @return {?Mode}
*/
getRotationPartner(mode) {
switch (mode.orientation) {
case HorizontalSpanned:
return this.modesForOrientation(VerticalSpanned)[0];
case VerticalSpanned:
return this.modesForOrientation(HorizontalSpanned)[0];
case Horizontal:
return this.modesForOrientation(Vertical)[0];
default:
return this.modesForOrientation(Horizontal)[0];
}
}
/**
* @return {*}
*/
_toJSON() {
const json = {};
json['title'] = this.title;
json['type'] = this.type;
json['user-agent'] = this.userAgent;
json['capabilities'] = this.capabilities;
/** @type {{'device-pixel-ratio': number, vertical: object, horizontal: object, 'vertical-spanned': (object|undefined), 'horizontal-spanned': (object|undefined)}} */
json['screen'] = {
'device-pixel-ratio': this.deviceScaleFactor,
vertical: this._orientationToJSON(this.vertical),
horizontal: this._orientationToJSON(this.horizontal),
'vertical-spanned': undefined,
'horizontal-spanned': undefined,
};
if (this.isDualScreen) {
json['screen']['vertical-spanned'] = this._orientationToJSON(this.verticalSpanned);
json['screen']['horizontal-spanned'] = this._orientationToJSON(this.horizontalSpanned);
}
/** @type {!Array.<!JSONMode>} */
json['modes'] = [];
for (let i = 0; i < this.modes.length; ++i) {
/** @type {!JSONMode} */
const mode = {
'title': this.modes[i].title,
'orientation': this.modes[i].orientation,
'insets': {
'left': this.modes[i].insets.left,
'top': this.modes[i].insets.top,
'right': this.modes[i].insets.right,
'bottom': this.modes[i].insets.bottom,
},
image: this.modes[i].image || undefined,
};
json['modes'].push(mode);
}
json['show-by-default'] = this._showByDefault;
json['dual-screen'] = this.isDualScreen;
json['show'] = this._show;
if (this.userAgentMetadata) {
json['user-agent-metadata'] = this.userAgentMetadata;
}
return json;
}
/**
* @param {!Orientation} orientation
* @return {*}
*/
_orientationToJSON(orientation) {
const json = {};
json['width'] = orientation.width;
json['height'] = orientation.height;
if (orientation.outlineInsets) {
/** @type {!{image: ?string, insets: {left: number, right: number, top: number, bottom: number}}} */
json.outline = {
insets: {
'left': orientation.outlineInsets.left,
'top': orientation.outlineInsets.top,
'right': orientation.outlineInsets.right,
'bottom': orientation.outlineInsets.bottom,
},
image: orientation.outlineImage
};
}
if (orientation.hinge) {
/** @type {!{width: number, height: number, x: number, y: number, contentColor: (!{r:number,g:number,b:number,a:number}|undefined), outlineColor: (!{r:number,g:number,b:number,a:number}|undefined)
* }} */
json.hinge = {
'width': orientation.hinge.width,
'height': orientation.hinge.height,
'x': orientation.hinge.x,
'y': orientation.hinge.y,
contentColor: undefined,
outlineColor: undefined,
};
if (orientation.hinge.contentColor) {
json.hinge.contentColor = {
'r': orientation.hinge.contentColor.r,
'g': orientation.hinge.contentColor.g,
'b': orientation.hinge.contentColor.b,
'a': orientation.hinge.contentColor.a,
};
}
if (orientation.hinge.outlineColor) {
json.hinge.outlineColor = {
'r': orientation.hinge.outlineColor.r,
'g': orientation.hinge.outlineColor.g,
'b': orientation.hinge.outlineColor.b,
'a': orientation.hinge.outlineColor.a,
};
}
}
return json;
}
/**
* @param {!Mode} mode
* @return {string}
*/
modeImage(mode) {
if (!mode.image) {
return '';
}
return Root.Runtime.Runtime.instance().module('emulated_devices').substituteURL(mode.image);
}
/**
* @param {!Mode} mode
* @return {string}
*/
outlineImage(mode) {
const orientation = this.orientationByName(mode.orientation);
if (!orientation.outlineImage) {
return '';
}
return Root.Runtime.Runtime.instance().module('emulated_devices').substituteURL(orientation.outlineImage);
}
/**
* @param {string} name
* @return {!Orientation}
*/
orientationByName(name) {
switch (name) {
case VerticalSpanned:
return this.verticalSpanned;
case HorizontalSpanned:
return this.horizontalSpanned;
case Vertical:
return this.vertical;
default:
return this.horizontal;
}
}
/**
* @return {boolean}
*/
show() {
if (this._show === _Show.Default) {
return this._showByDefault;
}
return this._show === _Show.Always;
}
/**
* @param {boolean} show
*/
setShow(show) {
this._show = show ? _Show.Always : _Show.Never;
}
/**
* @param {!EmulatedDevice} other
*/
copyShowFrom(other) {
this._show = other._show;
}
/**
* @return {boolean}
*/
touch() {
return this.capabilities.indexOf(Capability.Touch) !== -1;
}
/**
* @return {boolean}
*/
mobile() {
return this.capabilities.indexOf(Capability.Mobile) !== -1;
}
}
export const Horizontal = 'horizontal';
export const Vertical = 'vertical';
export const HorizontalSpanned = 'horizontal-spanned';
export const VerticalSpanned = 'vertical-spanned';
export const Type = {
Phone: 'phone',
Tablet: 'tablet',
Notebook: 'notebook',
Desktop: 'desktop',
Unknown: 'unknown'
};
export const Capability = {
Touch: 'touch',
Mobile: 'mobile'
};
export const _Show = {
Always: 'Always',
Default: 'Default',
Never: 'Never'
};
/** @type {!EmulatedDevicesList} */
let _instance;
export class EmulatedDevicesList extends Common.ObjectWrapper.ObjectWrapper {
constructor() {
super();
/** @type {!Common.Settings.Setting<!Array<?>>} */
this._standardSetting = Common.Settings.Settings.instance().createSetting('standardEmulatedDeviceList', []);
/** @type {!Set.<!EmulatedDevice>} */
this._standard = new Set();
this._listFromJSONV1(this._standardSetting.get(), this._standard);
this._updateStandardDevices();
/** @type {!Common.Settings.Setting<!Array<?>>} */
this._customSetting = Common.Settings.Settings.instance().createSetting('customEmulatedDeviceList', []);
/** @type {!Set.<!EmulatedDevice>} */
this._custom = new Set();
if (!this._listFromJSONV1(this._customSetting.get(), this._custom)) {
this.saveCustomDevices();
}
}
/**
* @return {!EmulatedDevicesList}
*/
static instance() {
if (!_instance) {
_instance = new EmulatedDevicesList();
}
return _instance;
}
_updateStandardDevices() {
/** @type {!Set<!EmulatedDevice>} */
const devices = new Set();
for (const extension of emulatedDevices) {
const device = EmulatedDevice.fromJSONV1(extension);
if (device) {
devices.add(device);
}
}
this._copyShowValues(this._standard, devices);
this._standard = devices;
this.saveStandardDevices();
}
/**
* @param {!Array.<*>} jsonArray
* @param {!Set.<!EmulatedDevice>} result
* @return {boolean}
*/
_listFromJSONV1(jsonArray, result) {
if (!Array.isArray(jsonArray)) {
return false;
}
let success = true;
for (let i = 0; i < jsonArray.length; ++i) {
const device = EmulatedDevice.fromJSONV1(jsonArray[i]);
if (device) {
result.add(device);
if (!device.modes.length) {
device.modes.push(
{title: '', orientation: Horizontal, insets: new UI.Geometry.Insets(0, 0, 0, 0), image: null});
device.modes.push(
{title: '', orientation: Vertical, insets: new UI.Geometry.Insets(0, 0, 0, 0), image: null});
}
} else {
success = false;
}
}
return success;
}
/**
* @return {!Array.<!EmulatedDevice>}
*/
standard() {
return [...this._standard];
}
/**
* @return {!Array.<!EmulatedDevice>}
*/
custom() {
return [...this._custom];
}
revealCustomSetting() {
Common.Revealer.reveal(this._customSetting);
}
/**
* @param {!EmulatedDevice} device
*/
addCustomDevice(device) {
this._custom.add(device);
this.saveCustomDevices();
}
/**
* @param {!EmulatedDevice} device
*/
removeCustomDevice(device) {
this._custom.delete(device);
this.saveCustomDevices();
}
saveCustomDevices() {
/** @type {!Array<?>} */
const json = [];
this._custom.forEach(device => json.push(device._toJSON()));
this._customSetting.set(json);
this.dispatchEventToListeners(Events.CustomDevicesUpdated);
}
saveStandardDevices() {
/** @type {!Array<?>} */
const json = [];
this._standard.forEach(device => json.push(device._toJSON()));
this._standardSetting.set(json);
this.dispatchEventToListeners(Events.StandardDevicesUpdated);
}
/**
* @param {!Set.<!EmulatedDevice>} from
* @param {!Set.<!EmulatedDevice>} to
*/
_copyShowValues(from, to) {
const fromDeviceById = new Map();
for (const device of from) {
fromDeviceById.set(device.title, device);
}
for (const toDevice of to) {
const fromDevice = fromDeviceById.get(toDevice.title);
if (fromDevice) {
toDevice.copyShowFrom(fromDevice);
}
}
}
}
/** @enum {symbol} */
export const Events = {
CustomDevicesUpdated: Symbol('CustomDevicesUpdated'),
StandardDevicesUpdated: Symbol('StandardDevicesUpdated')
};
/** @typedef {!{title: string, orientation: string, insets: !UI.Geometry.Insets, image: ?string}} */
// @ts-ignore typedef
export let Mode;
/** @typedef {!{width: number, height: number, outlineInsets: ?UI.Geometry.Insets, outlineImage: ?string, hinge: ?SDK.OverlayModel.Hinge}} */
// @ts-ignore typedef
export let Orientation;
/** @typedef {!{title: string, orientation: string, image: (string|undefined), insets: {left: number, right: number, top: number, bottom: number}}} */
// @ts-ignore typedef
export let JSONMode; // eslint-disable-line no-unused-vars
const emulatedDevices = [
{
'show-by-default': false,
'title': 'iPhone 4',
'screen': {
'horizontal': {'width': 480, 'height': 320},
'device-pixel-ratio': 2,
'vertical': {'width': 320, 'height': 480}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53',
'type': 'phone'
},
{
'order': 30,
'show-by-default': true,
'title': 'iPhone 5/SE',
'screen': {
'horizontal': {
'outline': {
'image': '@url(optimized/iPhone5-landscape.avif)',
'insets': {'left': 115, 'top': 25, 'right': 115, 'bottom': 28}
},
'width': 568,
'height': 320
},
'device-pixel-ratio': 2,
'vertical': {
'outline': {
'image': '@url(optimized/iPhone5-portrait.avif)',
'insets': {'left': 29, 'top': 105, 'right': 25, 'bottom': 111}
},
'width': 320,
'height': 568
}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
'type': 'phone'
},
{
'order': 31,
'show-by-default': true,
'title': 'iPhone 6/7/8',
'screen': {
'horizontal': {
'outline': {
'image': '@url(optimized/iPhone6-landscape.avif)',
'insets': {'left': 106, 'top': 28, 'right': 106, 'bottom': 28}
},
'width': 667,
'height': 375
},
'device-pixel-ratio': 2,
'vertical': {
'outline': {
'image': '@url(optimized/iPhone6-portrait.avif)',
'insets': {'left': 28, 'top': 105, 'right': 28, 'bottom': 105}
},
'width': 375,
'height': 667
}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1',
'type': 'phone'
},
{
'order': 32,
'show-by-default': true,
'title': 'iPhone 6/7/8 Plus',
'screen': {
'horizontal': {
'outline': {
'image': '@url(optimized/iPhone6Plus-landscape.avif)',
'insets': {'left': 109, 'top': 29, 'right': 109, 'bottom': 27}
},
'width': 736,
'height': 414
},
'device-pixel-ratio': 3,
'vertical': {
'outline': {
'image': '@url(optimized/iPhone6Plus-portrait.avif)',
'insets': {'left': 26, 'top': 107, 'right': 30, 'bottom': 111}
},
'width': 414,
'height': 736
}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1',
'type': 'phone'
},
{
'order': 33,
'show-by-default': true,
'title': 'iPhone X',
'screen': {
'horizontal': {'width': 812, 'height': 375},
'device-pixel-ratio': 3,
'vertical': {'width': 375, 'height': 812}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1',
'type': 'phone'
},
{
'show-by-default': false,
'title': 'BlackBerry Z30',
'screen': {
'horizontal': {'width': 640, 'height': 360},
'device-pixel-ratio': 2,
'vertical': {'width': 360, 'height': 640}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+',
'type': 'phone'
},
{
'show-by-default': false,
'title': 'Nexus 4',
'screen': {
'horizontal': {'width': 640, 'height': 384},
'device-pixel-ratio': 2,
'vertical': {'width': 384, 'height': 640}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
'user-agent-metadata':
{'platform': 'Android', 'platformVersion': '4.4.2', 'architecture': '', 'model': 'Nexus 4', 'mobile': true},
'type': 'phone'
},
{
'title': 'Nexus 5',
'type': 'phone',
'user-agent':
'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
'user-agent-metadata':
{'platform': 'Android', 'platformVersion': '6.0', 'architecture': '', 'model': 'Nexus 5', 'mobile': true},
'capabilities': ['touch', 'mobile'],
'show-by-default': false,
'screen': {
'device-pixel-ratio': 3,
'vertical': {'width': 360, 'height': 640},
'horizontal': {'width': 640, 'height': 360}
},
'modes': [
{
'title': 'default',
'orientation': 'vertical',
'insets': {'left': 0, 'top': 25, 'right': 0, 'bottom': 48},
'image':
'@url(optimized/google-nexus-5-vertical-default-1x.avif) 1x, @url(optimized/google-nexus-5-vertical-default-2x.avif) 2x'
},
{
'title': 'navigation bar',
'orientation': 'vertical',
'insets': {'left': 0, 'top': 80, 'right': 0, 'bottom': 48},
'image':
'@url(optimized/google-nexus-5-vertical-navigation-1x.avif) 1x, @url(optimized/google-nexus-5-vertical-navigation-2x.avif) 2x'
},
{
'title': 'keyboard',
'orientation': 'vertical',
'insets': {'left': 0, 'top': 80, 'right': 0, 'bottom': 312},
'image':
'@url(optimized/google-nexus-5-vertical-keyboard-1x.avif) 1x, @url(optimized/google-nexus-5-vertical-keyboard-2x.avif) 2x'
},
{
'title': 'default',
'orientation': 'horizontal',
'insets': {'left': 0, 'top': 25, 'right': 42, 'bottom': 0},
'image':
'@url(optimized/google-nexus-5-horizontal-default-1x.avif) 1x, @url(optimized/google-nexus-5-horizontal-default-2x.avif) 2x'
},
{
'title': 'navigation bar',
'orientation': 'horizontal',
'insets': {'left': 0, 'top': 80, 'right': 42, 'bottom': 0},
'image':
'@url(optimized/google-nexus-5-horizontal-navigation-1x.avif) 1x, @url(optimized/google-nexus-5-horizontal-navigation-2x.avif) 2x'
},
{
'title': 'keyboard',
'orientation': 'horizontal',
'insets': {'left': 0, 'top': 80, 'right': 42, 'bottom': 202},
'image':
'@url(optimized/google-nexus-5-horizontal-keyboard-1x.avif) 1x, @url(optimized/google-nexus-5-horizontal-keyboard-2x.avif) 2x'
}
]
},
{
'title': 'Nexus 5X',
'type': 'phone',
'user-agent':
'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
'user-agent-metadata':
{'platform': 'Android', 'platformVersion': '8.0.0', 'architecture': '', 'model': 'Nexus 5X', 'mobile': true},
'capabilities': ['touch', 'mobile'],
'show-by-default': false,
'screen': {
'device-pixel-ratio': 2.625,
'vertical': {
'outline': {
'image': '@url(optimized/Nexus5X-portrait.avif)',
'insets': {'left': 18, 'top': 88, 'right': 22, 'bottom': 98}
},
'width': 412,
'height': 732
},
'horizontal': {
'outline': {
'image': '@url(optimized/Nexus5X-landscape.avif)',
'insets': {'left': 88, 'top': 21, 'right': 98, 'bottom': 19}
},
'width': 732,
'height': 412
}
},
'modes': [
{
'title': 'default',
'orientation': 'vertical',
'insets': {'left': 0, 'top': 24, 'right': 0, 'bottom': 48},
'image':
'@url(optimized/google-nexus-5x-vertical-default-1x.avif) 1x, @url(optimized/google-nexus-5x-vertical-default-2x.avif) 2x'
},
{
'title': 'navigation bar',
'orientation': 'vertical',
'insets': {'left': 0, 'top': 80, 'right': 0, 'bottom': 48},
'image':
'@url(optimized/google-nexus-5x-vertical-navigation-1x.avif) 1x, @url(optimized/google-nexus-5x-vertical-navigation-2x.avif) 2x'
},
{
'title': 'keyboard',
'orientation': 'vertical',
'insets': {'left': 0, 'top': 80, 'right': 0, 'bottom': 342},
'image':
'@url(optimized/google-nexus-5x-vertical-keyboard-1x.avif) 1x, @url(optimized/google-nexus-5x-vertical-keyboard-2x.avif) 2x'
},
{
'title': 'default',
'orientation': 'horizontal',
'insets': {'left': 0, 'top': 24, 'right': 48, 'bottom': 0},
'image':
'@url(optimized/google-nexus-5x-horizontal-default-1x.avif) 1x, @url(optimized/google-nexus-5x-horizontal-default-2x.avif) 2x'
},
{
'title': 'navigation bar',
'orientation': 'horizontal',
'insets': {'left': 0, 'top': 80, 'right': 48, 'bottom': 0},
'image':
'@url(optimized/google-nexus-5x-horizontal-navigation-1x.avif) 1x, @url(optimized/google-nexus-5x-horizontal-navigation-2x.avif) 2x'
},
{
'title': 'keyboard',
'orientation': 'horizontal',
'insets': {'left': 0, 'top': 80, 'right': 48, 'bottom': 222},
'image':
'@url(optimized/google-nexus-5x-horizontal-keyboard-1x.avif) 1x, @url(optimized/google-nexus-5x-horizontal-keyboard-2x.avif) 2x'
}
]
},
{
'show-by-default': false,
'title': 'Nexus 6',
'screen': {
'horizontal': {'width': 732, 'height': 412},
'device-pixel-ratio': 3.5,
'vertical': {'width': 412, 'height': 732}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
'user-agent-metadata':
{'platform': 'Android', 'platformVersion': '7.1.1', 'architecture': '', 'model': 'Nexus 6', 'mobile': true},
'type': 'phone'
},
{
'show-by-default': false,
'title': 'Nexus 6P',
'screen': {
'horizontal': {
'outline': {
'image': '@url(optimized/Nexus6P-landscape.avif)',
'insets': {'left': 94, 'top': 17, 'right': 88, 'bottom': 17}
},
'width': 732,
'height': 412
},
'device-pixel-ratio': 3.5,
'vertical': {
'outline': {
'image': '@url(optimized/Nexus6P-portrait.avif)',
'insets': {'left': 16, 'top': 94, 'right': 16, 'bottom': 88}
},
'width': 412,
'height': 732
}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
'user-agent-metadata':
{'platform': 'Android', 'platformVersion': '8.0.0', 'architecture': '', 'model': 'Nexus 6P', 'mobile': true},
'type': 'phone'
},
{
'order': 20,
'show-by-default': true,
'title': 'Pixel 2',
'screen': {
'horizontal': {'width': 731, 'height': 411},
'device-pixel-ratio': 2.625,
'vertical': {'width': 411, 'height': 731}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
'user-agent-metadata':
{'platform': 'Android', 'platformVersion': '8.0', 'architecture': '', 'model': 'Pixel 2', 'mobile': true},
'type': 'phone'
},
{
'order': 21,
'show-by-default': true,
'title': 'Pixel 2 XL',
'screen': {
'horizontal': {'width': 823, 'height': 411},
'device-pixel-ratio': 3.5,
'vertical': {'width': 411, 'height': 823}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
'user-agent-metadata':
{'platform': 'Android', 'platformVersion': '8.0.0', 'architecture': '', 'model': 'Pixel 2 XL', 'mobile': true},
'type': 'phone'
},
{
'show-by-default': false,
'title': 'LG Optimus L70',
'screen': {
'horizontal': {'width': 640, 'height': 384},
'device-pixel-ratio': 1.25,
'vertical': {'width': 384, 'height': 640}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/%s Mobile Safari/537.36',
'user-agent-metadata':
{'platform': 'Android', 'platformVersion': '4.4.2', 'architecture': '', 'model': 'LGMS323', 'mobile': true},
'type': 'phone'
},
{
'show-by-default': false,
'title': 'Nokia N9',
'screen': {
'horizontal': {'width': 854, 'height': 480},
'device-pixel-ratio': 1,
'vertical': {'width': 480, 'height': 854}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13',
'type': 'phone'
},
{
'show-by-default': false,
'title': 'Nokia Lumia 520',
'screen': {
'horizontal': {'width': 533, 'height': 320},
'device-pixel-ratio': 1.5,
'vertical': {'width': 320, 'height': 533}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)',
'type': 'phone'
},
{
'show-by-default': false,
'title': 'Microsoft Lumia 550',
'screen': {
'horizontal': {'width': 640, 'height': 360},
'device-pixel-ratio': 2,
'vertical': {'width': 640, 'height': 360}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
'type': 'phone'
},
{
'show-by-default': false,
'title': 'Microsoft Lumia 950',
'screen': {
'horizontal': {'width': 640, 'height': 360},
'device-pixel-ratio': 4,
'vertical': {'width': 360, 'height': 640}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
'type': 'phone'
},
{
'show-by-default': false,
'title': 'Galaxy S III',
'screen': {
'horizontal': {'width': 640, 'height': 360},
'device-pixel-ratio': 2,
'vertical': {'width': 360, 'height': 640}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
'user-agent-metadata':
{'platform': 'Android', 'platformVersion': '4.0', 'architecture': '', 'model': 'GT-I9300', 'mobile': true},
'type': 'phone'
},
{
'order': 10,
'show-by-default': true,
'title': 'Galaxy S5',
'screen': {
'horizontal': {'width': 640, 'height': 360},
'device-pixel-ratio': 3,
'vertical': {'width': 360, 'height': 640}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
'user-agent-metadata':
{'platform': 'Android', 'platformVersion': '5.0', 'architecture': '', 'model': 'SM-G900P', 'mobile': true},
'type': 'phone'
},
{
'order': 1,
'show-by-default': false,
'title': 'JioPhone 2',
'screen': {
'horizontal': {'width': 320, 'height': 240},
'device-pixel-ratio': 1,
'vertical': {'width': 240, 'height': 320}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (Mobile; LYF/F300B/LYF-F300B-001-01-15-130718-i;Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5',
'type': 'phone'
},
{
'show-by-default': false,
'title': 'Kindle Fire HDX',
'screen': {
'horizontal': {'width': 1280, 'height': 800},
'device-pixel-ratio': 2,
'vertical': {'width': 800, 'height': 1280}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true',
'type': 'tablet'
},
{
'show-by-default': false,
'title': 'iPad Mini',
'screen': {
'horizontal': {'width': 1024, 'height': 768},
'device-pixel-ratio': 2,
'vertical': {'width': 768, 'height': 1024}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
'type': 'tablet'
},
{
'order': 40,
'show-by-default': true,
'title': 'iPad',
'screen': {
'horizontal': {
'outline': {
'image': '@url(optimized/iPad-landscape.avif)',
'insets': {'left': 112, 'top': 56, 'right': 116, 'bottom': 52}
},
'width': 1024,
'height': 768
},
'device-pixel-ratio': 2,
'vertical': {
'outline': {
'image': '@url(optimized/iPad-portrait.avif)',
'insets': {'left': 52, 'top': 114, 'right': 55, 'bottom': 114}
},
'width': 768,
'height': 1024
}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
'type': 'tablet'
},
{
'order': 41,
'show-by-default': true,
'title': 'iPad Pro',
'screen': {
'horizontal': {'width': 1366, 'height': 1024},
'device-pixel-ratio': 2,
'vertical': {'width': 1024, 'height': 1366}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
'type': 'tablet'
},
{
'show-by-default': false,
'title': 'Blackberry PlayBook',
'screen': {
'horizontal': {'width': 1024, 'height': 600},
'device-pixel-ratio': 1,
'vertical': {'width': 600, 'height': 1024}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+',
'type': 'tablet'
},
{
'show-by-default': false,
'title': 'Nexus 10',
'screen': {
'horizontal': {'width': 1280, 'height': 800},
'device-pixel-ratio': 2,
'vertical': {'width': 800, 'height': 1280}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36',
'user-agent-metadata':
{'platform': 'Android', 'platformVersion': '6.0.1', 'architecture': '', 'model': 'Nexus 10', 'mobile': false},
'type': 'tablet'
},
{
'show-by-default': false,
'title': 'Nexus 7',
'screen': {
'horizontal': {'width': 960, 'height': 600},
'device-pixel-ratio': 2,
'vertical': {'width': 600, 'height': 960}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36',
'user-agent-metadata':
{'platform': 'Android', 'platformVersion': '6.0.1', 'architecture': '', 'model': 'Nexus 7', 'mobile': false},
'type': 'tablet'
},
{
'show-by-default': false,
'title': 'Galaxy Note 3',
'screen': {
'horizontal': {'width': 640, 'height': 360},
'device-pixel-ratio': 3,
'vertical': {'width': 360, 'height': 640}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
'user-agent-metadata':
{'platform': 'Android', 'platformVersion': '4.3', 'architecture': '', 'model': 'SM-N900T', 'mobile': true},
'type': 'phone'
},
{
'show-by-default': false,
'title': 'Galaxy Note II',
'screen': {
'horizontal': {'width': 640, 'height': 360},
'device-pixel-ratio': 2,
'vertical': {'width': 360, 'height': 640}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
'user-agent-metadata':
{'platform': 'Android', 'platformVersion': '4.1', 'architecture': '', 'model': 'GT-N7100', 'mobile': true},
'type': 'phone'
},
{
'show-by-default': false,
'title': i18nLazyString(UIStrings.laptopWithTouch),
'screen': {
'horizontal': {'width': 1280, 'height': 950},
'device-pixel-ratio': 1,
'vertical': {'width': 950, 'height': 1280}
},
'capabilities': ['touch'],
'user-agent': '',
'type': 'notebook',
'modes': [{'title': 'default', 'orientation': 'horizontal'}]
},
{
'show-by-default': false,
'title': i18nLazyString(UIStrings.laptopWithHiDPIScreen),
'screen': {
'horizontal': {'width': 1440, 'height': 900},
'device-pixel-ratio': 2,
'vertical': {'width': 900, 'height': 1440}
},
'capabilities': [],
'user-agent': '',
'type': 'notebook',
'modes': [{'title': 'default', 'orientation': 'horizontal'}]
},
{
'show-by-default': false,
'title': i18nLazyString(UIStrings.laptopWithMDPIScreen),
'screen': {
'horizontal': {'width': 1280, 'height': 800},
'device-pixel-ratio': 1,
'vertical': {'width': 800, 'height': 1280}
},
'capabilities': [],
'user-agent': '',
'type': 'notebook',
'modes': [{'title': 'default', 'orientation': 'horizontal'}]
},
{
'show-by-default': true,
'title': 'Moto G4',
'screen': {
'horizontal': {
'outline': {
'image': '@url(optimized/MotoG4-landscape.avif)',
'insets': {'left': 91, 'top': 30, 'right': 74, 'bottom': 30}
},
'width': 640,
'height': 360
},
'device-pixel-ratio': 3,
'vertical': {
'outline': {
'image': '@url(optimized/MotoG4-portrait.avif)',
'insets': {'left': 30, 'top': 91, 'right': 30, 'bottom': 74}
},
'width': 360,
'height': 640
}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (Linux; Android 6.0.1; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
'user-agent-metadata':
{'platform': 'Android', 'platformVersion': '6.0.1', 'architecture': '', 'model': 'Moto G (4)', 'mobile': true},
'type': 'phone'
},
{
'order': 45,
'show-by-default': true,
'dual-screen': true,
'title': 'Surface Duo',
'screen': {
'horizontal': {'width': 720, 'height': 540},
'device-pixel-ratio': 2.5,
'vertical': {'width': 540, 'height': 720},
'vertical-spanned': {
'width': 1114,
'height': 720,
'hinge': {'width': 34, 'height': 720, 'x': 540, 'y': 0, 'contentColor': {'r': 38, 'g': 38, 'b': 38, 'a': 1}}
},
'horizontal-spanned': {
'width': 720,
'height': 1114,
'hinge': {'width': 720, 'height': 34, 'x': 0, 'y': 540, 'contentColor': {'r': 38, 'g': 38, 'b': 38, 'a': 1}}
}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
'type': 'phone',
'modes': [
{'title': 'default', 'orientation': 'vertical', 'insets': {'left': 0, 'top': 0, 'right': 0, 'bottom': 0}},
{'title': 'default', 'orientation': 'horizontal', 'insets': {'left': 0, 'top': 0, 'right': 0, 'bottom': 0}},
{'title': 'spanned', 'orientation': 'vertical-spanned', 'insets': {'left': 0, 'top': 0, 'right': 0, 'bottom': 0}},
{
'title': 'spanned',
'orientation': 'horizontal-spanned',
'insets': {'left': 0, 'top': 0, 'right': 0, 'bottom': 0}
}
]
},
{
'order': 46,
'show-by-default': true,
'dual-screen': true,
'title': 'Galaxy Fold',
'screen': {
'horizontal': {'width': 653, 'height': 280},
'device-pixel-ratio': 3,
'vertical': {'width': 280, 'height': 653},
'vertical-spanned': {'width': 717, 'height': 512},
'horizontal-spanned': {'width': 512, 'height': 717}
},
'capabilities': ['touch', 'mobile'],
'user-agent':
'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
'type': 'phone',
'modes': [
{'title': 'default', 'orientation': 'vertical', 'insets': {'left': 0, 'top': 0, 'right': 0, 'bottom': 0}},
{'title': 'default', 'orientation': 'horizontal', 'insets': {'left': 0, 'top': 0, 'right': 0, 'bottom': 0}},
{'title': 'spanned', 'orientation': 'vertical-spanned', 'insets': {'left': 0, 'top': 0, 'right': 0, 'bottom': 0}},
{
'title': 'spanned',
'orientation': 'horizontal-spanned',
'insets': {'left': 0, 'top': 0, 'right': 0, 'bottom': 0}
}
]
}
];