Initial
This commit is contained in:
290
resources/app/client/core/issues.js
Normal file
290
resources/app/client/core/issues.js
Normal file
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* An object structure of document types at the top level, with a count of different sub-types for that document type.
|
||||
* @typedef {Record<string, Record<string, number>>} ModuleSubTypeCounts
|
||||
*/
|
||||
|
||||
/**
|
||||
* A class responsible for tracking issues in the current world.
|
||||
*/
|
||||
class ClientIssues {
|
||||
/**
|
||||
* Keep track of valid Documents in the world that are using module-provided sub-types.
|
||||
* @type {Map<string, ModuleSubTypeCounts>}
|
||||
*/
|
||||
#moduleTypeMap = new Map();
|
||||
|
||||
/**
|
||||
* Keep track of document validation failures.
|
||||
* @type {object}
|
||||
*/
|
||||
#documentValidationFailures = {};
|
||||
|
||||
/**
|
||||
* @typedef {object} UsabilityIssue
|
||||
* @property {string} message The pre-localized message to display in relation to the usability issue.
|
||||
* @property {string} severity The severity of the issue, either "error", "warning", or "info".
|
||||
* @property {object} [params] Parameters to supply to the localization.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Keep track of any usability issues related to browser or technology versions.
|
||||
* @type {Record<string, UsabilityIssue>}
|
||||
*/
|
||||
#usabilityIssues = {};
|
||||
|
||||
/**
|
||||
* The minimum supported resolution.
|
||||
* @type {{WIDTH: number, HEIGHT: number}}
|
||||
*/
|
||||
static #MIN_RESOLUTION = {WIDTH: 1024, HEIGHT: 700};
|
||||
|
||||
/**
|
||||
* @typedef {object} BrowserTest
|
||||
* @property {number} minimum The minimum supported version for this browser.
|
||||
* @property {RegExp} match A regular expression to match the browser against the user agent string.
|
||||
* @property {string} message A message to display if the user's browser version does not meet the minimum.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The minimum supported client versions.
|
||||
* @type {Record<string, BrowserTest>}
|
||||
*/
|
||||
static #BROWSER_TESTS = {
|
||||
Electron: {
|
||||
minimum: 29,
|
||||
match: /Electron\/(\d+)\./,
|
||||
message: "ERROR.ElectronVersion"
|
||||
},
|
||||
Chromium: {
|
||||
minimum: 105,
|
||||
match: /Chrom(?:e|ium)\/(\d+)\./,
|
||||
message: "ERROR.BrowserVersion"
|
||||
},
|
||||
Firefox: {
|
||||
minimum: 121,
|
||||
match: /Firefox\/(\d+)\./,
|
||||
message: "ERROR.BrowserVersion"
|
||||
},
|
||||
Safari: {
|
||||
minimum: 15.4,
|
||||
match: /Version\/(\d+)\..*Safari\//,
|
||||
message: "ERROR.BrowserVersion"
|
||||
}
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add a Document to the count of module-provided sub-types.
|
||||
* @param {string} documentName The Document name.
|
||||
* @param {string} subType The Document's sub-type.
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.decrement=false] Decrement the counter rather than incrementing it.
|
||||
*/
|
||||
#countDocumentSubType(documentName, subType, {decrement=false}={}) {
|
||||
if ( !((typeof subType === "string") && subType.includes(".")) ) return;
|
||||
const [moduleId, ...rest] = subType.split(".");
|
||||
subType = rest.join(".");
|
||||
if ( !this.#moduleTypeMap.has(moduleId) ) this.#moduleTypeMap.set(moduleId, {});
|
||||
const counts = this.#moduleTypeMap.get(moduleId);
|
||||
const types = counts[documentName] ??= {};
|
||||
types[subType] ??= 0;
|
||||
if ( decrement ) types[subType] = Math.max(types[subType] - 1, 0);
|
||||
else types[subType]++;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Detect the user's browser and display a notification if it is below the minimum required version.
|
||||
*/
|
||||
#detectBrowserVersion() {
|
||||
for ( const [browser, {minimum, match, message}] of Object.entries(ClientIssues.#BROWSER_TESTS) ) {
|
||||
const [, version] = navigator.userAgent.match(match) ?? [];
|
||||
if ( !Number.isNumeric(version) ) continue;
|
||||
if ( Number(version) < minimum ) {
|
||||
const err = game.i18n.format(message, {browser, version, minimum});
|
||||
ui.notifications?.error(err, {permanent: true, console: true});
|
||||
this.#usabilityIssues.browserVersionIncompatible = {
|
||||
message,
|
||||
severity: "error",
|
||||
params: {browser, version, minimum}
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Record a reference to a resolution notification ID so that we can remove it if the problem is remedied.
|
||||
* @type {number}
|
||||
*/
|
||||
#resolutionTooLowNotification;
|
||||
|
||||
/**
|
||||
* Detect the user's resolution and display a notification if it is too small.
|
||||
*/
|
||||
#detectResolution() {
|
||||
const {WIDTH: reqWidth, HEIGHT: reqHeight} = ClientIssues.#MIN_RESOLUTION;
|
||||
const {innerWidth: width, innerHeight: height} = window;
|
||||
if ( (height < reqHeight) || (width < reqWidth) ) {
|
||||
|
||||
// Display a permanent error notification
|
||||
if ( ui.notifications && !this.#resolutionTooLowNotification ) {
|
||||
this.#resolutionTooLowNotification = ui.notifications.error(game.i18n.format("ERROR.LowResolution", {
|
||||
width, reqWidth, height, reqHeight
|
||||
}), {permanent: true});
|
||||
}
|
||||
|
||||
// Record the usability issue
|
||||
this.#usabilityIssues.resolutionTooLow = {
|
||||
message: "ERROR.LowResolution",
|
||||
severity: "error",
|
||||
params: {width, reqWidth, height, reqHeight}
|
||||
};
|
||||
}
|
||||
|
||||
// Remove an error notification if present
|
||||
else {
|
||||
if ( this.#resolutionTooLowNotification ) {
|
||||
this.#resolutionTooLowNotification = ui.notifications.remove(this.#resolutionTooLowNotification);
|
||||
}
|
||||
delete this.#usabilityIssues.resolutionTooLow;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Detect and display warnings for known performance issues which may occur due to the user's hardware or browser
|
||||
* configuration.
|
||||
* @internal
|
||||
*/
|
||||
_detectWebGLIssues() {
|
||||
const context = canvas.app.renderer.context;
|
||||
try {
|
||||
const rendererInfo = SupportDetails.getWebGLRendererInfo(context.gl);
|
||||
if ( /swiftshader/i.test(rendererInfo) ) {
|
||||
ui.notifications.warn("ERROR.NoHardwareAcceleration", {localize: true, permanent: true});
|
||||
this.#usabilityIssues.hardwareAccel = {message: "ERROR.NoHardwareAcceleration", severity: "error"};
|
||||
}
|
||||
} catch ( err ) {
|
||||
ui.notifications.warn("ERROR.RendererNotDetected", {localize: true, permanent: true});
|
||||
this.#usabilityIssues.noRenderer = {message: "ERROR.RendererNotDetected", severity: "warning"};
|
||||
}
|
||||
|
||||
// Verify that WebGL2 is being used.
|
||||
if ( !canvas.supported.webGL2 ) {
|
||||
ui.notifications.error("ERROR.NoWebGL2", {localize: true, permanent: true});
|
||||
this.#usabilityIssues.webgl2 = {message: "ERROR.NoWebGL2", severity: "error"};
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add an invalid Document to the module-provided sub-type counts.
|
||||
* @param {typeof Document} cls The Document class.
|
||||
* @param {object} source The Document's source data.
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.decrement=false] Decrement the counter rather than incrementing it.
|
||||
* @internal
|
||||
*/
|
||||
_countDocumentSubType(cls, source, options={}) {
|
||||
if ( cls.hasTypeData ) this.#countDocumentSubType(cls.documentName, source.type, options);
|
||||
for ( const [embeddedName, field] of Object.entries(cls.hierarchy) ) {
|
||||
if ( !(field instanceof foundry.data.fields.EmbeddedCollectionField) ) continue;
|
||||
for ( const embedded of source[embeddedName] ) {
|
||||
this._countDocumentSubType(field.model, embedded, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Track a validation failure that occurred in a WorldCollection.
|
||||
* @param {WorldCollection} collection The parent collection.
|
||||
* @param {object} source The Document's source data.
|
||||
* @param {DataModelValidationError} error The validation error.
|
||||
* @internal
|
||||
*/
|
||||
_trackValidationFailure(collection, source, error) {
|
||||
if ( !(collection instanceof WorldCollection) ) return;
|
||||
if ( !(error instanceof foundry.data.validation.DataModelValidationError) ) return;
|
||||
const documentName = collection.documentName;
|
||||
this.#documentValidationFailures[documentName] ??= {};
|
||||
this.#documentValidationFailures[documentName][source._id] = {name: source.name, error};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Detect and record certain usability error messages which are likely to result in the user having a bad experience.
|
||||
* @internal
|
||||
*/
|
||||
_detectUsabilityIssues() {
|
||||
this.#detectResolution();
|
||||
this.#detectBrowserVersion();
|
||||
window.addEventListener("resize", foundry.utils.debounce(this.#detectResolution.bind(this), 250), {passive: true});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the Document sub-type counts for a given module.
|
||||
* @param {Module|string} module The module or its ID.
|
||||
* @returns {ModuleSubTypeCounts}
|
||||
*/
|
||||
getSubTypeCountsFor(module) {
|
||||
return this.#moduleTypeMap.get(module.id ?? module);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Retrieve all sub-type counts in the world.
|
||||
* @returns {Iterator<string, ModuleSubTypeCounts>}
|
||||
*/
|
||||
getAllSubTypeCounts() {
|
||||
return this.#moduleTypeMap.entries();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Retrieve the tracked validation failures.
|
||||
* @returns {object}
|
||||
*/
|
||||
get validationFailures() {
|
||||
return this.#documentValidationFailures;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Retrieve the tracked usability issues.
|
||||
* @returns {Record<string, UsabilityIssue>}
|
||||
*/
|
||||
get usabilityIssues() {
|
||||
return this.#usabilityIssues;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @typedef {object} PackageCompatibilityIssue
|
||||
* @property {string[]} error Error messages.
|
||||
* @property {string[]} warning Warning messages.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Retrieve package compatibility issues.
|
||||
* @returns {Record<string, PackageCompatibilityIssue>}
|
||||
*/
|
||||
get packageCompatibilityIssues() {
|
||||
return game.data.packageWarnings;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user