Compare commits

..

No commits in common. "main" and "v1.1.0" have entirely different histories.
main ... v1.1.0

31 changed files with 240 additions and 401 deletions

View file

@ -11,7 +11,7 @@
}, },
"chatImageLinks": { "chatImageLinks": {
"name": "Image Shortcuts", "name": "Image Shortcuts",
"hint": "(v13+) Automatically embeds image and gif links when posted in the chat. The link must point to a specific image file (e.g. png, webp, gif), otherwise it will not be embedded at all." "hint": "(v13+) When attempting to send an image/gif in chat, this allows you to easily embed the actual image in the text by changing \"http\"/\"https\" into \"image\", automatically displaying the image after sending the message."
}, },
"chatSidebarBackground": { "chatSidebarBackground": {
"name": "Chat Background", "name": "Chat Background",
@ -65,20 +65,17 @@
"label": "Configure Hotbar" "label": "Configure Hotbar"
} }
}, },
"keybindings": {
"toggleMouseBroadcast": {
"name": "Toggle Show Cursor",
"hint": "(v13+) Temporarily turns off the mouse cursor position that other players can see. Hides the cursor until you activate this keybind again."
}
},
"apps": { "apps": {
"no-settings-to-display": "No settings to display", "no-settings-to-display": "No settings to display",
"make-global-reference": "Make Global Reference" "make-global-reference": "Make Global Reference"
}, },
"notifs": { "dialogs": {
"toggleMouseBroadcast": { "chatImageLinks": {
"hidingCursor": "Hiding your cursor from others!", "didYouKnowImageLink": "",
"showingCursor": "Showing your cursor to others!" "convertAndDontShowAgain": "",
"justConvert": "",
"ignoreAndDontShowAgain": "",
"disableEntirely": ""
} }
} }
} }

View file

