Module cleanup and tests
CI / ci (push) Failing after 7s

This commit is contained in:
2026-05-24 23:13:45 +02:00
parent 63d83e999a
commit 5dc9b3b8d4
72 changed files with 2545 additions and 1220 deletions
+4 -4
View File
@@ -288,14 +288,14 @@ export class ConfirmationBar {
* @private
*/
_buildMessage(presetName, counts, variant) {
const baseMsg = this._adapter.i18n.localize('video-view-manager.presets.confirmation.applied')
const baseMsg = this._adapter.i18n.localize('scrying-pool.presets.confirmation.applied')
.replace('{name}', presetName);
const countMsg = this._adapter.i18n.localize('video-view-manager.presets.confirmation.counts')
const countMsg = this._adapter.i18n.localize('scrying-pool.presets.confirmation.counts')
.replace('{hidden}', counts.hidden)
.replace('{visible}', counts.visible);
if (variant === 'amber') {
const suffix = this._adapter.i18n.localize('video-view-manager.presets.confirmation.partial-fail');
const suffix = this._adapter.i18n.localize('scrying-pool.presets.confirmation.partial-fail');
return `${baseMsg} ${countMsg} ${suffix}`;
}
@@ -313,7 +313,7 @@ export class ConfirmationBar {
*/
_buildHtml(message, variant) {
const variantClass = variant === 'amber' ? 'sp-confirmation-bar--amber' : 'sp-confirmation-bar--default';
const undoLabel = this._adapter.i18n.localize('video-view-manager.presets.confirmation.undo');
const undoLabel = this._adapter.i18n.localize('scrying-pool.presets.confirmation.undo');
// Use data-action for event delegation via StripOverlayLayer
// The onclick handler is set up in _setupEventListeners
+82 -13
View File
@@ -47,12 +47,12 @@ export class DirectorsBoard extends _AppBase {
id: 'scrying-pool-directors-board',
classes: ['scrying-pool', 'directors-board'],
window: { title: "Director's Board", resizable: true },
position: { width: 400, height: 300 },
position: { width: 420, height: 480 },
};
static PARTS = {
board: {
template: 'modules/video-view-manager/templates/directors-board.hbs',
template: 'modules/scrying-pool/templates/directors-board.hbs',
},
};
@@ -116,7 +116,7 @@ export class DirectorsBoard extends _AppBase {
/** Loads saved window position from GM user flag. */
_loadPosition() {
try {
const saved = game.user?.getFlag('video-view-manager', 'directorsBoardState');
const saved = game.user?.getFlag('scrying-pool', 'directorsBoardState');
if (saved?.open === true && saved.left != null && saved.top != null) {
// Ensure options.position exists and is mutable
if (this.options?.position) {
@@ -124,8 +124,8 @@ export class DirectorsBoard extends _AppBase {
Object.assign(this.options.position, {
left: saved.left,
top: saved.top,
width: saved.width ?? 400,
height: saved.height ?? 300,
width: saved.width ?? 420,
height: saved.height ?? 480,
});
}
}
@@ -374,6 +374,8 @@ export class DirectorsBoard extends _AppBase {
autoApplyPresetName: autoApplyConfig.presetName,
autoApplyPreDelay: autoApplyConfig.preDelay,
presets: this._scenePresetManager?.list?.() ?? [],
// A/V mode — reflects current world AV state (0 = disabled, 3 = audio+video)
avModeEnabled: (game.webrtc?.settings?.world?.mode ?? 0) !== 0,
};
}
@@ -416,6 +418,9 @@ export class DirectorsBoard extends _AppBase {
case 'import-presets': this._onImportPresets(); break;
// Story 3.2: Scene auto-apply panel toggle
case 'toggle-preset-panel': this._togglePresetPanel(); break;
case 'toggle-av-mode': this._onToggleAVMode(); break;
case 'open-av-config': this._onOpenAVConfig(); break;
case 'close': this.close(); break;
}
};
this._focusinHandler = (e) => {
@@ -428,6 +433,28 @@ export class DirectorsBoard extends _AppBase {
root.addEventListener('click', this._clickHandler);
root.addEventListener('focusin', this._focusinHandler);
root.addEventListener('keydown', this._keydownHandler);
// Drag grip — custom drag (ApplicationV2 header is hidden)
const grip = root.querySelector('[data-action="drag-grip"]');
if (grip) {
grip.addEventListener('mousedown', e => {
if (e.button !== 0) return;
e.preventDefault();
const startX = e.clientX;
const startY = e.clientY;
const { left: startLeft, top: startTop } = this.position;
const onMove = mv => this.setPosition({
left: startLeft + (mv.clientX - startX),
top: startTop + (mv.clientY - startY),
});
const onUp = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
}
// Story 3.2: Append ScenePresetPanel to DOM and refresh
this._appendPresetPanel(root);
@@ -542,8 +569,8 @@ export class DirectorsBoard extends _AppBase {
const localize = (key) => game.i18n?.localize(key) ?? key;
const getBinding = (actionKey) => {
// Check both namespaces due to migration from video-view-manager to scrying-pool
const namespaces = ['scrying-pool', 'video-view-manager'];
// Check both namespaces due to migration from scrying-pool to scrying-pool
const namespaces = ['scrying-pool', 'scrying-pool'];
for (const ns of namespaces) {
const bindings = game.keybindings?.bindings?.get(`${ns}.${actionKey}`);
if (bindings?.[0]) {
@@ -556,10 +583,10 @@ export class DirectorsBoard extends _AppBase {
};
const shortcuts = [
{ label: localize('video-view-manager.directorsBoard.shortcuts.openBoard'), binding: getBinding('openDirectorsBoard') ?? 'Ctrl+Shift+V' },
{ label: localize('video-view-manager.directorsBoard.shortcuts.showAll'), binding: getBinding('showAll') ?? 'Ctrl+Shift+S' },
{ label: localize('video-view-manager.directorsBoard.shortcuts.hideAll'), binding: getBinding('hideAll') ?? 'Ctrl+Shift+H' },
{ label: localize('video-view-manager.directorsBoard.shortcuts.spotlight'), binding: getBinding('spotlightParticipant') ?? 'Ctrl+Shift+P' },
{ label: localize('scrying-pool.directorsBoard.shortcuts.openBoard'), binding: getBinding('openDirectorsBoard') ?? 'Ctrl+Shift+V' },
{ label: localize('scrying-pool.directorsBoard.shortcuts.showAll'), binding: getBinding('showAll') ?? 'Ctrl+Shift+S' },
{ label: localize('scrying-pool.directorsBoard.shortcuts.hideAll'), binding: getBinding('hideAll') ?? 'Ctrl+Shift+H' },
{ label: localize('scrying-pool.directorsBoard.shortcuts.spotlight'), binding: getBinding('spotlightParticipant') ?? 'Ctrl+Shift+P' },
];
// Escape HTML to prevent injection via localised strings or keybinding labels
@@ -570,7 +597,7 @@ export class DirectorsBoard extends _AppBase {
if (typeof Dialog !== 'undefined') {
new Dialog({
title: localize('video-view-manager.directorsBoard.shortcuts.title'),
title: localize('scrying-pool.directorsBoard.shortcuts.title'),
content,
buttons: { close: { label: 'Close' } },
default: 'close',
@@ -592,6 +619,48 @@ export class DirectorsBoard extends _AppBase {
}
}
/**
* Toggles Foundry A/V mode between AUDIO_VIDEO (3) and DISABLED (0).
* The module is the single point of control for A/V activation — Foundry's
* native AVConfig dialog is redirected here by initScryingPoolCameraViews.
*
* Uses reestablish() rather than explicit connect()/disconnect() to avoid
* racing with Foundry's internal mode-change listeners (AVMaster hooks into
* the settings change event itself).
*/
async _onToggleAVMode() {
if (!game.webrtc) {
console.warn('[ScryingPool] DirectorsBoard: game.webrtc not available');
return;
}
const currentMode = game.webrtc.settings?.world?.mode ?? 0;
// AV_MODES: DISABLED=0, AUDIO=1, VIDEO=2, AUDIO_VIDEO=3
const newMode = currentMode === 0 ? 3 : 0;
try {
await game.webrtc.settings.set('world', 'mode', newMode);
// reestablish() tears down and rebuilds the WebRTC connection honouring the
// new mode — same approach used by Foundry's own AVConfig save handler.
await game.webrtc.reestablish?.();
} catch (err) {
console.error('[ScryingPool] DirectorsBoard: failed to toggle A/V mode:', err);
}
if (this.rendered) this.render({ force: true });
}
/**
* Opens Foundry's native AVConfig dialog for signaling server configuration.
* This is separate from the A/V mode toggle — AVConfig is where you set up the
* LiveKit/WebRTC server address, username, password, etc.
* The module controls A/V mode (on/off); Foundry's dialog handles infrastructure.
*/
_onOpenAVConfig() {
if (!game.webrtc?.config) {
console.warn('[ScryingPool] DirectorsBoard: game.webrtc.config not available');
return;
}
game.webrtc.config.render({ force: true });
}
/**
* Opens the PresetSaveDialog for saving the current visibility matrix as a preset.
*/
@@ -749,7 +818,7 @@ export class DirectorsBoard extends _AppBase {
*/
_savePosition(state) {
try {
game.user?.setFlag('video-view-manager', 'directorsBoardState', state);
game.user?.setFlag('scrying-pool', 'directorsBoardState', state);
} catch (err) {
console.error('[ScryingPool] Failed to save directors board position:', err);
}
+3 -1
View File
@@ -190,7 +190,9 @@ export class GMPlayerPrivacySelectorMenu extends _AppBase {
const id = escapeHtml(user.id ?? '');
const role = user.isGM ? 'GM' : 'Player';
return `
<div class="sp-user-item" data-user-id="${id}">
<div class="sp-user-item"
data-user-id="${id}"
data-tooltip="${name}${role}">
<span class="sp-user-name">${name}</span>
<span class="sp-user-role">${role}</span>
</div>
+1 -1
View File
@@ -77,7 +77,7 @@ export class PresetExportDialog extends _AppBase {
static PARTS = {
dialog: {
template: 'modules/video-view-manager/templates/preset-export.hbs',
template: 'modules/scrying-pool/templates/preset-export.hbs',
},
};
+1 -1
View File
@@ -92,7 +92,7 @@ export class PresetImportDialog extends _AppBase {
static PARTS = {
dialog: {
template: 'modules/video-view-manager/templates/preset-import.hbs',
template: 'modules/scrying-pool/templates/preset-import.hbs',
},
};
+6 -6
View File
@@ -40,7 +40,7 @@ export class PresetLoadDialog extends _AppBase {
static PARTS = {
dialog: {
template: 'modules/video-view-manager/templates/preset-load-dialog.hbs',
template: 'modules/scrying-pool/templates/preset-load-dialog.hbs',
},
};
@@ -80,10 +80,10 @@ export class PresetLoadDialog extends _AppBase {
return {
presets: this._presets,
hasPresets: this._presets.length > 0,
loadLabel: i18n.localize('video-view-manager.presets.load.loadButton'),
cancelLabel: i18n.localize('video-view-manager.presets.load.cancelButton'),
title: i18n.localize('video-view-manager.presets.load.title'),
emptyMessage: i18n.localize('video-view-manager.presets.load.emptyMessage'),
loadLabel: i18n.localize('scrying-pool.presets.load.loadButton'),
cancelLabel: i18n.localize('scrying-pool.presets.load.cancelButton'),
title: i18n.localize('scrying-pool.presets.load.title'),
emptyMessage: i18n.localize('scrying-pool.presets.load.emptyMessage'),
};
}
@@ -157,7 +157,7 @@ export class PresetLoadDialog extends _AppBase {
// Show success notification
this._adapter.notifications.info(
this._adapter.i18n.localize('video-view-manager.presets.notifications.applied')
this._adapter.i18n.localize('scrying-pool.presets.notifications.applied')
.replace('{name}', presetName)
);
+9 -8
View File
@@ -35,12 +35,12 @@ export class PresetSaveDialog extends _AppBase {
id: 'scrying-pool-preset-save-dialog',
classes: ['scrying-pool', 'preset-save-dialog'],
window: { title: 'Save Scene Preset', resizable: false },
position: { width: 320, height: 'auto' },
position: { width: 360, height: 'auto' },
};
static PARTS = {
dialog: {
template: 'modules/video-view-manager/templates/preset-save-dialog.hbs',
template: 'modules/scrying-pool/templates/preset-save-dialog.hbs',
},
};
@@ -76,11 +76,12 @@ export class PresetSaveDialog extends _AppBase {
return {
defaultName: '',
saveLabel: i18n.localize('video-view-manager.presets.save.saveButton'),
cancelLabel: i18n.localize('video-view-manager.presets.save.cancelButton'),
title: i18n.localize('video-view-manager.presets.save.title'),
nameLabel: i18n.localize('video-view-manager.presets.save.nameLabel'),
namePlaceholder: i18n.localize('video-view-manager.presets.save.namePlaceholder'),
saveLabel: i18n.localize('scrying-pool.presets.save.saveButton'),
cancelLabel: i18n.localize('scrying-pool.presets.save.cancelButton'),
title: i18n.localize('scrying-pool.presets.save.title'),
nameLabel: i18n.localize('scrying-pool.presets.save.nameLabel'),
namePlaceholder: i18n.localize('scrying-pool.presets.save.namePlaceholder'),
descriptionHint: i18n.localize('scrying-pool.presets.save.descriptionHint'),
};
}
@@ -173,7 +174,7 @@ export class PresetSaveDialog extends _AppBase {
// Show success notification
this._adapter.notifications.info(
this._adapter.i18n.localize('video-view-manager.presets.notifications.saved')
this._adapter.i18n.localize('scrying-pool.presets.notifications.saved')
.replace('{name}', name)
);
+15 -15
View File
@@ -66,7 +66,7 @@ export class ScenePresetPanel {
this._element = document.createElement('div');
this._element.className = 'directors-board__preset-panel';
this._element.setAttribute('role', 'region');
this._element.setAttribute('aria-label', this._adapter.i18n.localize('video-view-manager.scenePresetPanel.title'));
this._element.setAttribute('aria-label', this._adapter.i18n.localize('scrying-pool.scenePresetPanel.title'));
this._element.setAttribute('aria-expanded', 'false');
// Initially hidden
@@ -158,11 +158,11 @@ export class ScenePresetPanel {
* @private
*/
_buildEmptyHtml() {
const message = this._adapter.i18n.localize('video-view-manager.scenePresetPanel.noScene');
const message = this._adapter.i18n.localize('scrying-pool.scenePresetPanel.noScene');
return `
<div class="directors-board__preset-panel-header">
<h3 class="directors-board__preset-panel-title">
${this._escapeHtml(this._adapter.i18n.localize('video-view-manager.scenePresetPanel.title'))}
${this._escapeHtml(this._adapter.i18n.localize('scrying-pool.scenePresetPanel.title'))}
</h3>
</div>
<p class="directors-board__preset-panel-message">${this._escapeHtml(message)}</p>
@@ -195,14 +195,14 @@ export class ScenePresetPanel {
// Add default option
const defaultOption = `
<option value="" ${!presetName ? 'selected' : ''}>
${this._escapeHtml(localize('video-view-manager.scenePresetPanel.selectPreset'))}
${this._escapeHtml(localize('scrying-pool.scenePresetPanel.selectPreset'))}
</option>
`;
return `
<div class="directors-board__preset-panel-header">
<h3 class="directors-board__preset-panel-title">
${this._escapeHtml(localize('video-view-manager.scenePresetPanel.title'))}
${this._escapeHtml(localize('scrying-pool.scenePresetPanel.title'))}
</h3>
</div>
@@ -214,18 +214,18 @@ export class ScenePresetPanel {
data-action="toggle-auto-apply"
${enabled ? 'checked' : ''}
role="switch"
aria-label="${this._escapeHtml(localize('video-view-manager.scenePresetPanel.enableAutoApply'))}">
${this._escapeHtml(localize('video-view-manager.scenePresetPanel.enableAutoApply'))}
aria-label="${this._escapeHtml(localize('scrying-pool.scenePresetPanel.enableAutoApply'))}">
${this._escapeHtml(localize('scrying-pool.scenePresetPanel.enableAutoApply'))}
</label>
</div>
<div class="directors-board__preset-panel-row">
<label class="directors-board__preset-panel-label">
${this._escapeHtml(localize('video-view-manager.scenePresetPanel.preset'))}
${this._escapeHtml(localize('scrying-pool.scenePresetPanel.preset'))}
<select class="directors-board__preset-panel-select"
data-action="select-preset"
${!presets.length ? 'disabled' : ''}
aria-label="${this._escapeHtml(localize('video-view-manager.scenePresetPanel.selectPreset'))}">
aria-label="${this._escapeHtml(localize('scrying-pool.scenePresetPanel.selectPreset'))}">
${defaultOption}
${presetOptions}
</select>
@@ -234,7 +234,7 @@ export class ScenePresetPanel {
<div class="directors-board__preset-panel-row">
<label class="directors-board__preset-panel-label">
${this._escapeHtml(localize('video-view-manager.scenePresetPanel.preDelay'))}
${this._escapeHtml(localize('scrying-pool.scenePresetPanel.preDelay'))}
<span class="directors-board__preset-panel-delay-value">${preDelay}ms</span>
<input type="range"
class="directors-board__preset-panel-slider"
@@ -243,7 +243,7 @@ export class ScenePresetPanel {
max="${this._MAX_PREDELAY}"
value="${preDelay}"
step="100"
aria-label="${this._escapeHtml(localize('video-view-manager.scenePresetPanel.preDelay'))}"
aria-label="${this._escapeHtml(localize('scrying-pool.scenePresetPanel.preDelay'))}"
aria-valuemin="${this._MIN_PREDELAY}"
aria-valuemax="${this._MAX_PREDELAY}"
aria-valuenow="${preDelay}">
@@ -251,7 +251,7 @@ export class ScenePresetPanel {
</div>
<div class="directors-board__preset-panel-row directors-board__preset-panel-row--hint">
<span>${this._escapeHtml(localize('video-view-manager.scenePresetPanel.globalSettingsHint'))}</span>
<span>${this._escapeHtml(localize('scrying-pool.scenePresetPanel.globalSettingsHint'))}</span>
</div>
</div>
`;
@@ -338,8 +338,8 @@ export class ScenePresetPanel {
// Notify
this._adapter.notifications.info(
isChecked
? this._adapter.i18n.localize('video-view-manager.scenePresetPanel.notifications.enabled')
: this._adapter.i18n.localize('video-view-manager.scenePresetPanel.notifications.disabled')
? this._adapter.i18n.localize('scrying-pool.scenePresetPanel.notifications.enabled')
: this._adapter.i18n.localize('scrying-pool.scenePresetPanel.notifications.disabled')
);
} catch (err) {
console.error('[ScryingPool] ScenePresetPanel: failed to toggle auto-apply', err);
@@ -373,7 +373,7 @@ export class ScenePresetPanel {
// Notify
if (presetName) {
this._adapter.notifications.info(
this._adapter.i18n.localize('video-view-manager.scenePresetPanel.notifications.presetSelected')
this._adapter.i18n.localize('scrying-pool.scenePresetPanel.notifications.presetSelected')
.replace('{name}', presetName)
);
}
+53 -8
View File
@@ -96,7 +96,7 @@ export class ScryingPoolStrip extends _AppBase {
: super.defaultOptions ?? {};
return Object.assign({}, base, {
id: 'scrying-pool-strip',
template: 'modules/video-view-manager/templates/roster-strip.hbs',
template: 'modules/scrying-pool/templates/roster-strip.hbs',
popOut: true,
resizable: false,
title: 'Scrying Pool',
@@ -140,7 +140,7 @@ export class ScryingPoolStrip extends _AppBase {
getData() {
const savedState =
typeof game !== 'undefined'
? game.user?.getFlag?.('video-view-manager', 'stripState')
? game.user?.getFlag?.('scrying-pool', 'stripState')
: null;
if (savedState?.expanded !== undefined) {
this._isExpanded = savedState.expanded;
@@ -148,17 +148,24 @@ export class ScryingPoolStrip extends _AppBase {
const showFirstOpenTip =
typeof game !== 'undefined' &&
!game.user?.getFlag?.('video-view-manager', 'firstStripOpen');
!game.user?.getFlag?.('scrying-pool', 'firstStripOpen');
const userIds = this._adapter.users.all
? this._adapter.users.all().map(u => u.id)
: [];
// Respect the showGMSelfFeed setting: if false, exclude the current GM user
const showGMSelfFeed = this._adapter.settings?.get?.('showGMSelfFeed') ?? true;
const currentUserId = this._adapter.users.current?.()?.id;
const filteredUserIds = showGMSelfFeed
? userIds
: userIds.filter(id => id !== currentUserId);
// Check if we have stream access for video replacement (full AV replacement mode)
const hasStreamAccess = this._adapter.webrtc?.getMediaStreamForUser !== undefined;
const participants = buildParticipantList(
userIds,
filteredUserIds,
this._stateStore,
this._controller,
this._adapter,
@@ -208,18 +215,56 @@ export class ScryingPoolStrip extends _AppBase {
});
}
// Custom close button (replaces Foundry window header close)
const closeBtn = el.querySelector('[data-action="close-strip"]');
if (closeBtn) {
closeBtn.addEventListener('click', () => this.close());
}
// Drag grip — custom drag implementation (Foundry v14 ApplicationV1 does not expose its drag handler)
const grip = el.querySelector('[data-action="drag-grip"]');
if (grip) {
grip.addEventListener('mousedown', e => {
if (e.button !== 0) return;
e.preventDefault();
const startX = e.clientX;
const startY = e.clientY;
const { left: startLeft, top: startTop } = this.position;
const onMove = mv => {
this.setPosition({
left: startLeft + (mv.clientX - startX),
top: startTop + (mv.clientY - startY),
});
};
const onUp = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
}
// First open tip: set flag so it doesn't show again
const isFirstOpen =
typeof game !== 'undefined' &&
!game.user?.getFlag?.('video-view-manager', 'firstStripOpen');
!game.user?.getFlag?.('scrying-pool', 'firstStripOpen');
if (isFirstOpen) {
game.user?.setFlag?.('video-view-manager', 'firstStripOpen', true);
game.user?.setFlag?.('scrying-pool', 'firstStripOpen', true);
}
// Attach video streams if we have stream access (full AV replacement mode)
if (this._adapter.webrtc?.getMediaStreamForUser !== undefined) {
this._attachVideoStreams(el);
}
// Sync the outer Application window width with the expanded/collapsed state.
// The LESS max-width only applies to the inner template div (.scrying-pool.scrying-pool-strip);
// the outer window must be explicitly resized so it doesn't clip the expanded content.
if (typeof this.setPosition === 'function') {
this.setPosition({ width: this._isExpanded ? 240 : 44, height: 'auto' });
}
}
/** @inheritdoc */
@@ -233,7 +278,7 @@ export class ScryingPoolStrip extends _AppBase {
this._cleanupVideoStreams();
if (typeof game !== 'undefined') {
game.user?.setFlag?.('video-view-manager', 'stripState', {
game.user?.setFlag?.('scrying-pool', 'stripState', {
left: this.position?.left,
top: this.position?.top,
open: false,
@@ -267,7 +312,7 @@ export class ScryingPoolStrip extends _AppBase {
_toggleExpanded() {
this._isExpanded = !this._isExpanded;
if (typeof game !== 'undefined') {
game.user?.setFlag?.('video-view-manager', 'stripState', {
game.user?.setFlag?.('scrying-pool', 'stripState', {
left: this.position?.left,
top: this.position?.top,
open: true,
+49 -17
View File
@@ -58,7 +58,7 @@ export class PlayerPrivacyPanel extends _AppBase {
static PARTS = {
dialog: {
template: 'modules/video-view-manager/templates/player-privacy-panel.hbs',
template: 'modules/scrying-pool/templates/player-privacy-panel.hbs',
},
};
@@ -157,13 +157,6 @@ export class PlayerPrivacyPanel extends _AppBase {
enabled: settings.reactionCamEnabled,
settingKey: 'reactionCamEnabled',
},
{
key: 'hpReactiveCamStyling',
label: i18n.localize('SCRYING_POOL.PrivacyPanel.hpReactiveCamStylingLabel'),
description: i18n.localize('SCRYING_POOL.PrivacyPanel.hpReactiveCamStylingDescription'),
enabled: settings.hpReactiveCamStylingEnabled,
settingKey: 'hpReactiveCamStylingEnabled',
},
],
// Toggle labels
@@ -187,19 +180,28 @@ export class PlayerPrivacyPanel extends _AppBase {
/**
* Sets up event handlers after rendering.
* @param {HTMLElement|JQuery|object} html - The dialog element, jQuery object, or plain object with querySelector.
* @param {HTMLElement|JQuery|object} context - ApplicationV2 context, jQuery object, or plain object with querySelector.
*/
_onRender(html) {
// Normalize html to element with querySelector
// FoundryVTT ApplicationV2 passes jQuery object, tests pass plain objects
const element = html instanceof HTMLElement
? html
: (html?.[0] ?? html);
_onRender(context) {
// ApplicationV2 passes the template context as the first argument, not the element.
// Prefer this.element (the rendered root). Fall back to treating the argument as an element
// (for jQuery objects or test mocks that expose querySelector directly).
let element;
if (this.element instanceof HTMLElement) {
element = this.element;
} else if (context instanceof HTMLElement) {
element = context;
} else {
element = context?.[0] ?? context;
}
if (!element || typeof element.querySelector !== 'function') return;
// Cache toggle elements
this._reactionCamToggle = element.querySelector('[data-setting="reactionCamEnabled"]');
this._hpReactiveCamToggle = element.querySelector('[data-setting="hpReactiveCamStylingEnabled"]');
// Sync badge text with actual checkbox state (Handlebars renders from context which may
// be the default false state, while the real setting is already true in the DB).
this._syncToggleBadges(element);
// Story 4.2: Set up portrait section event handlers
this._setupPortraitHandlers(element);
@@ -208,6 +210,27 @@ export class PlayerPrivacyPanel extends _AppBase {
this._setupToggleHandlers(element);
}
/**
* Syncs toggle badge text/icon with current checkbox state.
* Needed because Handlebars renders from context at render time, which may not
* match the actual persisted setting if it was already enabled.
* @param {HTMLElement} element - The panel root element.
* @private
*/
_syncToggleBadges(element) {
const onLabel = this._adapter.i18n.localize('SCRYING_POOL.PrivacyPanel.toggleOn');
const offLabel = this._adapter.i18n.localize('SCRYING_POOL.PrivacyPanel.toggleOff');
const toggles = element.querySelectorAll('.player-privacy-panel__toggle-input');
for (const toggle of toggles) {
const span = toggle.nextElementSibling;
if (span?.classList.contains('player-privacy-panel__toggle-text')) {
span.innerHTML = toggle.checked
? `<i class="fas fa-check" aria-hidden="true"></i> ${onLabel}`
: `<i class="fas fa-times" aria-hidden="true"></i> ${offLabel}`;
}
}
}
/**
* Sets up event handlers for toggle switches.
* @param {HTMLElement} element - The dialog element.
@@ -428,6 +451,16 @@ export class PlayerPrivacyPanel extends _AppBase {
this._currentSettings[settingKey] = newValue;
}
// Update toggle badge text+icon dynamically
const span = checkbox.nextElementSibling;
if (span?.classList.contains('player-privacy-panel__toggle-text')) {
const onLabel = this._adapter.i18n.localize('SCRYING_POOL.PrivacyPanel.toggleOn');
const offLabel = this._adapter.i18n.localize('SCRYING_POOL.PrivacyPanel.toggleOff');
span.innerHTML = newValue
? `<i class="fas fa-check" aria-hidden="true"></i> ${onLabel}`
: `<i class="fas fa-times" aria-hidden="true"></i> ${offLabel}`;
}
// Show success notification
this._adapter.notifications.info(
this._adapter.i18n.localize('SCRYING_POOL.PrivacyPanel.savedNotification')
@@ -466,7 +499,6 @@ export class PlayerPrivacyPanel extends _AppBase {
_onClose() {
// Clear cached elements
this._reactionCamToggle = null;
this._hpReactiveCamToggle = null;
this._fileInput = null;
this._portraitPreview = null;
this._currentSettings = null;
+2 -2
View File
@@ -501,7 +501,7 @@ export class VisibilityBadge {
* @returns {boolean}
*/
_getFirstBadgeEncountered() {
return this._adapter.users.current()?.getFlag('video-view-manager', 'firstBadgeEncounter') ?? false;
return this._adapter.users.current()?.getFlag('scrying-pool', 'firstBadgeEncounter') ?? false;
}
/**
@@ -509,7 +509,7 @@ export class VisibilityBadge {
* @returns {Promise<void>}
*/
async _setFirstBadgeEncountered() {
await this._adapter.users.current()?.setFlag('video-view-manager', 'firstBadgeEncounter', true);
await this._adapter.users.current()?.setFlag('scrying-pool', 'firstBadgeEncounter', true);
}
/**
+90
View File
@@ -0,0 +1,90 @@
/**
* ScryingPoolCameraViews — replaces Foundry's CameraViews as CONFIG.ui.webrtc.
*
* Two responsibilities:
* 1. Redirect the "configure" camera action to the Scrying Pool Directors Board
* instead of Foundry's native AVConfig dialog.
* 2. Inject the Scrying Pool visibility state (sp-cam-hidden) into each user's
* camera context so the dock reflects the same hidden/active state as the
* module's state machine.
*
* Set as CONFIG.ui.webrtc in the 'init' hook (before Foundry instantiates ui.webrtc).
* Dependencies are injected after 'ready' via initScryingPoolCameraViews().
*/
// Lazy base class — avoids ReferenceError at module load time in tests where
// foundry globals are not defined.
function _getCameraViewsBase() {
if (typeof foundry !== 'undefined') {
const cls = foundry.applications?.apps?.av?.CameraViews;
if (cls) return cls;
}
// Minimal test-environment fallback — mirrors the pattern in DirectorsBoard.js
return class _FallbackCameraViews {
static DEFAULT_OPTIONS = {};
static PARTS = {};
constructor(options = {}) { this.options = options; }
async render() {}
async close() {}
_prepareUserContext(_id) { return {}; }
_onConfigure() {}
};
}
/** @type {object|null} DirectorsBoard instance — set via initScryingPoolCameraViews */
let _directorsBoard = null;
/** @type {object|null} StateStore instance — set via initScryingPoolCameraViews */
let _stateStore = null;
/**
* Inject module dependencies. Called from module.js after 'ready' resolves.
* @param {object|null} directorsBoard - The singleton DirectorsBoard (GM only, else null)
* @param {object} stateStore - The module StateStore
*/
export function initScryingPoolCameraViews(directorsBoard, stateStore) {
_directorsBoard = directorsBoard;
_stateStore = stateStore;
}
export class ScryingPoolCameraViews extends _getCameraViewsBase() {
/**
* Intercept the configure camera button.
* Opens the Scrying Pool Directors Board instead of Foundry's AVConfig dialog.
* For non-GM players, shows an informational notification since A/V config
* is GM-controlled in this module.
* @override
*/
_onConfigure(event, target) {
if (_directorsBoard) {
_directorsBoard.render({ force: true });
} else if (typeof ui !== 'undefined') {
ui.notifications?.info(
game?.i18n?.localize('scrying-pool.notifications.avConfigGMOnly') ??
'A/V settings are managed by the GM.'
);
}
}
/**
* Inject Scrying Pool visibility state into each user's camera tile context.
* Adds the 'sp-cam-hidden' CSS class when the SP state machine considers
* this user hidden, allowing the dock to visually reflect module state.
* @override
* @param {string} id - User ID
* @returns {object|undefined}
*/
_prepareUserContext(id) {
const ctx = super._prepareUserContext(id);
if (!ctx) return ctx;
const spState = _stateStore?.getState?.(id) ?? 'active';
const spHidden = spState === 'hidden';
if (spHidden) {
ctx.css = [ctx.css, 'sp-cam-hidden'].filter(Boolean).join(' ');
}
return ctx;
}
}