@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user