summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/devices.js
blob: 6bb3135e613e4082844501286f315db93bf2e0a4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
const L10N = new LocalizationHelper(
  "devtools/client/locales/device.properties"
);

const { RemoteSettings } = ChromeUtils.importESModule(
  "resource://services-settings/remote-settings.sys.mjs"
);

loader.lazyRequireGetter(
  this,
  "asyncStorage",
  "resource://devtools/shared/async-storage.js"
);

const LOCAL_DEVICES = "devtools.devices.local";

/* This is a catalog of common web-enabled devices and their properties,
 * intended for (mobile) device emulation.
 *
 * The properties of a device are:
 * - name: brand and model(s).
 * - width: viewport width.
 * - height: viewport height.
 * - pixelRatio: ratio from viewport to physical screen pixels.
 * - userAgent: UA string of the device's browser.
 * - touch: whether it has a touch screen.
 * - os: default OS, such as "ios", "fxos", "android".
 *
 * The device types are:
 *   ["phones", "tablets", "laptops", "televisions", "consoles", "watches"].
 *
 * To propose new devices for the shared catalog, see
 * https://firefox-source-docs.mozilla.org/devtools/responsive/devices.html#adding-and-removing-devices.
 *
 * You can easily add more devices to this catalog from your own code (e.g. an
 * addon) like so:
 *
 *   var myPhone = { name: "My Phone", ... };
 *   require("devtools/client/shared/devices").addDevice(myPhone, "phones");
 */

// Local devices catalog that addons can add to.
let localDevices;
let localDevicesLoaded = false;

/**
 * Load local devices from storage.
 */
async function loadLocalDevices() {
  if (localDevicesLoaded) {
    return;
  }
  let devicesJSON = await asyncStorage.getItem(LOCAL_DEVICES);
  if (!devicesJSON) {
    devicesJSON = "{}";
  }
  localDevices = JSON.parse(devicesJSON);
  localDevicesLoaded = true;
}

/**
 * Add a device to the local catalog.
 * Returns `true` if the device is added, `false` otherwise.
 */
async function addDevice(device, type = "phones") {
  await loadLocalDevices();
  let list = localDevices[type];
  if (!list) {
    list = localDevices[type] = [];
  }

  // Ensure the new device is has a unique name
  const exists = list.some(entry => entry.name == device.name);
  if (exists) {
    return false;
  }

  list.push(Object.assign({}, device));
  await asyncStorage.setItem(LOCAL_DEVICES, JSON.stringify(localDevices));

  return true;
}

/**
 * Edit a device from the local catalog.
 * Returns `true` if the device is edited, `false` otherwise.
 */
async function editDevice(oldDevice, newDevice, type = "phones") {
  await loadLocalDevices();
  const list = localDevices[type];
  if (!list) {
    return false;
  }

  const index = list.findIndex(entry => entry.name == oldDevice.name);
  if (index == -1) {
    return false;
  }

  // Replace old device info with new one
  list.splice(index, 1, newDevice);
  await asyncStorage.setItem(LOCAL_DEVICES, JSON.stringify(localDevices));

  return true;
}

/**
 * Remove a device from the local catalog.
 * Returns `true` if the device is removed, `false` otherwise.
 */
async function removeDevice(device, type = "phones") {
  await loadLocalDevices();
  const list = localDevices[type];
  if (!list) {
    return false;
  }

  const index = list.findIndex(entry => entry.name == device.name);
  if (index == -1) {
    return false;
  }

  list.splice(index, 1);
  await asyncStorage.setItem(LOCAL_DEVICES, JSON.stringify(localDevices));

  return true;
}

/**
 * Remove all local devices.  Useful to clear everything when testing.
 */
async function removeLocalDevices() {
  await asyncStorage.removeItem(LOCAL_DEVICES);
  localDevices = {};
}

/**
 * Get the complete devices catalog.
 */
async function getDevices() {
  const records = await RemoteSettings("devtools-devices").get();
  const devicesByType = new Map();
  for (const record of records) {
    const { type } = record;
    if (!devicesByType.has(type)) {
      devicesByType.set(type, []);
    }
    devicesByType.get(type).push(record);
  }

  await loadLocalDevices();
  for (const type in localDevices) {
    if (!devicesByType.has(type)) {
      devicesByType.set(type, []);
    }
    devicesByType.get(type).push(...localDevices[type]);
  }
  return devicesByType;
}

/**
 * Get the localized string for a device type.
 */
function getDeviceString(deviceType) {
  return L10N.getStr("device." + deviceType);
}

module.exports = {
  addDevice,
  editDevice,
  removeDevice,
  removeLocalDevices,
  getDevices,
  getDeviceString,
};