@ -1,7 +1,7 @@
{ {
"id": "oft", "id": "oft",
"title": "Oliver's Foundry Tweaks", "title": "Oliver's Foundry Tweaks",
"version": "1.2.0", "version": "1.1.0",
"authors": [ "authors": [
{ "name": "Oliver" } { "name": "Oliver" }
], ],

View file

@ -9,7 +9,6 @@ export class DevSettingsMenu extends OFTSettingsMenu {
}; };
static get _SETTINGS() { static get _SETTINGS() {
if (!categories.has(`dev`)) { return [] };
const devSettings = categories.get(`dev`); const devSettings = categories.get(`dev`);
const settingIDs = []; const settingIDs = [];
for (const [settingID, shown] of devSettings.entries()) { for (const [settingID, shown] of devSettings.entries()) {

View file

@ -9,7 +9,6 @@ export class HotbarSettingsMenu extends OFTSettingsMenu {
}; };
static get _SETTINGS() { static get _SETTINGS() {
if (!categories.has(`hotbar`)) { return [] };
const settings = categories.get(`hotbar`); const settings = categories.get(`hotbar`);
const settingIDs = []; const settingIDs = [];
for (const [settingID, shown] of settings.entries()) { for (const [settingID, shown] of settings.entries()) {

View file

@ -47,10 +47,6 @@ export class OFTSettingsMenu extends HAM(ApplicationV2) {
}; };
static _SETTINGS = []; static _SETTINGS = [];
static get isEmpty() {
return this._SETTINGS.length === 0;
};
// #endregion Options // #endregion Options
// #region Data Prep // #region Data Prep

View file

@ -1,10 +0,0 @@
import { key as preventMovementHistoryKey } from "../tweaks/preventMovementHistory.mjs";
Hooks.on(`oft.preRegisterTweak`, (tweakID) => {
if (game.system.id !== `crucible`) { return };
switch (tweakID) {
case preventMovementHistoryKey:
return false;
};
});

View file

@ -1,6 +1,5 @@
// Settings // Settings
import { preventMovementHistory } from "../tweaks/preventMovementHistory.mjs"; import { preventMovementHistory } from "../settings/preventMovementHistory.mjs";
import { toggleMouseBroadcast } from "../tweaks/toggleMouseBroadcast.mjs";
// Utils // Utils
import { Logger } from "../utils/Logger.mjs"; import { Logger } from "../utils/Logger.mjs";
@ -15,5 +14,4 @@ Hooks.on(`init`, () => {
Logger.log(`Initializing`); Logger.log(`Initializing`);
preventMovementHistory(); preventMovementHistory();
toggleMouseBroadcast();
}); });

View file

@ -1,19 +0,0 @@
/*
This hook is used to give external modules or systems the ability to interact
with the tweak registration lifecycle to do something before a tweak is registered
or being able to prevent registration of incompatible tweaks.
The hook receives a string indicating which tweak this hook is being called for
and a boolean value indicating if the tweak is considered invasive. Returning
explicit false prevents that tweak from being registered.
Invasive tweaks are additions that manipulate or override Document or helper
classes. An example of an invasive tweak is the "toggleMouseBroadcast",
tweak which replaces the existing "CONFIG.Canvas.layers.controls.layerClass"
class, most of these tweaks do smartly extend from the same CONFIG class
that they replace, however if they override a part of the class that
other modules/systems rely on, then that is a good time to block that
specific tweak's registration.
Call Signature: (tweakKey: string, isInvasive: boolean) => (void | boolean)
*/

View file

@ -0,0 +1,12 @@
/*
This hook is used for invasive hooks that we want to provide the
option for systems and other modules to be able to disable in case
of incompatabilities for whatever reason. This can also be used
internally within this module if we discover incompatabilites with
systems and want to disable it on our side.
This file is meant more documentation than anything at this point in
time.
Call Signature: (settingKey: string) => (void | boolean)
*/

View file

@ -0,0 +1,13 @@
/*
This hook is used to enable any modules that attempt to disable settings
or just want to investigate what settings are enabled to be able to get
a ping with information about which settings where registered entirely
and which weren't. The object that is passed to this is frozen and is
not meant to be edited as you cannot de-register nor prevent setting
registration from this hook. For that see the "oft.preventSetting" hook.
This file is meant more documentation than anything at this point in
time.
Call Signature: (settings: Record<string, boolean>) => void
*/

View file

@ -1,9 +0,0 @@
/*
This hook is used to broadcast the final status of all tweaks within the module,
allowing other modules to either confirm their registration didn't happen or to
do something once all of the module setup has been finalized. Tweak statuses
cannot be blocked or changed from this hook, to prevent a tweak from being
registered you should use the "oft.preRegisterTweak" hook.
Call Signature: (settings: Record<string, string>) => void
*/

View file

@ -6,19 +6,11 @@ prevent it from being as attention-grabbing compared to being at the top of the
list. list.
*/ */
Hooks.on(`renderSettingsConfig`, (app) => { Hooks.on(`renderSettingsConfig`, (app) => {
// MARK: Hide empty menus
for (const [key, config] of game.settings.menus) {
if (!key.startsWith(__ID__)) { continue };
if (config.type.isEmpty) {
const entry = app.element.querySelector(`.form-group:has(button[data-key="${key}"])`);
entry?.remove();
};
};
// MARK: devSettings Menu
/** @type {Node | undefined} */ /** @type {Node | undefined} */
const settingList = app.element.querySelector(`.tab[data-group="categories"][data-tab="oft"]`); const settingList = app.element.querySelector(`.tab[data-group="categories"][data-tab="oft"]`);
// MARK: devSettings Menu
/** @type {Node | undefined} */ /** @type {Node | undefined} */
const devSettingsMenu = app.element.querySelector(`.form-group:has(button[data-key="${__ID__}.devSettings"])`); const devSettingsMenu = app.element.querySelector(`.form-group:has(button[data-key="${__ID__}.devSettings"])`);
if (settingList && devSettingsMenu) { if (settingList && devSettingsMenu) {

View file

@ -1,15 +1,15 @@
// Settings // Settings
import { addGlobalDocReferrer } from "../tweaks/addGlobalDocReferrer.mjs"; import { addGlobalDocReferrer } from "../settings/addGlobalDocReferrer.mjs";
import { autoUnpauseOnLoad } from "../tweaks/autoUnpauseOnLoad.mjs"; import { autoUnpauseOnLoad } from "../settings/autoUnpauseOnLoad.mjs";
import { chatImageLinks } from "../tweaks/chatImageLinks.mjs"; import { chatImageLinks } from "../settings/chatImageLinks.mjs";
import { chatSidebarBackground } from "../tweaks/chatSidebarBackground.mjs"; import { chatSidebarBackground } from "../settings/chatSidebarBackground.mjs";
import { hotbarButtonGap } from "../tweaks/hotbarButtonGap.mjs"; import { hotbarButtonGap } from "../settings/hotbarButtonGap.mjs";
import { hotbarButtonSize } from "../tweaks/hotbarButtonSize.mjs"; import { hotbarButtonSize } from "../settings/hotbarButtonSize.mjs";
import { preventTokenRotation } from "../tweaks/preventTokenRotation.mjs"; import { preventTokenRotation } from "../settings/preventTokenRotation.mjs";
import { preventUserConfigOpen } from "../tweaks/preventUserConfigOpen.mjs"; import { preventUserConfigOpen } from "../settings/preventUserConfigOpen.mjs";
import { repositionHotbar } from "../tweaks/repositionHotbar.mjs"; import { repositionHotbar } from "../settings/repositionHotbar.mjs";
import { startingSidebarTab } from "../tweaks/startingSidebarTab.mjs"; import { startingSidebarTab } from "../settings/startingSidebarTab.mjs";
import { startSidebarExpanded } from "../tweaks/startSidebarExpanded.mjs"; import { startSidebarExpanded } from "../settings/startSidebarExpanded.mjs";
// Apps // Apps
import { DevSettingsMenu } from "../apps/DevSettingsMenu.mjs"; import { DevSettingsMenu } from "../apps/DevSettingsMenu.mjs";
@ -51,16 +51,7 @@ Hooks.on(`setup`, () => {
preventTokenRotation(); preventTokenRotation();
preventUserConfigOpen(); preventUserConfigOpen();
// Compatibility Code
if (Hooks.events[`oft.settingStatuses`] != null) {
foundry.utils.logCompatibilityWarning(
`The hook "${__ID__}.settingStatuses" has been renamed "${__ID__}.tweakStatuses".`,
{ since: `v1.2.0`, until: `v2.0.0`, stack: false, once: true },
);
Hooks.callAll(`oft.settingStatuses`, deepFreeze(status)); Hooks.callAll(`oft.settingStatuses`, deepFreeze(status));
};
Hooks.callAll(`${__ID__}.tweakStatuses`, deepFreeze(status));
game.modules.get(__ID__).api = deepFreeze({ game.modules.get(__ID__).api = deepFreeze({
settings: status, settings: status,
}); });

View file

@ -2,6 +2,3 @@
import "./hooks/init.mjs"; import "./hooks/init.mjs";
import "./hooks/setup.mjs"; import "./hooks/setup.mjs";
import "./hooks/renderSettingsConfig.mjs"; import "./hooks/renderSettingsConfig.mjs";
// Compatibility w/ other packages
import "./conflicts/crucible.mjs";

View file

@ -1,14 +1,12 @@
import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs";
import { __ID__ } from "../consts.mjs"; import { __ID__ } from "../consts.mjs";
import { Logger } from "../utils/Logger.mjs"; import { Logger } from "../utils/Logger.mjs";
import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs";
import { registerDevSetting } from "../utils/SubMenuSettings.mjs"; import { registerDevSetting } from "../utils/SubMenuSettings.mjs";
export const key = `addGlobalDocReferrer`; const key = `addGlobalDocReferrer`;
export function addGlobalDocReferrer() { export function addGlobalDocReferrer() {
status[key] = SettingStatusEnum.Unknown; status[key] = SettingStatusEnum.Unknown;
if (preventTweakRegistration(key)) { return };
// #region Registration // #region Registration
Logger.log(`Registering setting: ${key}`); Logger.log(`Registering setting: ${key}`);

View file

@ -1,14 +1,19 @@
import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs";
import { __ID__ } from "../consts.mjs"; import { __ID__ } from "../consts.mjs";
import { Logger } from "../utils/Logger.mjs"; import { Logger } from "../utils/Logger.mjs";
import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs";
import { registerDevSetting } from "../utils/SubMenuSettings.mjs"; import { registerDevSetting } from "../utils/SubMenuSettings.mjs";
export const key = `autoUnpauseOnLoad`; const key = `autoUnpauseOnLoad`;
export function autoUnpauseOnLoad() { export function autoUnpauseOnLoad() {
status[key] = SettingStatusEnum.Unknown; status[key] = SettingStatusEnum.Unknown;
if (preventTweakRegistration(key)) { return };
const prevented = Hooks.call(`${__ID__}.preventSetting`, key);
if (!prevented) {
Logger.log(`Preventing setting "${key}" from being registered`);
status[key] = SettingStatusEnum.Blocked;
return;
};
// #region Registration // #region Registration
Logger.log(`Registering setting: ${key}`); Logger.log(`Registering setting: ${key}`);

View file

@ -0,0 +1,130 @@
import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs";
import { __ID__ } from "../consts.mjs";
import { Logger } from "../utils/Logger.mjs";
const { DialogV2 } = foundry.applications.api;
const key = `chatImageLinks`;
const IMAGE_TYPES = [
`png`,
`jpg`,
`jpeg`,
`webp`,
`svg`,
];
export function chatImageLinks() {
status[key] = SettingStatusEnum.Unknown;
const prevented = Hooks.call(`${__ID__}.preventSetting`, key);
if (!prevented) {
Logger.log(`Preventing setting "${key}" from being registered`);
status[key] = SettingStatusEnum.Blocked;
return;
};
// #region Registration
Logger.log(`Registering setting: ${key}`);
game.settings.register(__ID__, key, {
name: `OFT.setting.${key}.name`,
hint: `OFT.setting.${key}.hint`,
scope: `user`,
type: Boolean,
default: true,
config: true,
requiresReload: true,
});
game.settings.register(__ID__, key + `-showPromptAgain`, {
scope: `user`,
type: Boolean,
default: true,
config: false,
});
// #endregion Registration
// #region Implementation
if (game.settings.get(__ID__, key)) {
Logger.log(`setting:${key} | Adding text enricher`);
// MARK: Enricher
const pattern = new RegExp(
`(?<!=|")(image:\\/\\/.*\\.(?:${IMAGE_TYPES.join(`|`)}))`,
`gi`,
);
CONFIG.TextEditor.enrichers.push({
pattern,
replaceParent: true,
enricher: async (url) => {
Logger.debug(url);
url = url[0].replace(/^image:\/\//, ``);
const secure = `https://${url}`;
const insecure = `http://${url}`;
if (await isAcceptableImage(secure)) {
const img = document.createElement(`img`);
img.src = secure;
img.alt = secure;
return img;
};
if (await isAcceptableImage(insecure)) {
const img = document.createElement(`img`);
img.src = insecure;
img.alt = insecure;
return img;
};
return null;
},
});
// MARK: Chat Input
// Hooks.on(`chatMessage`, (chatLog, message, options) => {
// if (!game.settings.get(__ID__, key)) { return };
// const match = message.match(pattern);
// if (!match) { return };
// DialogV2.wait({
// rejectClose: false,
// content: game.i18n.localize(`OFT.dialogs.chatImageLinks.didYouKnowImageLink`),
// buttons: [
// { action: ``, label: `OFT.dialogs.chatImageLinks.convertAndDontShowAgain` },
// { action: ``, label: `OFT.dialogs.chatImageLinks.justConvert` },
// { action: ``, label: `OFT.dialogs.chatImageLinks.ignoreAndDontShowAgain` },
// { action: ``, label: `OFT.dialogs.chatImageLinks.disableEntirely` },
// ],
// })
// .then((selected) => {
// chatLog.processMessage(message, options);
// });
// return false;
// });
}
// #endregion Implementation
status[key] = SettingStatusEnum.Registered;
};
// #region Helpers
async function isAcceptableImage(url) {
try {
const response = await fetch(url, { method: `HEAD` });
const contentType = response.headers.get(`Content-Type`);
Logger.debug(`Image data:`, { url, contentType });
let [ superType, subtype ] = contentType.split(`/`);
if (superType !== `image`) {
return false;
};
if (subtype.includes(`+`)) {
subtype = subtype.split(`+`, 2).at(0);
};
return IMAGE_TYPES.includes(subtype);
} catch {
return false;
};
};
// #endregion Helpers

View file

@ -1,13 +1,11 @@
import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs";
import { __ID__ } from "../consts.mjs"; import { __ID__ } from "../consts.mjs";
import { Logger } from "../utils/Logger.mjs"; import { Logger } from "../utils/Logger.mjs";
import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs";
export const key = `chatSidebarBackground`; const key = `chatSidebarBackground`;
export function chatSidebarBackground() { export function chatSidebarBackground() {
status[key] = SettingStatusEnum.Unknown; status[key] = SettingStatusEnum.Unknown;
if (preventTweakRegistration(key)) { return };
// #region Registration // #region Registration
Logger.log(`Registering setting: ${key}`); Logger.log(`Registering setting: ${key}`);

View file

@ -1,14 +1,19 @@
import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs";
import { __ID__ } from "../consts.mjs"; import { __ID__ } from "../consts.mjs";
import { Logger } from "../utils/Logger.mjs"; import { Logger } from "../utils/Logger.mjs";
import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs";
import { registerCategorySetting } from "../utils/SubMenuSettings.mjs"; import { registerCategorySetting } from "../utils/SubMenuSettings.mjs";
export const key = `hotbarButtonGap`; const key = `hotbarButtonGap`;
export function hotbarButtonGap() { export function hotbarButtonGap() {
status[key] = SettingStatusEnum.Unknown; status[key] = SettingStatusEnum.Unknown;
if (preventTweakRegistration(key)) { return };
const prevented = Hooks.call(`${__ID__}.preventSetting`, key);
if (!prevented) {
Logger.log(`Preventing setting "${key}" from being registered`);
status[key] = SettingStatusEnum.Blocked;
return;
};
// #region Registration // #region Registration
Logger.log(`Registering setting: ${key}`); Logger.log(`Registering setting: ${key}`);

View file

@ -1,14 +1,19 @@
import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs";
import { __ID__ } from "../consts.mjs"; import { __ID__ } from "../consts.mjs";
import { Logger } from "../utils/Logger.mjs"; import { Logger } from "../utils/Logger.mjs";
import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs";
import { registerCategorySetting } from "../utils/SubMenuSettings.mjs"; import { registerCategorySetting } from "../utils/SubMenuSettings.mjs";
export const key = `hotbarButtonSize`; const key = `hotbarButtonSize`;
export function hotbarButtonSize() { export function hotbarButtonSize() {
status[key] = SettingStatusEnum.Unknown; status[key] = SettingStatusEnum.Unknown;
if (preventTweakRegistration(key)) { return };
const prevented = Hooks.call(`${__ID__}.preventSetting`, key);
if (!prevented) {
Logger.log(`Preventing setting "${key}" from being registered`);
status[key] = SettingStatusEnum.Blocked;
return;
};
// #region Registration // #region Registration
Logger.log(`Registering setting: ${key}`); Logger.log(`Registering setting: ${key}`);

View file

@ -1,13 +1,18 @@
import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs";
import { __ID__ } from "../consts.mjs"; import { __ID__ } from "../consts.mjs";
import { Logger } from "../utils/Logger.mjs"; import { Logger } from "../utils/Logger.mjs";
import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs";
export const key = `preventMovementHistory`; const key = `preventMovementHistory`;
export function preventMovementHistory() { export function preventMovementHistory() {
status[key] = SettingStatusEnum.Unknown; status[key] = SettingStatusEnum.Unknown;
if (preventTweakRegistration(key, true)) { return };
const prevented = Hooks.call(`${__ID__}.preventSetting`, key);
if (!prevented) {
Logger.log(`Preventing setting "${key}" from being registered`);
status[key] = SettingStatusEnum.Blocked;
return;
};
// #region Registration // #region Registration
Logger.log(`Registering setting: ${key}`); Logger.log(`Registering setting: ${key}`);

View file

@ -1,13 +1,18 @@
import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs";
import { __ID__ } from "../consts.mjs"; import { __ID__ } from "../consts.mjs";
import { Logger } from "../utils/Logger.mjs"; import { Logger } from "../utils/Logger.mjs";
import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs";
export const key = `preventTokenRotation`; const key = `preventTokenRotation`;
export function preventTokenRotation() { export function preventTokenRotation() {
status[key] = SettingStatusEnum.Unknown; status[key] = SettingStatusEnum.Unknown;
if (preventTweakRegistration(key)) { return };
const prevented = Hooks.call(`${__ID__}.preventSetting`, key);
if (!prevented) {
Logger.log(`Preventing setting "${key}" from being registered`);
status[key] = SettingStatusEnum.Blocked;
return;
};
/** @type {number|null} */ /** @type {number|null} */
let hookID = null; let hookID = null;

View file

@ -1,13 +1,18 @@
import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs";
import { __ID__ } from "../consts.mjs"; import { __ID__ } from "../consts.mjs";
import { Logger } from "../utils/Logger.mjs"; import { Logger } from "../utils/Logger.mjs";
import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs";
export const key = `preventUserConfigOpen`; const key = `preventUserConfigOpen`;
export function preventUserConfigOpen() { export function preventUserConfigOpen() {
status[key] = SettingStatusEnum.Unknown; status[key] = SettingStatusEnum.Unknown;
if (preventTweakRegistration(key)) { return };
const prevented = Hooks.call(`${__ID__}.preventSetting`, key);
if (!prevented) {
Logger.log(`Preventing setting "${key}" from being registered`);
status[key] = SettingStatusEnum.Blocked;
return;
};
// #region Registration // #region Registration
Logger.log(`Registering setting: ${key}`); Logger.log(`Registering setting: ${key}`);

View file

@ -1,14 +1,19 @@
import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs";
import { __ID__ } from "../consts.mjs"; import { __ID__ } from "../consts.mjs";
import { Logger } from "../utils/Logger.mjs"; import { Logger } from "../utils/Logger.mjs";
import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs";
import { registerCategorySetting } from "../utils/SubMenuSettings.mjs"; import { registerCategorySetting } from "../utils/SubMenuSettings.mjs";
export const key = `repositionHotbar`; const key = `repositionHotbar`;
export function repositionHotbar() { export function repositionHotbar() {
status[key] = SettingStatusEnum.Unknown; status[key] = SettingStatusEnum.Unknown;
if (preventTweakRegistration(key)) { return };
const prevented = Hooks.call(`${__ID__}.preventSetting`, key);
if (!prevented) {
Logger.log(`Preventing setting "${key}" from being registered`);
status[key] = SettingStatusEnum.Blocked;
return;
};
// #region Registration // #region Registration
Logger.log(`Registering setting: ${key}`); Logger.log(`Registering setting: ${key}`);

View file

@ -1,13 +1,11 @@
import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs";
import { __ID__ } from "../consts.mjs"; import { __ID__ } from "../consts.mjs";
import { Logger } from "../utils/Logger.mjs"; import { Logger } from "../utils/Logger.mjs";
import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs";
export const key = `startSidebarExpanded`; const key = `startSidebarExpanded`;
export function startSidebarExpanded() { export function startSidebarExpanded() {
status[key] = SettingStatusEnum.Unknown; status[key] = SettingStatusEnum.Unknown;
if (preventTweakRegistration(key)) { return };
// #region Registration // #region Registration
Logger.log(`Registering setting: ${key}`); Logger.log(`Registering setting: ${key}`);

View file

@ -1,13 +1,11 @@
import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs";
import { __ID__ } from "../consts.mjs"; import { __ID__ } from "../consts.mjs";
import { Logger } from "../utils/Logger.mjs"; import { Logger } from "../utils/Logger.mjs";
import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs";
export const key = `startingSidebarTab`; const key = `startingSidebarTab`;
export function startingSidebarTab() { export function startingSidebarTab() {
status[key] = SettingStatusEnum.Unknown; status[key] = SettingStatusEnum.Unknown;
if (preventTweakRegistration(key)) { return };
// #region Registration // #region Registration
Logger.log(`Registering setting: ${key}`); Logger.log(`Registering setting: ${key}`);

View file

@ -1,125 +0,0 @@
import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs";
import { __ID__ } from "../consts.mjs";
import { Logger } from "../utils/Logger.mjs";
import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs";
export const key = `chatImageLinks`;
const IMAGE_TYPES = [
`png`,
`jpg`,
`jpeg`,
`webp`,
`svg`,
];
export function chatImageLinks() {
status[key] = SettingStatusEnum.Unknown;
if (preventTweakRegistration(key)) { return };
/** @type {number|null} */
let hookID = null;
// #region Registration
Logger.log(`Registering setting: ${key}`);
game.settings.register(__ID__, key, {
name: `OFT.setting.${key}.name`,
hint: `OFT.setting.${key}.hint`,
scope: `user`,
type: Boolean,
default: true,
config: true,
requiresReload: false,
onChange: (newValue) => {
if (newValue) {
hookID = Hooks.on(`chatMessage`, chatMessageHandler);
} else if (hookID != null) {
Hooks.off(`chatMessage`, hookID);
};
},
});
// #endregion Registration
// #region Implementation
if (game.settings.get(__ID__, key)) {
Logger.log(`setting:${key} | Adding chat message listener`);
Hooks.on(`chatMessage`, chatMessageHandler);
};
// #endregion Implementation
status[key] = SettingStatusEnum.Registered;
};
// #region Helpers
const pattern = new RegExp(
`https?:\\/\\/\\S*\\.(?:${IMAGE_TYPES.join(`|`)})`,
`gi`,
);
// MARK: Mutate & Resend
const handled = new Set();
async function mutateAndResendMessage(chatLog, message, options) {
const validMatches = new Set();
const matches = message.match(pattern);
for (const match of matches) {
if (await isAcceptableImage(match)) {
validMatches.add(match);
};
};
message = message.replaceAll(
pattern,
(url) => {
if (!validMatches.has(url)) {
return url;
};
return `<img src="${url}" alt="${url}">`;
},
);
handled.add(message);
chatLog.processMessage(message, options);
};
// MARK: Chat Message
/**
* Must be synchronous since it is a hook handler, but the mutation +
* resending can be done asynchronously since it doesn't matter how
* long it takes.
*/
function chatMessageHandler(chatLog, message, options) {
if (!game.settings.get(__ID__, key)) { return };
// Don't re-process the same message
if (handled.has(message)) { return };
// Prevent cancellation for non-matching messages
const match = message.match(pattern);
if (!match) { return };
mutateAndResendMessage(chatLog, message, options);
return false;
};
// MARK: isAcceptableImage
async function isAcceptableImage(url) {
if (!URL.canParse(url)) { return false };
try {
const response = await fetch(url, { method: `HEAD` });
const contentType = response.headers.get(`Content-Type`);
Logger.debug(`Image data:`, { url, contentType });
let [ superType, subtype ] = contentType.split(`/`);
if (superType !== `image`) {
return false;
};
if (subtype.includes(`+`)) {
subtype = subtype.split(`+`, 2).at(0);
};
return IMAGE_TYPES.includes(subtype);
} catch {
return false;
};
};
// #endregion Helpers

View file

@ -1,122 +0,0 @@
import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs";
import { __ID__ } from "../consts.mjs";
import { Logger } from "../utils/Logger.mjs";
import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs";
export const key = `toggleMouseBroadcast`;
/** @type {number | null} */
let notifID = null;
export function toggleMouseBroadcast() {
status[key] = SettingStatusEnum.Unknown;
if (preventTweakRegistration(key, true)) { return };
// #region Registration
Logger.log(`Registering setting: ${key}`);
// MARK: setting
game.settings.register(__ID__, key, {
scope: `client`,
config: false,
default: true,
});
// MARK: keybind
game.keybindings.register(__ID__, key, {
name: `OFT.keybindings.${key}.name`,
hint: `OFT.keybindings.${key}.hint`,
precedence: CONST.KEYBINDING_PRECEDENCE.NORMAL,
restricted: true,
editable: [
{ key: `KeyH` },
],
onDown: (event) => {
if (!game.user.hasPermission(`SHOW_CURSOR`)) { return };
event.preventDefault?.();
const current = game.settings.get(__ID__, key);
if (current) {
notifID = ui.notifications.warn(
`OFT.notifs.${key}.hidingCursor`,
{ console: false, localize: true, permanent: true },
).id;
} else {
if (notifID != null) {
ui.notifications.remove(notifID);
notifID = null;
};
ui.notifications.success(
`OFT.notifs.${key}.showingCursor`,
{ console: false, localize: true },
);
};
game.settings.set(__ID__, key, !current);
// Hide the existing cursor
game.user.broadcastActivity({ cursor: null });
return true;
},
});
// #endregion Registration
// #region Implementation
Hooks.on(`renderControlsConfig`, renderControlsConfigHandler);
Hooks.once(`ready`, readyHandler);
class OFTControlsLayer extends CONFIG.Canvas.layers.controls.layerClass {
_onMouseMove(currentPos) {
if (!game.settings.get(__ID__, key)) {
game.user.broadcastActivity({});
};
super._onMouseMove(currentPos);
};
};
CONFIG.Canvas.layers.controls.layerClass = OFTControlsLayer;
// #endregion Implementation
status[key] = SettingStatusEnum.Registered;
};
// #region Helpers
const tabGroup = `categories`;
/**
* Handle showing the "hiding your cursor" notification when the user
* connects to the server initially, allowing the current state to be
* correctly described to the user.
*/
function readyHandler() {
const hideCursor = !game.settings.get(__ID__, key);
const canShowCursor = game.user.hasPermission(`SHOW_CURSOR`);
if (hideCursor && canShowCursor) {
notifID = ui.notifications.warn(
`OFT.notifs.${key}.hidingCursor`,
{ console: false, localize: true, permanent: true },
).id;
};
};
/**
* Handles hiding the keybinding from the configuration if and when the user
* does not have the SHOW_CURSOR permission since this keybinding is really
* useless if they can't even broadcast their cursor in the first place.
*/
function renderControlsConfigHandler(_app, element) {
if (game.user.hasPermission(`SHOW_CURSOR`)) { return };
const keybindingList = element.querySelector(`section[data-group="${tabGroup}"][data-tab="${__ID__}"]`);
const tabButton = element.querySelector(`button[data-group="${tabGroup}"][data-tab="${__ID__}"]`);
const keybind = keybindingList.querySelector(`.form-group[data-action-id="${__ID__}.${key}"]`);
keybind?.remove();
if (keybindingList.childElementCount === 0) {
keybindingList.remove();
tabButton.remove();
};
};
// #endregion Helpers

View file

@ -1,26 +0,0 @@
import { SettingStatusEnum, status } from "./SettingStatus.mjs";
import { __ID__ } from "../consts.mjs";
import { Logger } from "./Logger.mjs";
export function preventTweakRegistration(key, invasive = false) {
let prevented = Hooks.call(`${__ID__}.preRegisterTweak`, key, invasive);
// Compatibility Code
if (Hooks.events[`${__ID__}.preventSetting`] != null) {
foundry.utils.logCompatibilityWarning(
`The hook "${__ID__}.preventSetting" has been renamed "${__ID__}.registerTweak".`,
{ since: `v1.2.0`, until: `v2.0.0`, stack: false, once: true },
);
if (prevented !== false) {
prevented = Hooks.call(`${__ID__}.preventSetting`, key);
};
};
if (!prevented) {
Logger.log(`Preventing setting "${key}" from being registered`);
status[key] = SettingStatusEnum.Blocked;
return true;
};
return false;
};

View file

@ -1 +0,0 @@
🔒

View file

@ -6,4 +6,4 @@
@import url("./apps.css"); @import url("./apps.css");
/* Make the chat sidebar the same width as all the other tabs */ /* Make the chat sidebar the same width as all the other tabs */
.chat-sidebar:not(.sidebar-popout) { width: var(--sidebar-width); } .chat-sidebar:not(.sidebar-popout) { width: unset; }