Story 4.1: Tasks 3-6 Complete - Director's Board Integration & Settings Menu

- Task 3: Extended FoundryAdapter with user flag access methods
  - Added getFlag(userId, scope, key) method
  - Added setFlag(userId, scope, key, value) method
  - Added getFlagModule(userId, key) convenience method
  - Added setFlagModule(userId, key, value) convenience method

- Task 4: Integrated Privacy Settings with Director's Board
  - Updated participant-card.hbs to show Reaction Cam badge
  - Modified boardUtils.js to pass playerPrivacyManager through context
  - Updated DirectorsBoard to accept and pass playerPrivacyManager
  - Added CSS styles for Reaction Cam badge (SP accent color)

- Task 5: Registered PlayerPrivacyPanel in module settings
  - Added settings menu registration in module.js Hooks.once('ready')
  - Available to all users (restricted: false)
  - Uses localized labels and hints

- Task 6: Added all localization strings
  - Added SCRYING_POOL.PrivacyPanel.* strings for panel UI
  - Added SCRYING_POOL.Settings.* strings for settings menu

- Updated story file with task completion status

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
2026-05-23 21:29:58 +02:00
parent 61f362004e
commit fd0a7868f3
13 changed files with 1049 additions and 47 deletions
+4 -2
View File
@@ -58,14 +58,16 @@ export class DirectorsBoard extends _AppBase {
* @param {object} controller
* @param {object} adapter
* @param {import('../core/ScenePresetManager.js').ScenePresetManager} scenePresetManager
* @param {import('../../core/PlayerPrivacyManager.js').PlayerPrivacyManager} playerPrivacyManager
* @param {object} [options]
*/
constructor(stateStore, controller, adapter, scenePresetManager, options = {}) {
constructor(stateStore, controller, adapter, scenePresetManager, playerPrivacyManager, options = {}) {
super(options);
this._stateStore = stateStore;
this._controller = controller;
this._adapter = adapter;
this._scenePresetManager = scenePresetManager;
this._playerPrivacyManager = playerPrivacyManager;
this._hookId = null;
/** @type {Map<string, string>|null} Pre-bulk-action snapshot for single-step undo */
this._undoSnapshot = null;
@@ -337,7 +339,7 @@ export class DirectorsBoard extends _AppBase {
/** @inheritdoc */
async _prepareContext() {
const base = buildBoardContext(this._stateStore, this._controller, this._adapter);
const base = buildBoardContext(this._stateStore, this._controller, this._adapter, this._playerPrivacyManager);
const presetCount = this._scenePresetManager?.list?.().length ?? 0;
// Get auto-apply config for current scene (Story 3.2)
+258
View File
@@ -0,0 +1,258 @@
// @ts-nocheck
// Conditional base class — test environment lacks foundry globals.
// At module load time in tests, foundry is undefined → fallback class is used.
/** @private */
const _AppBase =
typeof foundry !== 'undefined' &&
foundry.applications?.api?.HandlebarsApplicationMixin &&
foundry.applications?.api?.ApplicationV2
? foundry.applications.api.HandlebarsApplicationMixin(
foundry.applications.api.ApplicationV2
)
: class _FallbackApp {
static DEFAULT_OPTIONS = {};
static PARTS = {};
get rendered() { return this._rendered ?? false; }
set rendered(v) { this._rendered = v; }
get element() { return this._element ?? null; }
set element(v) { this._element = v; }
async render() { this._rendered = true; }
async close() { this._rendered = false; }
async _prepareContext() { return {}; }
_onRender() {}
_onClose() {}
_onPosition() {}
};
/**
* Player Privacy Panel — UI component for players to control automation opt-ins.
*
* Extends ApplicationV2 via HandlebarsApplicationMixin.
* Displays privacy settings and allows toggling (for own user) or read-only view (for other users).
*
* Import rule: may import from src/core/, src/foundry/, src/contracts/.
* Constructors are side-effect free.
*
* @module ui/player/PlayerPrivacyPanel
*/
/**
* Player Privacy Panel dialog for managing automation opt-in settings.
*/
export class PlayerPrivacyPanel extends _AppBase {
static DEFAULT_OPTIONS = {
id: 'scrying-pool-player-privacy-panel',
classes: ['scrying-pool', 'player-privacy-panel'],
window: {
title: 'Player Privacy Panel',
resizable: false,
width: 380,
height: 'auto',
},
position: {},
};
static PARTS = {
dialog: {
template: 'modules/video-view-manager/templates/player-privacy-panel.hbs',
},
};
/**
* @param {import('../../foundry/FoundryAdapter.js').FoundryAdapter} adapter
* Injected FoundryAdapter surface.
* @param {import('../../core/PlayerPrivacyManager.js').PlayerPrivacyManager} playerPrivacyManager
* Injected PlayerPrivacyManager for privacy settings operations.
* @param {string} targetUserId - The user ID whose settings are being viewed/edited.
* @param {object} [options]
* @throws {TypeError} If adapter or playerPrivacyManager is invalid.
*/
constructor(adapter, playerPrivacyManager, targetUserId, options = {}) {
// Validate dependencies
if (!adapter || typeof adapter !== 'object') {
throw new TypeError(
'PlayerPrivacyPanel: adapter argument is required and must be an object'
);
}
if (!playerPrivacyManager || typeof playerPrivacyManager !== 'object') {
throw new TypeError(
'PlayerPrivacyPanel: playerPrivacyManager argument is required and must be an object'
);
}
if (typeof targetUserId !== 'string' || targetUserId.length === 0) {
throw new TypeError(
'PlayerPrivacyPanel: targetUserId must be a non-empty string'
);
}
super(options);
this._adapter = adapter;
this._playerPrivacyManager = playerPrivacyManager;
this._targetUserId = targetUserId;
// Cache for DOM elements
/** @type {HTMLElement|null} */
this._reactionCamToggle = null;
/** @type {HTMLElement|null} */
this._hpReactiveCamToggle = null;
// Current settings state
/** @type {import('../../contracts/privacy-settings.js').PrivacySettings|null} */
this._currentSettings = null;
}
/**
* Prepares the template context with i18n labels and current settings.
* @returns {Promise<object>} Template context.
*/
async _prepareContext() {
const i18n = this._adapter.i18n;
// Get current settings for the target user
const settings = this._playerPrivacyManager.getSettings(this._targetUserId);
this._currentSettings = settings;
// Determine if this is the current user (editable) or another user (read-only)
const currentUserId = this._adapter.users.current?.()?.id;
const isOwnUser = this._targetUserId === currentUserId;
const isReadOnly = !isOwnUser;
return {
// Panel metadata
title: i18n.localize('SCRYING_POOL.PrivacyPanel.title'),
sectionHeader: i18n.localize('SCRYING_POOL.PrivacyPanel.sectionHeader'),
readOnlyNotice: i18n.localize('SCRYING_POOL.PrivacyPanel.readOnlyNotice'),
// Automation effects
automationEffects: [
{
key: 'reactionCam',
label: i18n.localize('SCRYING_POOL.PrivacyPanel.reactionCamLabel'),
description: i18n.localize('SCRYING_POOL.PrivacyPanel.reactionCamDescription'),
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
toggleOnLabel: i18n.localize('SCRYING_POOL.PrivacyPanel.toggleOn'),
toggleOffLabel: i18n.localize('SCRYING_POOL.PrivacyPanel.toggleOff'),
// State
isReadOnly,
isOwnUser,
};
}
/**
* Sets up event handlers after rendering.
* @param {HTMLElement} element - The dialog element.
*/
_onRender(element) {
// Cache toggle elements
this._reactionCamToggle = element.querySelector('[data-setting="reactionCamEnabled"]');
this._hpReactiveCamToggle = element.querySelector('[data-setting="hpReactiveCamStylingEnabled"]');
// Set up toggle change handlers
this._setupToggleHandlers(element);
}
/**
* Sets up event handlers for toggle switches.
* @param {HTMLElement} element - The dialog element.
*/
_setupToggleHandlers(element) {
const toggles = element.querySelectorAll('.player-privacy-panel__toggle input[type="checkbox"]');
for (const toggle of toggles) {
toggle.addEventListener('change', (event) => this._onToggleChange(event));
}
}
/**
* Handles toggle change events.
* @param {Event} event - The change event from the checkbox.
*/
async _onToggleChange(event) {
const checkbox = event.target;
if (!checkbox || checkbox.type !== 'checkbox') return;
const settingKey = checkbox.getAttribute('data-setting');
const newValue = checkbox.checked;
// Don't allow changes in read-only mode
if (this._isReadOnlyMode()) {
checkbox.checked = !newValue; // Revert the change
return;
}
if (!settingKey) {
console.warn('[ScryingPool] PlayerPrivacyPanel: toggle missing data-setting attribute');
return;
}
try {
// Update the setting via PlayerPrivacyManager
await this._playerPrivacyManager.setSetting(
this._targetUserId,
settingKey,
newValue
);
// Update our cached settings
if (this._currentSettings) {
this._currentSettings[settingKey] = newValue;
}
// Show success notification
this._adapter.notifications.info(
this._adapter.i18n.localize('SCRYING_POOL.PrivacyPanel.savedNotification')
);
} catch (err) {
// Revert the checkbox if the update failed
checkbox.checked = !newValue;
// Log error
console.error('[ScryingPool] PlayerPrivacyPanel: failed to update setting:', err);
// Show error notification
if (err instanceof TypeError) {
this._adapter.notifications.error(err.message);
} else {
this._adapter.notifications.error(
this._adapter.i18n.localize('SCRYING_POOL.PrivacyPanel.saveError')
);
}
}
}
/**
* Checks if the panel is in read-only mode.
* @returns {boolean} True if in read-only mode.
* @private
*/
_isReadOnlyMode() {
const currentUserId = this._adapter.users.current?.()?.id;
return this._targetUserId !== currentUserId;
}
/**
* Cleans up event listeners.
*/
_onClose() {
// Clear cached elements
this._reactionCamToggle = null;
this._hpReactiveCamToggle = null;
this._currentSettings = null;
}
}