Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
9
gaia/distros/spark/apps/customizer-launcher/js/app.js
Executable file
9
gaia/distros/spark/apps/customizer-launcher/js/app.js
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
define(["exports", "js/globals", "js/controller/main_controller"], function (exports, _jsGlobals, _jsControllerMainController) {
|
||||
"use strict";
|
||||
|
||||
var MainController = _jsControllerMainController["default"];
|
||||
|
||||
|
||||
var mainController = new MainController();
|
||||
mainController.main();
|
||||
});
|
||||
88
gaia/distros/spark/apps/customizer-launcher/js/controller/list_controller.js
Executable file
88
gaia/distros/spark/apps/customizer-launcher/js/controller/list_controller.js
Executable file
|
|
@ -0,0 +1,88 @@
|
|||
define(["exports", "components/fxos-mvc/dist/mvc", "js/model/list_model", "js/view/list_view", "js/lib/web_server"], function (exports, _componentsFxosMvcDistMvc, _jsModelListModel, _jsViewListView, _jsLibWebServer) {
|
||||
"use strict";
|
||||
|
||||
var _extends = function (child, parent) {
|
||||
child.prototype = Object.create(parent.prototype, {
|
||||
constructor: {
|
||||
value: child,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
configurable: true
|
||||
}
|
||||
});
|
||||
child.__proto__ = parent;
|
||||
};
|
||||
|
||||
var Controller = _componentsFxosMvcDistMvc.Controller;
|
||||
var ListModel = _jsModelListModel["default"];
|
||||
var ListView = _jsViewListView["default"];
|
||||
var WebServer = _jsLibWebServer["default"];
|
||||
var ListController = (function (Controller) {
|
||||
var ListController = function ListController() {
|
||||
this.model = new ListModel();
|
||||
this.listView = new ListView();
|
||||
this.webServer = new WebServer();
|
||||
};
|
||||
|
||||
_extends(ListController, Controller);
|
||||
|
||||
ListController.prototype.main = function () {
|
||||
var _this = this;
|
||||
this.model.getAllApps().then(function (allApps) {
|
||||
_this.enableCustomizerAddOn(allApps).then(function () {
|
||||
return _this.createList(allApps);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
ListController.prototype.enableCustomizerAddOn = function (allApps) {
|
||||
var _this2 = this;
|
||||
return new Promise(function (resolve, reject) {
|
||||
_this2.model.getCustomizerAddOn(allApps).then(function (addon) {
|
||||
if (addon && !addon.enabled) {
|
||||
navigator.mozApps.mgmt.setEnabled(addon, true);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
ListController.prototype.createList = function (allApps) {
|
||||
var _this3 = this;
|
||||
this.listView.render();
|
||||
document.body.appendChild(this.listView.el);
|
||||
|
||||
this.model.getAppList(allApps).then(function (appsList) {
|
||||
_this3.listView.update(appsList);
|
||||
_this3.listView.setOpenHandler(_this3.handleOpen.bind(_this3));
|
||||
});
|
||||
};
|
||||
|
||||
ListController.prototype.handleOpen = function (data) {
|
||||
var _this4 = this;
|
||||
if (data.app) {
|
||||
this.webServer.startServer().then(function (result) {
|
||||
if (result) {
|
||||
_this4.launchApp(data);
|
||||
_this4.webServer.setData(data.manifestURL);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new Error("Could not open app: " + data);
|
||||
}
|
||||
};
|
||||
|
||||
ListController.prototype.launchApp = function (data) {
|
||||
// Use entry_point to launch app, if entry_point exists
|
||||
if (data.entry_point) {
|
||||
data.app.launch(data.entry_point);
|
||||
} else {
|
||||
data.app.launch();
|
||||
}
|
||||
};
|
||||
|
||||
return ListController;
|
||||
})(Controller);
|
||||
|
||||
exports["default"] = ListController;
|
||||
});
|
||||
46
gaia/distros/spark/apps/customizer-launcher/js/controller/main_controller.js
Executable file
46
gaia/distros/spark/apps/customizer-launcher/js/controller/main_controller.js
Executable file
|
|
@ -0,0 +1,46 @@
|
|||
define(["exports", "components/fxos-mvc/dist/mvc", "gaia-component", "js/view/main_view", "js/controller/list_controller", "js/lib/helpers"], function (exports, _componentsFxosMvcDistMvc, _gaiaComponent, _jsViewMainView, _jsControllerListController, _jsLibHelpers) {
|
||||
"use strict";
|
||||
|
||||
var _extends = function (child, parent) {
|
||||
child.prototype = Object.create(parent.prototype, {
|
||||
constructor: {
|
||||
value: child,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
configurable: true
|
||||
}
|
||||
});
|
||||
child.__proto__ = parent;
|
||||
};
|
||||
|
||||
var Controller = _componentsFxosMvcDistMvc.Controller;
|
||||
var MainView = _jsViewMainView["default"];
|
||||
var ListController = _jsControllerListController["default"];
|
||||
var AchievementsHelper = _jsLibHelpers.AchievementsHelper;
|
||||
var MainController = (function (Controller) {
|
||||
var MainController = function MainController() {
|
||||
this.achievementsHelper = new AchievementsHelper();
|
||||
this.view = new MainView({ el: document.body });
|
||||
this.listController = new ListController();
|
||||
Controller.call(this);
|
||||
};
|
||||
|
||||
_extends(MainController, Controller);
|
||||
|
||||
MainController.prototype.main = function () {
|
||||
this.view.render();
|
||||
this.dialog = document.querySelector("fxos-dev-mode-dialog");
|
||||
|
||||
// fxos-dev-mode-dialog checks and emit event 'enabled' when
|
||||
// device dev mode perf is set to true. Continue to load
|
||||
// list of apps inside dialog enabled event handler.
|
||||
this.dialog.addEventListener("enabled", function () {
|
||||
this.listController.main();
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
return MainController;
|
||||
})(Controller);
|
||||
|
||||
exports["default"] = MainController;
|
||||
});
|
||||
12
gaia/distros/spark/apps/customizer-launcher/js/globals.js
Executable file
12
gaia/distros/spark/apps/customizer-launcher/js/globals.js
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
define(["exports"], function (exports) {
|
||||
"use strict";
|
||||
|
||||
window.COMPONENTS_BASE_URL = "./components/";
|
||||
window.DEFAULT_ICON_URL = "./img/default_icon.png";
|
||||
require.config({
|
||||
baseUrl: "/",
|
||||
paths: {
|
||||
"gaia-component": "components/gaia-component/gaia-component"
|
||||
}
|
||||
});
|
||||
});
|
||||
1416
gaia/distros/spark/apps/customizer-launcher/js/initapp.js
Executable file
1416
gaia/distros/spark/apps/customizer-launcher/js/initapp.js
Executable file
File diff suppressed because it is too large
Load diff
96
gaia/distros/spark/apps/customizer-launcher/js/lib/helpers.js
Executable file
96
gaia/distros/spark/apps/customizer-launcher/js/lib/helpers.js
Executable file
|
|
@ -0,0 +1,96 @@
|
|||
define(["exports", "components/fxos-achievements-service/dist/achievements-service"], function (exports, _componentsFxosAchievementsServiceDistAchievementsService) {
|
||||
"use strict";
|
||||
|
||||
var AchievementsService = _componentsFxosAchievementsServiceDistAchievementsService["default"];
|
||||
var IconHelper = (function () {
|
||||
var IconHelper = function IconHelper() {};
|
||||
|
||||
IconHelper.setImage = function (imageElement, imagePath) {
|
||||
imageElement.src = imagePath || window.DEFAULT_ICON_URL;
|
||||
imageElement.onerror = function (e) {
|
||||
console.warn("Warning, failed to load icon url", imageElement.src, e);
|
||||
imageElement.src = window.DEFAULT_ICON_URL;
|
||||
};
|
||||
};
|
||||
|
||||
IconHelper.getIconURL = function (app, icons) {
|
||||
if (!icons || !Object.keys(icons).length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// The preferred size is 30 by the default. If we use HDPI device, we may
|
||||
// use the image larger than 30 * 1.5 = 45 pixels.
|
||||
var preferredIconSize = 30 * (window.devicePixelRatio || 1);
|
||||
var preferredSize = Number.MAX_VALUE;
|
||||
var max = 0;
|
||||
|
||||
for (var size in icons) {
|
||||
size = parseInt(size, 10);
|
||||
if (size > max) {
|
||||
max = size;
|
||||
}
|
||||
|
||||
if (size >= preferredIconSize && size < preferredSize) {
|
||||
preferredSize = size;
|
||||
}
|
||||
}
|
||||
// If there is an icon matching the preferred size, we return the result,
|
||||
// if there isn't, we will return the maximum available size.
|
||||
if (preferredSize === Number.MAX_VALUE) {
|
||||
preferredSize = max;
|
||||
}
|
||||
|
||||
var url = icons[preferredSize];
|
||||
|
||||
if (url) {
|
||||
return !(/^(http|https|data):/.test(url)) ? app.origin + url : url;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
return IconHelper;
|
||||
})();
|
||||
|
||||
exports.IconHelper = IconHelper;
|
||||
var AppsHelper = (function () {
|
||||
var AppsHelper = function AppsHelper() {};
|
||||
|
||||
AppsHelper.getAllApps = function () {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var mgmt = navigator.mozApps.mgmt;
|
||||
if (!mgmt) {
|
||||
reject(new Error("Cannot fetch apps, no permissions"));
|
||||
}
|
||||
|
||||
var req = mgmt.getAll();
|
||||
req.onsuccess = function () {
|
||||
resolve(req.result);
|
||||
};
|
||||
req.onerror = function (e) {
|
||||
reject(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return AppsHelper;
|
||||
})();
|
||||
|
||||
exports.AppsHelper = AppsHelper;
|
||||
var AchievementsHelper = (function () {
|
||||
var AchievementsHelper = function AchievementsHelper() {
|
||||
// Create an achievements service
|
||||
this.achievementsService = new AchievementsService();
|
||||
|
||||
window.addEventListener("achievement-rewarded", this);
|
||||
};
|
||||
|
||||
AchievementsHelper.prototype.handleEvent = function (aEvent) {
|
||||
this.achievementsService.reward(aEvent.detail);
|
||||
};
|
||||
|
||||
return AchievementsHelper;
|
||||
})();
|
||||
|
||||
exports.AchievementsHelper = AchievementsHelper;
|
||||
});
|
||||
71
gaia/distros/spark/apps/customizer-launcher/js/lib/web_server.js
Executable file
71
gaia/distros/spark/apps/customizer-launcher/js/lib/web_server.js
Executable file
|
|
@ -0,0 +1,71 @@
|
|||
define(["exports", "components/fxos-web-server/dist/fxos-web-server"], function (exports, _componentsFxosWebServerDistFxosWebServer) {
|
||||
"use strict";
|
||||
|
||||
var WebServer = (function () {
|
||||
var WebServer = function WebServer() {
|
||||
var _this = this;
|
||||
this.lastLaunchedManifestURL = null;
|
||||
if (!this.httpServer) {
|
||||
this.httpServer = new HTTPServer(3215);
|
||||
}
|
||||
|
||||
this.httpServer.addEventListener("request", function (evt) {
|
||||
var request = evt.request;
|
||||
var response = evt.response;
|
||||
response.headers["Access-Control-Allow-Origin"] = "*";
|
||||
response.headers["Cache-Control"] = "no-cache";
|
||||
|
||||
if (request.path === "/request") {
|
||||
response.send(_this.lastLaunchedManifestURL);
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.path === "/confirm") {
|
||||
var url = (request.params && request.params.url) || "";
|
||||
if (url && url === _this.lastLaunchedManifestURL) {
|
||||
response.send("STOP_SERVER_INVOKED");
|
||||
_this.stopServer();
|
||||
window.dispatchEvent(new CustomEvent("achievement-rewarded", {
|
||||
detail: {
|
||||
criteria: "achievements/customize-champ",
|
||||
evidence: "urn:fxos-customizer-launcher:customizer:launched",
|
||||
name: "Customize Champ",
|
||||
description: "Open Customizer in an app",
|
||||
image: "./img/customize-champ.png"
|
||||
}
|
||||
}));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Default response for all other requests
|
||||
response.send(null, 404);
|
||||
});
|
||||
};
|
||||
|
||||
WebServer.prototype.setData = function (url) {
|
||||
this.lastLaunchedManifestURL = url;
|
||||
};
|
||||
|
||||
WebServer.prototype.startServer = function () {
|
||||
var _this2 = this;
|
||||
return new Promise(function (resolve, reject) {
|
||||
_this2.httpServer.start();
|
||||
console.log("WebServer Started");
|
||||
resolve(true);
|
||||
});
|
||||
};
|
||||
|
||||
WebServer.prototype.stopServer = function () {
|
||||
if (!this.httpServer) {
|
||||
return;
|
||||
}
|
||||
this.httpServer.stop();
|
||||
this.lastLaunchedManifestURL = null;
|
||||
console.log("WebServer Stopped");
|
||||
};
|
||||
|
||||
return WebServer;
|
||||
})();
|
||||
|
||||
exports["default"] = WebServer;
|
||||
});
|
||||
109
gaia/distros/spark/apps/customizer-launcher/js/model/list_model.js
Executable file
109
gaia/distros/spark/apps/customizer-launcher/js/model/list_model.js
Executable file
|
|
@ -0,0 +1,109 @@
|
|||
define(["exports", "components/fxos-mvc/dist/mvc", "js/lib/helpers"], function (exports, _componentsFxosMvcDistMvc, _jsLibHelpers) {
|
||||
"use strict";
|
||||
|
||||
var _extends = function (child, parent) {
|
||||
child.prototype = Object.create(parent.prototype, {
|
||||
constructor: {
|
||||
value: child,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
configurable: true
|
||||
}
|
||||
});
|
||||
child.__proto__ = parent;
|
||||
};
|
||||
|
||||
var Model = _componentsFxosMvcDistMvc.Model;
|
||||
var AppsHelper = _jsLibHelpers.AppsHelper;
|
||||
var IconHelper = _jsLibHelpers.IconHelper;
|
||||
var ListModel = (function (Model) {
|
||||
var ListModel = function ListModel() {
|
||||
Model.apply(this, arguments);
|
||||
};
|
||||
|
||||
_extends(ListModel, Model);
|
||||
|
||||
ListModel.prototype.getAllApps = function () {
|
||||
return new Promise(function (resolve, reject) {
|
||||
AppsHelper.getAllApps().then(function (allApps) {
|
||||
resolve(allApps);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
ListModel.prototype.filterCustomizerAddon = function (apps) {
|
||||
return apps.filter(function (app) {
|
||||
return app.manifestURL == "app://customizer.gaiamobile.org/manifest.webapp";
|
||||
});
|
||||
};
|
||||
|
||||
ListModel.prototype.getCustomizerAddOn = function (allApps) {
|
||||
var _this = this;
|
||||
return new Promise(function (resolve, reject) {
|
||||
var addOnList = _this.filterCustomizerAddon(allApps);
|
||||
if (addOnList && addOnList.length == 1) {
|
||||
resolve(addOnList[0]);
|
||||
} else {
|
||||
reject(new Error("Cannot fetch customizer add on"));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ListModel.prototype.filterApps = function (apps) {
|
||||
var excludedApps = ["app://keyboard.gaiamobile.org/manifest.webapp", "app://customizer-launcher.gaiamobile.org/manifest.webapp"];
|
||||
|
||||
return apps.filter(function (app) {
|
||||
return app.manifest.role !== "addon" && app.manifest.role !== "theme" && app.manifest.role !== "system" && excludedApps.indexOf(app.manifestURL) === -1;
|
||||
});
|
||||
};
|
||||
|
||||
ListModel.prototype.fillAppDetails = function (app) {
|
||||
var detail = Object.create(null);
|
||||
detail.manifestURL = app.manifestURL;
|
||||
detail.name = app.manifest.name;
|
||||
detail.description = app.manifest.description;
|
||||
detail.icon = IconHelper.getIconURL(app, app.manifest.icons);
|
||||
detail.author = app.manifest.developer ? app.manifest.developer.name : "";
|
||||
detail.app = app;
|
||||
return detail;
|
||||
};
|
||||
|
||||
ListModel.prototype.fillAppDetailsFromEntryPoint = function (app, entry_point) {
|
||||
var detail = Object.create(null);
|
||||
detail.manifestURL = app.manifestURL;
|
||||
detail.description = app.manifest.description;
|
||||
detail.author = app.manifest.developer ? app.manifest.developer.name : "";
|
||||
detail.app = app;
|
||||
detail.name = app.manifest.entry_points[entry_point].name;
|
||||
detail.icon = IconHelper.getIconURL(app, app.manifest.entry_points[entry_point].icons);
|
||||
detail.entry_point = entry_point;
|
||||
return detail;
|
||||
};
|
||||
|
||||
ListModel.prototype.getAppList = function (allApps) {
|
||||
var _this2 = this;
|
||||
return new Promise(function (resolve, reject) {
|
||||
var installedApps = Object.create(null);
|
||||
var filterList = _this2.filterApps(allApps);
|
||||
filterList.forEach(function (app) {
|
||||
// Check for Communications app, only app that uses
|
||||
// multiple entry_points in manifest
|
||||
if (app.manifest.entry_points) {
|
||||
// Iterate manifest.entry_points to fill dialer and
|
||||
// contacts app details
|
||||
for (var entry_point in app.manifest.entry_points) {
|
||||
installedApps[app.manifestURL + "/" + entry_point] = _this2.fillAppDetailsFromEntryPoint(app, entry_point);
|
||||
}
|
||||
} else {
|
||||
installedApps[app.manifestURL] = _this2.fillAppDetails(app);
|
||||
}
|
||||
});
|
||||
resolve(installedApps);
|
||||
});
|
||||
};
|
||||
|
||||
return ListModel;
|
||||
})(Model);
|
||||
|
||||
exports["default"] = ListModel;
|
||||
});
|
||||
82
gaia/distros/spark/apps/customizer-launcher/js/view/list_view.js
Executable file
82
gaia/distros/spark/apps/customizer-launcher/js/view/list_view.js
Executable file
|
|
@ -0,0 +1,82 @@
|
|||
define(["exports", "components/fxos-mvc/dist/mvc", "components/gaia-list/gaia-list", "components/gaia-button/gaia-button", "js/lib/helpers"], function (exports, _componentsFxosMvcDistMvc, _componentsGaiaListGaiaList, _componentsGaiaButtonGaiaButton, _jsLibHelpers) {
|
||||
"use strict";
|
||||
|
||||
var _taggedTemplateLiteral = function (strings, raw) {
|
||||
return Object.defineProperties(strings, {
|
||||
raw: {
|
||||
value: raw
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var _extends = function (child, parent) {
|
||||
child.prototype = Object.create(parent.prototype, {
|
||||
constructor: {
|
||||
value: child,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
configurable: true
|
||||
}
|
||||
});
|
||||
child.__proto__ = parent;
|
||||
};
|
||||
|
||||
var View = _componentsFxosMvcDistMvc.View;
|
||||
var IconHelper = _jsLibHelpers.IconHelper;
|
||||
|
||||
|
||||
function capitalize(string) {
|
||||
return string[0].toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
var ListView = (function (View) {
|
||||
var ListView = function ListView() {
|
||||
this.el = document.createElement("gaia-list");
|
||||
this.el.className = "app-list";
|
||||
|
||||
this.elements = Object.create(null);
|
||||
};
|
||||
|
||||
_extends(ListView, View);
|
||||
|
||||
ListView.prototype.update = function (list) {
|
||||
for (var key in list) {
|
||||
var data = list[key];
|
||||
if (!this.elements[key]) {
|
||||
this.elements[key] = this.addElement(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ListView.prototype.setOpenHandler = function (handler) {
|
||||
if (!this.openHandler) {
|
||||
this.openHandler = handler;
|
||||
}
|
||||
};
|
||||
|
||||
ListView.prototype.addElement = function (data) {
|
||||
var item = document.createElement("li");
|
||||
item.classList.add("app");
|
||||
item.innerHTML = Sanitizer.unwrapSafeHTML(this.listItemTemplate(data));
|
||||
IconHelper.setImage(item.querySelector(".icon"), data.icon);
|
||||
this.el.appendChild(item);
|
||||
|
||||
item.querySelector(".open-button").addEventListener("click", function (data) {
|
||||
this.openHandler(data);
|
||||
}.bind(this, data));
|
||||
|
||||
return item;
|
||||
};
|
||||
|
||||
ListView.prototype.listItemTemplate = function (_ref) {
|
||||
var name = _ref.name;
|
||||
var author = _ref.author;
|
||||
var string = Sanitizer.createSafeHTML(_taggedTemplateLiteral(["\n <img class=\"icon\" />\n <div flex class=\"description\">\n <p class=\"name\">", "</p>\n <p class=\"author\">", "</p>\n </div>\n <gaia-button class=\"open-button\">Open</gaia-button>"], ["\n <img class=\"icon\" />\n <div flex class=\"description\">\n <p class=\"name\">", "</p>\n <p class=\"author\">", "</p>\n </div>\n <gaia-button class=\"open-button\">Open</gaia-button>"]), capitalize(name), author);
|
||||
return string;
|
||||
};
|
||||
|
||||
return ListView;
|
||||
})(View);
|
||||
|
||||
exports["default"] = ListView;
|
||||
});
|
||||
37
gaia/distros/spark/apps/customizer-launcher/js/view/main_view.js
Executable file
37
gaia/distros/spark/apps/customizer-launcher/js/view/main_view.js
Executable file
|
|
@ -0,0 +1,37 @@
|
|||
define(["exports", "components/fxos-mvc/dist/mvc", "components/gaia-header/dist/gaia-header", "components/fxos-dev-mode-dialog/fxos-dev-mode-dialog"], function (exports, _componentsFxosMvcDistMvc, _componentsGaiaHeaderDistGaiaHeader, _componentsFxosDevModeDialogFxosDevModeDialog) {
|
||||
"use strict";
|
||||
|
||||
var _extends = function (child, parent) {
|
||||
child.prototype = Object.create(parent.prototype, {
|
||||
constructor: {
|
||||
value: child,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
configurable: true
|
||||
}
|
||||
});
|
||||
child.__proto__ = parent;
|
||||
};
|
||||
|
||||
var View = _componentsFxosMvcDistMvc.View;
|
||||
var MainView = (function (View) {
|
||||
var MainView = function MainView() {
|
||||
View.apply(this, arguments);
|
||||
};
|
||||
|
||||
_extends(MainView, View);
|
||||
|
||||
MainView.prototype.render = function () {
|
||||
View.prototype.render.call(this);
|
||||
};
|
||||
|
||||
MainView.prototype.template = function () {
|
||||
var string = "\n <gaia-header>\n <h1>Customizer Launcher</h1>\n </gaia-header>\n <fxos-dev-mode-dialog></fxos-dev-mode-dialog>";
|
||||
return string;
|
||||
};
|
||||
|
||||
return MainView;
|
||||
})(View);
|
||||
|
||||
exports["default"] = MainView;
|
||||
});
|
||||
Loading…
Reference in a new issue