+53
-29
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"video-view-manager": {
|
"scrying-pool": {
|
||||||
"badge": {
|
"badge": {
|
||||||
"state": {
|
"state": {
|
||||||
"hidden": "Hidden from table",
|
"hidden": "Hidden from table",
|
||||||
@@ -26,9 +26,14 @@
|
|||||||
"gmHid": "GM hid {name}'s camera",
|
"gmHid": "GM hid {name}'s camera",
|
||||||
"gmShowed": "GM showed {name}'s camera",
|
"gmShowed": "GM showed {name}'s camera",
|
||||||
"personalHidden": "GM has hidden your camera. Your portrait is shown to other Participants.",
|
"personalHidden": "GM has hidden your camera. Your portrait is shown to other Participants.",
|
||||||
"personalShowed": "Your camera is now visible to the table."
|
"personalShowed": "Your camera is now visible to the table.",
|
||||||
|
"avConfigGMOnly": "A/V settings are managed by the GM."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"showGMSelfFeed": {
|
||||||
|
"label": "Show GM Self Feed",
|
||||||
|
"hint": "When enabled, the GM's own camera feed is shown in the Scrying Pool strip."
|
||||||
|
},
|
||||||
"notificationVerbosity": {
|
"notificationVerbosity": {
|
||||||
"label": "Notification Verbosity",
|
"label": "Notification Verbosity",
|
||||||
"hint": "Controls which camera-state notifications you see. 'All' shows every change; 'GM Only' shows changes only to the GM and affected participant; 'Silent' suppresses all notifications except your own camera changes.",
|
"hint": "Controls which camera-state notifications you see. 'All' shows every change; 'GM Only' shows changes only to the GM and affected participant; 'Silent' suppresses all notifications except your own camera changes.",
|
||||||
@@ -41,13 +46,19 @@
|
|||||||
},
|
},
|
||||||
"directorsBoard": {
|
"directorsBoard": {
|
||||||
"title": "Director's Board",
|
"title": "Director's Board",
|
||||||
|
"close": "Close",
|
||||||
"empty": "No participants connected.",
|
"empty": "No participants connected.",
|
||||||
"openButton": "Open Director's Board",
|
"openButton": "Open Director's Board",
|
||||||
"footer": {
|
"footer": {
|
||||||
"savePreset": "Save Preset…",
|
"savePreset": "Save Layout",
|
||||||
"loadPreset": "Load Preset…",
|
"loadPreset": "Load Layout",
|
||||||
"exportPresets": "Export Presets…",
|
"exportPresets": "Export",
|
||||||
"importPresets": "Import Presets…"
|
"importPresets": "Import",
|
||||||
|
"autoApplySettings": "Auto-Apply…",
|
||||||
|
"avModeEnable": "Enable A/V",
|
||||||
|
"avModeDisable": "Disable A/V",
|
||||||
|
"avConfig": "A/V Settings…",
|
||||||
|
"avConfigTitle": "Open Foundry A/V server settings (signaling server, LiveKit, etc.)"
|
||||||
},
|
},
|
||||||
"bulk": {
|
"bulk": {
|
||||||
"showAll": "Show All",
|
"showAll": "Show All",
|
||||||
@@ -81,46 +92,61 @@
|
|||||||
},
|
},
|
||||||
"presets": {
|
"presets": {
|
||||||
"save": {
|
"save": {
|
||||||
"saveButton": "Save",
|
"saveButton": "Save Layout",
|
||||||
"cancelButton": "Cancel",
|
"cancelButton": "Cancel",
|
||||||
"title": "Save Scene Preset",
|
"title": "Save Camera Layout",
|
||||||
"nameLabel": "Preset Name",
|
"nameLabel": "Layout Name",
|
||||||
"namePlaceholder": "Enter a name for this camera layout"
|
"namePlaceholder": "e.g. Combat, Roleplay, Intro…",
|
||||||
|
"descriptionHint": "Saves the current camera visibility layout for all participants."
|
||||||
},
|
},
|
||||||
"load": {
|
"load": {
|
||||||
"loadButton": "Load",
|
"loadButton": "Load",
|
||||||
"cancelButton": "Cancel",
|
"cancelButton": "Cancel",
|
||||||
"title": "Load Scene Preset",
|
"title": "Load Camera Layout",
|
||||||
"emptyMessage": "No presets saved yet. Use 'Save Preset' to create one."
|
"emptyMessage": "No layouts saved yet. Use 'Save Layout' to create one."
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"saved": "Scene preset '{name}' saved.",
|
"saved": "Layout '{name}' saved.",
|
||||||
"applied": "Scene preset '{name}' applied.",
|
"applied": "Layout '{name}' applied.",
|
||||||
"scene-applied": "Scene changed: camera layout updated"
|
"scene-applied": "Scene changed: camera layout updated"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"scenePresetPanel": {
|
||||||
|
"title": "Auto-Apply Settings",
|
||||||
|
"enableAutoApply": "Enable Auto-Apply",
|
||||||
|
"preset": "Layout",
|
||||||
|
"selectPreset": "Select a layout…",
|
||||||
|
"preDelay": "Pre-Delay",
|
||||||
|
"globalSettingsHint": "Auto-apply can also be toggled globally in module settings.",
|
||||||
|
"noScene": "No scene is currently active.",
|
||||||
|
"notifications": {
|
||||||
|
"enabled": "Auto-Apply enabled for this scene.",
|
||||||
|
"disabled": "Auto-Apply disabled for this scene.",
|
||||||
|
"presetSelected": "Auto-Apply layout set to: {name}"
|
||||||
|
}
|
||||||
|
},
|
||||||
"presetExport": {
|
"presetExport": {
|
||||||
"title": "Export Scene Presets",
|
"title": "Export Camera Layouts",
|
||||||
"description": "Download all scene presets as a JSON file that can be imported into another world.",
|
"description": "Download all camera layouts as a JSON file that can be imported into another world.",
|
||||||
"scene": "Scene",
|
"scene": "Scene",
|
||||||
"presetCount": "Presets",
|
"presetCount": "Layouts",
|
||||||
"filename": "Filename",
|
"filename": "Filename",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"exporting": "Exporting…",
|
"exporting": "Exporting…",
|
||||||
"exportSuccess": "Scene presets exported successfully.",
|
"exportSuccess": "Camera layouts exported successfully.",
|
||||||
"exportFailed": "Failed to export presets"
|
"exportFailed": "Failed to export layouts"
|
||||||
},
|
},
|
||||||
"presetImport": {
|
"presetImport": {
|
||||||
"title": "Import Scene Presets",
|
"title": "Import Camera Layouts",
|
||||||
"description": "Upload a JSON file containing scene presets to add to this scene.",
|
"description": "Upload a JSON file containing camera layouts to add to this scene.",
|
||||||
"selectFile": "Select File",
|
"selectFile": "Select File",
|
||||||
"chooseFile": "Choose a JSON file…",
|
"chooseFile": "Choose a JSON file…",
|
||||||
"importMode": "Import Mode",
|
"importMode": "Import Mode",
|
||||||
"importModeMerge": "Merge",
|
"importModeMerge": "Merge",
|
||||||
"importModeReplace": "Replace",
|
"importModeReplace": "Replace",
|
||||||
"importModeMergeHint": "Add new presets, skip duplicates",
|
"importModeMergeHint": "Add new layouts, skip duplicates",
|
||||||
"importModeReplaceHint": "Delete all existing presets and import new ones",
|
"importModeReplaceHint": "Delete all existing layouts and import new ones",
|
||||||
"previewTitle": "Preview",
|
"previewTitle": "Preview",
|
||||||
"previewWillImport": "Will import",
|
"previewWillImport": "Will import",
|
||||||
"previewWillSkip": "Will skip (already exists)",
|
"previewWillSkip": "Will skip (already exists)",
|
||||||
@@ -128,10 +154,10 @@
|
|||||||
"confirmReplace": "Replace All",
|
"confirmReplace": "Replace All",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"importing": "Importing…",
|
"importing": "Importing…",
|
||||||
"importFailed": "Failed to import presets",
|
"importFailed": "Failed to import layouts",
|
||||||
"selectFileFirst": "Please select a file first",
|
"selectFileFirst": "Please select a file first",
|
||||||
"existingPresetsWarning": "This scene has {existingPresetCount} existing preset(s).",
|
"existingPresetsWarning": "This scene has {existingPresetCount} existing layout(s).",
|
||||||
"replaceConfirmation": "This will delete all {existingPresetCount} existing preset(s) and replace them with the imported ones. This cannot be undone."
|
"replaceConfirmation": "This will delete all {existingPresetCount} existing layout(s) and replace them with the imported ones. This cannot be undone."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SCRYING_POOL": {
|
"SCRYING_POOL": {
|
||||||
@@ -143,8 +169,6 @@
|
|||||||
"sectionDescription": "Control which automation features can affect your camera and on-screen presence.",
|
"sectionDescription": "Control which automation features can affect your camera and on-screen presence.",
|
||||||
"reactionCamLabel": "Reaction Cam",
|
"reactionCamLabel": "Reaction Cam",
|
||||||
"reactionCamDescription": "Automatically show your camera during key moments (combat, rolls, etc.)",
|
"reactionCamDescription": "Automatically show your camera during key moments (combat, rolls, etc.)",
|
||||||
"hpReactiveCamStylingLabel": "HP-Reactive Cam Styling",
|
|
||||||
"hpReactiveCamStylingDescription": "Apply visual styling to your camera based on your character's HP",
|
|
||||||
"toggleOn": "Enabled",
|
"toggleOn": "Enabled",
|
||||||
"toggleOff": "Disabled",
|
"toggleOff": "Disabled",
|
||||||
"readOnlyNotice": "This player's privacy settings are read-only. You cannot modify another player's consent preferences.",
|
"readOnlyNotice": "This player's privacy settings are read-only. You cannot modify another player's consent preferences.",
|
||||||
@@ -164,7 +188,7 @@
|
|||||||
"Settings": {
|
"Settings": {
|
||||||
"PlayerPrivacyPanel": "Player Privacy Panel",
|
"PlayerPrivacyPanel": "Player Privacy Panel",
|
||||||
"PlayerPrivacyPanelLabel": "Control automation effects for your camera",
|
"PlayerPrivacyPanelLabel": "Control automation effects for your camera",
|
||||||
"PlayerPrivacyPanelHint": "Opt in or out of Reaction Cam, HP-Reactive Cam Styling, and other automation features",
|
"PlayerPrivacyPanelHint": "Opt in or out of Reaction Cam and other automation features",
|
||||||
"GMPlayerPrivacySelector": "View Player Privacy Settings",
|
"GMPlayerPrivacySelector": "View Player Privacy Settings",
|
||||||
"GMPlayerPrivacySelectorLabel": "View and manage player privacy consent settings",
|
"GMPlayerPrivacySelectorLabel": "View and manage player privacy consent settings",
|
||||||
"GMPlayerPrivacySelectorHint": "Select a player to view their automation opt-in preferences (read-only)"
|
"GMPlayerPrivacySelectorHint": "Select a player to view their automation opt-in preferences (read-only)"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// @ts-nocheck — Module entry point with FoundryVTT globals, no exports needed
|
// @ts-nocheck — Module entry point with FoundryVTT globals, no exports needed
|
||||||
/* global Handlebars */
|
/* global Handlebars */
|
||||||
/**
|
/**
|
||||||
* module.js — Entry point and wiring diagram for Video View Manager (Scrying Pool).
|
* module.js — Entry point and wiring diagram for Scrying Pool.
|
||||||
*
|
*
|
||||||
* This file is the wiring diagram ONLY. It imports all modules, constructs them
|
* This file is the wiring diagram ONLY. It imports all modules, constructs them
|
||||||
* with injected dependencies, and holds NO business logic.
|
* with injected dependencies, and holds NO business logic.
|
||||||
@@ -34,6 +34,7 @@ import { DirectorsBoard } from './src/ui/gm/DirectorsBoard.js';
|
|||||||
import { ConfirmationBar } from './src/ui/gm/ConfirmationBar.js';
|
import { ConfirmationBar } from './src/ui/gm/ConfirmationBar.js';
|
||||||
import { PlayerPrivacyPanelMenu, initPlayerPrivacyPanelMenu } from './src/ui/player/PlayerPrivacyPanelMenu.js';
|
import { PlayerPrivacyPanelMenu, initPlayerPrivacyPanelMenu } from './src/ui/player/PlayerPrivacyPanelMenu.js';
|
||||||
import { initGMPlayerPrivacySelector } from './src/ui/gm/GMPlayerPrivacySelector.js';
|
import { initGMPlayerPrivacySelector } from './src/ui/gm/GMPlayerPrivacySelector.js';
|
||||||
|
import { ScryingPoolCameraViews, initScryingPoolCameraViews } from './src/ui/shared/ScryingPoolCameraViews.js';
|
||||||
import { SOCKET_EVENTS } from './src/contracts/socket-message.js';
|
import { SOCKET_EVENTS } from './src/contracts/socket-message.js';
|
||||||
|
|
||||||
// Module-level references — constructed in init hook, used across hooks
|
// Module-level references — constructed in init hook, used across hooks
|
||||||
@@ -57,6 +58,18 @@ let directorsBoardButtonAdded = false;
|
|||||||
Hooks.once("init", () => {
|
Hooks.once("init", () => {
|
||||||
console.log("[ScryingPool] init — module loading");
|
console.log("[ScryingPool] init — module loading");
|
||||||
|
|
||||||
|
// Foundry sets body class from URL route. If the URL has a query string
|
||||||
|
// (e.g. /game?presetName=ALL), the class becomes "game?presetName=ALL"
|
||||||
|
// instead of "game", breaking body.game { display:flex } and collapsing the UI.
|
||||||
|
const badClass = Array.from(document.body.classList).find(c => c.startsWith("game") && c !== "game");
|
||||||
|
if (badClass) {
|
||||||
|
document.body.classList.remove(badClass);
|
||||||
|
document.body.classList.add("game");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take over Foundry's camera dock — must be set before Foundry instantiates ui.webrtc
|
||||||
|
CONFIG.ui.webrtc = ScryingPoolCameraViews;
|
||||||
|
|
||||||
// WebRTC mode setting — determines how the module handles AV integration
|
// WebRTC mode setting — determines how the module handles AV integration
|
||||||
// Updated for FULL REPLACEMENT architecture (hiding Foundry's dock, showing our own)
|
// Updated for FULL REPLACEMENT architecture (hiding Foundry's dock, showing our own)
|
||||||
game.settings.register("scrying-pool", "webrtcMode", {
|
game.settings.register("scrying-pool", "webrtcMode", {
|
||||||
@@ -88,6 +101,8 @@ Hooks.once("init", () => {
|
|||||||
config: true,
|
config: true,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
|
name: "Show GM Self Feed",
|
||||||
|
hint: "When enabled, the GM's own camera feed is shown in the Scrying Pool strip.",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Story 2.1: per-user notification verbosity preference (client-scoped)
|
// Story 2.1: per-user notification verbosity preference (client-scoped)
|
||||||
@@ -95,6 +110,8 @@ Hooks.once("init", () => {
|
|||||||
scope: "client",
|
scope: "client",
|
||||||
config: true,
|
config: true,
|
||||||
type: String,
|
type: String,
|
||||||
|
name: "Notification Verbosity",
|
||||||
|
hint: "Controls which camera-state notifications you see. 'All' shows every change; 'GM Only' shows changes only to the GM and affected participant; 'Silent' suppresses all notifications except your own camera changes.",
|
||||||
choices: {
|
choices: {
|
||||||
all: "All",
|
all: "All",
|
||||||
"gm-only": "GM Only",
|
"gm-only": "GM Only",
|
||||||
@@ -109,8 +126,8 @@ Hooks.once("init", () => {
|
|||||||
config: true,
|
config: true,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
name: "Enable Scene Preset Auto-Apply",
|
name: "Enable Camera Layout Auto-Apply",
|
||||||
hint: "When enabled, scenes with configured presets will automatically apply them on activation",
|
hint: "When enabled, scenes with a configured camera layout will automatically apply it on activation",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Construct data layer — constructors are side-effect-free
|
// Construct data layer — constructors are side-effect-free
|
||||||
@@ -129,22 +146,22 @@ Hooks.once("init", () => {
|
|||||||
|
|
||||||
// Story 2.3: Bulk-action keybindings (GM only, migrated to scrying-pool namespace)
|
// Story 2.3: Bulk-action keybindings (GM only, migrated to scrying-pool namespace)
|
||||||
game.keybindings.register('scrying-pool', 'showAll', {
|
game.keybindings.register('scrying-pool', 'showAll', {
|
||||||
name: game.i18n.localize('video-view-manager.keybindings.showAll.name'),
|
name: game.i18n.localize('scrying-pool.keybindings.showAll.name'),
|
||||||
hint: game.i18n.localize('video-view-manager.keybindings.showAll.hint'),
|
hint: game.i18n.localize('scrying-pool.keybindings.showAll.hint'),
|
||||||
editable: [{ key: 'KeyS', modifiers: ['Control', 'Shift'] }],
|
editable: [{ key: 'KeyS', modifiers: ['Control', 'Shift'] }],
|
||||||
restricted: true,
|
restricted: true,
|
||||||
onDown: () => directorsBoard?.showAll(),
|
onDown: () => directorsBoard?.showAll(),
|
||||||
});
|
});
|
||||||
game.keybindings.register('scrying-pool', 'hideAll', {
|
game.keybindings.register('scrying-pool', 'hideAll', {
|
||||||
name: game.i18n.localize('video-view-manager.keybindings.hideAll.name'),
|
name: game.i18n.localize('scrying-pool.keybindings.hideAll.name'),
|
||||||
hint: game.i18n.localize('video-view-manager.keybindings.hideAll.hint'),
|
hint: game.i18n.localize('scrying-pool.keybindings.hideAll.hint'),
|
||||||
editable: [{ key: 'KeyH', modifiers: ['Control', 'Shift'] }],
|
editable: [{ key: 'KeyH', modifiers: ['Control', 'Shift'] }],
|
||||||
restricted: true,
|
restricted: true,
|
||||||
onDown: () => directorsBoard?.hideAll(),
|
onDown: () => directorsBoard?.hideAll(),
|
||||||
});
|
});
|
||||||
game.keybindings.register('scrying-pool', 'spotlightParticipant', {
|
game.keybindings.register('scrying-pool', 'spotlightParticipant', {
|
||||||
name: game.i18n.localize('video-view-manager.keybindings.spotlightParticipant.name'),
|
name: game.i18n.localize('scrying-pool.keybindings.spotlightParticipant.name'),
|
||||||
hint: game.i18n.localize('video-view-manager.keybindings.spotlightParticipant.hint'),
|
hint: game.i18n.localize('scrying-pool.keybindings.spotlightParticipant.hint'),
|
||||||
editable: [{ key: 'KeyP', modifiers: ['Control', 'Shift'] }],
|
editable: [{ key: 'KeyP', modifiers: ['Control', 'Shift'] }],
|
||||||
restricted: true,
|
restricted: true,
|
||||||
onDown: () => directorsBoard?.spotlightFocused(),
|
onDown: () => directorsBoard?.spotlightFocused(),
|
||||||
@@ -302,13 +319,20 @@ Hooks.once("ready", () => {
|
|||||||
window.directorsBoard = directorsBoard;
|
window.directorsBoard = directorsBoard;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inject Scrying Pool deps into our camera views replacement (all clients)
|
||||||
|
// Directors Board reference is GM-only — players get null so _onConfigure is a no-op
|
||||||
|
initScryingPoolCameraViews(
|
||||||
|
adapter.users.isGM() ? directorsBoard : null,
|
||||||
|
stateStore
|
||||||
|
);
|
||||||
|
|
||||||
// Pre-load participant-card as a Handlebars partial for directors-board
|
// Pre-load participant-card as a Handlebars partial for directors-board
|
||||||
// ApplicationV2 requires partials to be registered explicitly
|
// ApplicationV2 requires partials to be registered explicitly
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('modules/video-view-manager/templates/participant-card.hbs');
|
const resp = await fetch('modules/scrying-pool/templates/participant-card.hbs');
|
||||||
const source = await resp.text();
|
const source = await resp.text();
|
||||||
Handlebars.registerPartial('modules/video-view-manager/templates/participant-card.hbs', source);
|
Handlebars.registerPartial('modules/scrying-pool/templates/participant-card.hbs', source);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[ScryingPool] Failed to register participant-card partial:', err);
|
console.warn('[ScryingPool] Failed to register participant-card partial:', err);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"id": "video-view-manager",
|
"id": "scrying-pool",
|
||||||
"title": "Video View Manager (Scrying Pool)",
|
"title": "Scrying Pool",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "GM camera visibility control for FoundryVTT v14 — hide, show, and manage participant feeds in real time.",
|
"description": "GM camera visibility control for FoundryVTT v14 — hide, show, and manage participant feeds in real time.",
|
||||||
"authors": [
|
"authors": [
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "video-view-manager",
|
"name": "scrying-pool",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "video-view-manager",
|
"name": "scrying-pool",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@league-of-foundry-developers/foundry-vtt-types": "9.280.1",
|
"@league-of-foundry-developers/foundry-vtt-types": "9.280.1",
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "video-view-manager",
|
"name": "scrying-pool",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "FoundryVTT v14 module — Scrying Pool camera visibility control",
|
"description": "FoundryVTT v14 module — Scrying Pool camera visibility control",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
* Privacy settings control player opt-in/out for automation features that affect
|
* Privacy settings control player opt-in/out for automation features that affect
|
||||||
* their on-screen presence. Settings are stored as user flags on the user document.
|
* their on-screen presence. Settings are stored as user flags on the user document.
|
||||||
*
|
*
|
||||||
* Storage key: game.user.setFlag('video-view-manager', key, value)
|
* Storage key: game.user.setFlag('scrying-pool', key, value)
|
||||||
* Shape: { reactionCamEnabled: boolean, hpReactiveCamStylingEnabled: boolean, customPortraitFallback: string | null }
|
* Shape: { reactionCamEnabled: boolean, customPortraitFallback: string | null }
|
||||||
*
|
*
|
||||||
* @module contracts/privacy-settings
|
* @module contracts/privacy-settings
|
||||||
*/
|
*/
|
||||||
@@ -31,7 +31,6 @@ export const VALID_PORTRAIT_FORMATS = Object.freeze([
|
|||||||
/**
|
/**
|
||||||
* @typedef {Object} PrivacySettings
|
* @typedef {Object} PrivacySettings
|
||||||
* @property {boolean} reactionCamEnabled - Whether Reaction Cam automation is enabled for this user.
|
* @property {boolean} reactionCamEnabled - Whether Reaction Cam automation is enabled for this user.
|
||||||
* @property {boolean} hpReactiveCamStylingEnabled - Whether HP-Reactive Cam Styling is enabled for this user.
|
|
||||||
* @property {string|null} customPortraitFallback - DataURL string for custom portrait fallback image, or null if not set.
|
* @property {string|null} customPortraitFallback - DataURL string for custom portrait fallback image, or null if not set.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -43,7 +42,6 @@ export const PRIVACY_SETTINGS_VERSION = 1;
|
|||||||
*/
|
*/
|
||||||
export const PRIVACY_SETTINGS_DEFAULT = {
|
export const PRIVACY_SETTINGS_DEFAULT = {
|
||||||
reactionCamEnabled: false,
|
reactionCamEnabled: false,
|
||||||
hpReactiveCamStylingEnabled: false,
|
|
||||||
customPortraitFallback: null,
|
customPortraitFallback: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,7 +51,6 @@ export const PRIVACY_SETTINGS_DEFAULT = {
|
|||||||
*/
|
*/
|
||||||
export const PRIVACY_SETTING_KEYS = Object.freeze([
|
export const PRIVACY_SETTING_KEYS = Object.freeze([
|
||||||
"reactionCamEnabled",
|
"reactionCamEnabled",
|
||||||
"hpReactiveCamStylingEnabled",
|
|
||||||
"customPortraitFallback",
|
"customPortraitFallback",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -63,7 +60,6 @@ export const PRIVACY_SETTING_KEYS = Object.freeze([
|
|||||||
*/
|
*/
|
||||||
export const FEATURE_NAME_MAP = Object.freeze({
|
export const FEATURE_NAME_MAP = Object.freeze({
|
||||||
reactionCam: "reactionCamEnabled",
|
reactionCam: "reactionCamEnabled",
|
||||||
hpReactiveCamStyling: "hpReactiveCamStylingEnabled",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -164,13 +160,6 @@ export function isValidPrivacySettings(data) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ("hpReactiveCamStylingEnabled" in obj) {
|
|
||||||
if (typeof obj.hpReactiveCamStylingEnabled !== "boolean") {
|
|
||||||
throw new TypeError(
|
|
||||||
`PrivacySettings: hpReactiveCamStylingEnabled must be a boolean, got ${typeof obj.hpReactiveCamStylingEnabled}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ("customPortraitFallback" in obj) {
|
if ("customPortraitFallback" in obj) {
|
||||||
if (obj.customPortraitFallback !== null && typeof obj.customPortraitFallback !== "string") {
|
if (obj.customPortraitFallback !== null && typeof obj.customPortraitFallback !== "string") {
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* A Scene Preset is a named snapshot of the Visibility Matrix, stored as a
|
* A Scene Preset is a named snapshot of the Visibility Matrix, stored as a
|
||||||
* flag on a FoundryVTT Scene document. Up to 50 presets per world.
|
* flag on a FoundryVTT Scene document. Up to 50 presets per world.
|
||||||
*
|
*
|
||||||
* Storage key: scene.getFlag('video-view-manager', 'preset')
|
* Storage key: scene.getFlag('scrying-pool', 'preset')
|
||||||
* Shape: { _version: 1, presets: { [name: string]: ScenePreset } }
|
* Shape: { _version: 1, presets: { [name: string]: ScenePreset } }
|
||||||
*
|
*
|
||||||
* @module contracts/scene-preset
|
* @module contracts/scene-preset
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ import {
|
|||||||
* Manages player privacy settings for automation opt-ins.
|
* Manages player privacy settings for automation opt-ins.
|
||||||
*
|
*
|
||||||
* Settings are stored as world-level user flags:
|
* Settings are stored as world-level user flags:
|
||||||
* - game.user.setFlag('video-view-manager', 'reactionCamEnabled', boolean)
|
* - game.user.setFlag('scrying-pool', 'reactionCamEnabled', boolean)
|
||||||
* - game.user.setFlag('video-view-manager', 'hpReactiveCamStylingEnabled', boolean)
|
|
||||||
*
|
*
|
||||||
* Players can only edit their own settings.
|
* Players can only edit their own settings.
|
||||||
* GM can read (but not edit) all players' settings.
|
* GM can read (but not edit) all players' settings.
|
||||||
@@ -87,7 +86,7 @@ export class PlayerPrivacyManager {
|
|||||||
/**
|
/**
|
||||||
* Retrieves privacy settings for a specific user.
|
* Retrieves privacy settings for a specific user.
|
||||||
*
|
*
|
||||||
* Reads from user flags with module scope 'video-view-manager'.
|
* Reads from user flags with module scope 'scrying-pool'.
|
||||||
* Missing settings are merged with defaults (all false).
|
* Missing settings are merged with defaults (all false).
|
||||||
*
|
*
|
||||||
* @param {string} userId - The user ID to retrieve settings for.
|
* @param {string} userId - The user ID to retrieve settings for.
|
||||||
@@ -104,7 +103,7 @@ export class PlayerPrivacyManager {
|
|||||||
const settings = { ...PRIVACY_SETTINGS_DEFAULT };
|
const settings = { ...PRIVACY_SETTINGS_DEFAULT };
|
||||||
|
|
||||||
for (const key of PRIVACY_SETTING_KEYS) {
|
for (const key of PRIVACY_SETTING_KEYS) {
|
||||||
const value = user.getFlag("video-view-manager", key);
|
const value = user.getFlag("scrying-pool", key);
|
||||||
if (value !== undefined && value !== null) {
|
if (value !== undefined && value !== null) {
|
||||||
settings[key] = value;
|
settings[key] = value;
|
||||||
}
|
}
|
||||||
@@ -157,7 +156,7 @@ export class PlayerPrivacyManager {
|
|||||||
|
|
||||||
// Persist the setting via user flag
|
// Persist the setting via user flag
|
||||||
// Note: FoundryVTT user.setFlag returns a Promise
|
// Note: FoundryVTT user.setFlag returns a Promise
|
||||||
await user.setFlag("video-view-manager", key, value);
|
await user.setFlag("scrying-pool", key, value);
|
||||||
|
|
||||||
// Notify subscribers
|
// Notify subscribers
|
||||||
this._notifySubscribers(userId, key, value, previousValue);
|
this._notifySubscribers(userId, key, value, previousValue);
|
||||||
@@ -167,7 +166,7 @@ export class PlayerPrivacyManager {
|
|||||||
* Checks if a user has opted in to a specific automation feature.
|
* Checks if a user has opted in to a specific automation feature.
|
||||||
*
|
*
|
||||||
* @param {string} userId - The user ID to check.
|
* @param {string} userId - The user ID to check.
|
||||||
* @param {string} feature - The feature name ('reactionCam' or 'hpReactiveCamStyling').
|
* @param {string} feature - The feature name ('reactionCam').
|
||||||
* @returns {boolean} True if the user has opted in, false otherwise.
|
* @returns {boolean} True if the user has opted in, false otherwise.
|
||||||
* @throws {TypeError} If feature name is invalid.
|
* @throws {TypeError} If feature name is invalid.
|
||||||
*/
|
*/
|
||||||
@@ -306,7 +305,7 @@ export class PlayerPrivacyManager {
|
|||||||
const previousValue = this.getPortraitFallback(userId);
|
const previousValue = this.getPortraitFallback(userId);
|
||||||
|
|
||||||
// Persist the setting via user flag
|
// Persist the setting via user flag
|
||||||
await user.setFlag("video-view-manager", "customPortraitFallback", dataURL);
|
await user.setFlag("scrying-pool", "customPortraitFallback", dataURL);
|
||||||
|
|
||||||
// Notify subscribers with special portrait type
|
// Notify subscribers with special portrait type
|
||||||
this._notifyPortraitChange(userId, dataURL, previousValue);
|
this._notifyPortraitChange(userId, dataURL, previousValue);
|
||||||
@@ -326,7 +325,7 @@ export class PlayerPrivacyManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataURL = user.getFlag("video-view-manager", "customPortraitFallback");
|
const dataURL = user.getFlag("scrying-pool", "customPortraitFallback");
|
||||||
|
|
||||||
// Validate the stored DataURL (defensive programming)
|
// Validate the stored DataURL (defensive programming)
|
||||||
if (dataURL !== null && dataURL !== undefined) {
|
if (dataURL !== null && dataURL !== undefined) {
|
||||||
@@ -392,7 +391,7 @@ export class PlayerPrivacyManager {
|
|||||||
const previousValue = this.getPortraitFallback(userId);
|
const previousValue = this.getPortraitFallback(userId);
|
||||||
|
|
||||||
// Remove the setting via user flag
|
// Remove the setting via user flag
|
||||||
await user.unsetFlag("video-view-manager", "customPortraitFallback");
|
await user.unsetFlag("scrying-pool", "customPortraitFallback");
|
||||||
|
|
||||||
// Notify subscribers with special portrait type
|
// Notify subscribers with special portrait type
|
||||||
this._notifyPortraitChange(userId, null, previousValue);
|
this._notifyPortraitChange(userId, null, previousValue);
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export class ScenePresetManager {
|
|||||||
|
|
||||||
// Emit notification
|
// Emit notification
|
||||||
this._adapter.notifications.info(
|
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)
|
.replace('{name}', name)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ export class ScenePresetManager {
|
|||||||
|
|
||||||
// Emit notification
|
// Emit notification
|
||||||
this._adapter.notifications.info(
|
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}', name)
|
.replace('{name}', name)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -295,7 +295,7 @@ export class ScenePresetManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const flagData = /** @type {object & { getFlag?: (scope: string, key: string) => unknown }} */ (currentScene).getFlag?.('video-view-manager', 'presets');
|
const flagData = /** @type {object & { getFlag?: (scope: string, key: string) => unknown }} */ (currentScene).getFlag?.('scrying-pool', 'presets');
|
||||||
if (!flagData || typeof flagData !== 'object') {
|
if (!flagData || typeof flagData !== 'object') {
|
||||||
return; // No presets or invalid format
|
return; // No presets or invalid format
|
||||||
}
|
}
|
||||||
@@ -360,7 +360,7 @@ export class ScenePresetManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await /** @type {object & { setFlag?: (scope: string, key: string, value: unknown) => Promise<unknown> }} */ (currentScene).setFlag?.('video-view-manager', 'presets', flagData);
|
await /** @type {object & { setFlag?: (scope: string, key: string, value: unknown) => Promise<unknown> }} */ (currentScene).setFlag?.('scrying-pool', 'presets', flagData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(
|
console.error(
|
||||||
'[ScryingPool] ScenePresetManager: failed to save scene presets',
|
'[ScryingPool] ScenePresetManager: failed to save scene presets',
|
||||||
@@ -383,7 +383,7 @@ export class ScenePresetManager {
|
|||||||
*/
|
*/
|
||||||
async onSceneActivate(scene) {
|
async onSceneActivate(scene) {
|
||||||
// Check if auto-apply is globally enabled
|
// Check if auto-apply is globally enabled
|
||||||
const globalEnabled = this._adapter.settings.get?.('video-view-manager.autoApplyEnabled') ?? true;
|
const globalEnabled = this._adapter.settings.get?.('autoApplyEnabled') ?? true;
|
||||||
if (!globalEnabled) {
|
if (!globalEnabled) {
|
||||||
return; // Global disable
|
return; // Global disable
|
||||||
}
|
}
|
||||||
@@ -528,7 +528,7 @@ export class ScenePresetManager {
|
|||||||
|
|
||||||
// Persist to scene flag
|
// Persist to scene flag
|
||||||
try {
|
try {
|
||||||
await /** @type {object & { setFlag?: (scope: string, key: string, value: unknown) => Promise<unknown> }} */ (scene).setFlag?.('video-view-manager', 'presets', newFlagData);
|
await /** @type {object & { setFlag?: (scope: string, key: string, value: unknown) => Promise<unknown> }} */ (scene).setFlag?.('scrying-pool', 'presets', newFlagData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(
|
console.error(
|
||||||
'[ScryingPool] ScenePresetManager: failed to configure auto-apply',
|
'[ScryingPool] ScenePresetManager: failed to configure auto-apply',
|
||||||
@@ -579,7 +579,7 @@ export class ScenePresetManager {
|
|||||||
|
|
||||||
// Notify via ui.notifications
|
// Notify via ui.notifications
|
||||||
this._adapter.notifications.info(
|
this._adapter.notifications.info(
|
||||||
this._adapter.i18n.localize('video-view-manager.presets.notifications.scene-applied')
|
this._adapter.i18n.localize('scrying-pool.presets.notifications.scene-applied')
|
||||||
.replace('{name}', presetName)
|
.replace('{name}', presetName)
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -638,7 +638,7 @@ export class ScenePresetManager {
|
|||||||
*/
|
*/
|
||||||
_getSceneFlagData(scene) {
|
_getSceneFlagData(scene) {
|
||||||
try {
|
try {
|
||||||
const flagData = /** @type {object & { getFlag?: (scope: string, key: string) => unknown }} */ (scene).getFlag?.('video-view-manager', 'presets');
|
const flagData = /** @type {object & { getFlag?: (scope: string, key: string) => unknown }} */ (scene).getFlag?.('scrying-pool', 'presets');
|
||||||
if (!flagData || typeof flagData !== 'object') {
|
if (!flagData || typeof flagData !== 'object') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,10 @@ export class ScryingPoolController {
|
|||||||
if (currentState === targetState) return;
|
if (currentState === targetState) return;
|
||||||
|
|
||||||
// 5. Register PendingOp
|
// 5. Register PendingOp
|
||||||
const previousState = currentState ?? 'never-connected';
|
// Use 'active' (not 'never-connected') as the fallback previousState for users not yet in
|
||||||
|
// the matrix: 'active' matches the render-time default in buildParticipantList and prevents
|
||||||
|
// a socket-timeout revert from persisting 'never-connected' into the world settings.
|
||||||
|
const previousState = currentState ?? 'active';
|
||||||
const pendingOp = createPendingOp(opId, participantId, targetState, previousState);
|
const pendingOp = createPendingOp(opId, participantId, targetState, previousState);
|
||||||
this._pendingOps.set(participantId, pendingOp);
|
this._pendingOps.set(participantId, pendingOp);
|
||||||
|
|
||||||
@@ -186,19 +189,20 @@ export class ScryingPoolController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Socket emit
|
// 7. Socket emit (best-effort broadcast to player clients)
|
||||||
const msg = createSocketIntentMessage(opId, participantId, targetState, baseRevision);
|
const msg = createSocketIntentMessage(opId, participantId, targetState, baseRevision);
|
||||||
this._socketHandler.emit(msg.event, msg.payload);
|
this._socketHandler.emit(msg.event, msg.payload);
|
||||||
|
|
||||||
// 8. Start acknowledgement timer
|
// 8. Start acknowledgement timer
|
||||||
this._socketHandler.registerPendingOp(pendingOp, msg.event, msg.payload);
|
this._socketHandler.registerPendingOp(pendingOp, msg.event, msg.payload);
|
||||||
|
|
||||||
// 9. Notify UI subscribers
|
// 9. Self-confirm: the GM is the source of truth.
|
||||||
try {
|
// The state was already applied and persisted optimistically (step 6).
|
||||||
this._adapter.hooks.callAll('scrying-pool:controllerAction', { participantId, targetState, source, opId });
|
// Waiting for a socket echo would require a server-side relay that doesn't
|
||||||
} catch (hookErr) {
|
// exist, resulting in a 6-second timeout + spurious revert warning.
|
||||||
console.error('[ScryingPool] ScryingPoolController.action: hook emission failed', hookErr);
|
// Confirm immediately to close the pending op lifecycle cleanly.
|
||||||
}
|
// _onEcho also fires scrying-pool:controllerAction, so skip step 10.
|
||||||
|
this._onEcho({ opId, userId: participantId, state: targetState, revision: baseRevision + 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -48,6 +48,15 @@ export class StateStore {
|
|||||||
this._matrix = { ...validated.matrix };
|
this._matrix = { ...validated.matrix };
|
||||||
this._version = validated._version ?? 1;
|
this._version = validated._version ?? 1;
|
||||||
this._revision = validated._revision ?? 0;
|
this._revision = validated._revision ?? 0;
|
||||||
|
|
||||||
|
// Migration: 'never-connected' was incorrectly written to the matrix by socket-timeout
|
||||||
|
// reverts when currentState was null (the fallback should have been 'active').
|
||||||
|
// Replace persisted 'never-connected' entries with 'active' at hydration time.
|
||||||
|
for (const userId of Object.keys(this._matrix)) {
|
||||||
|
if (this._matrix[userId] === 'never-connected') {
|
||||||
|
this._matrix[userId] = 'active';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof TypeError) {
|
if (err instanceof TypeError) {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export class FoundryAdapter {
|
|||||||
static SETTING_WEBRTC_MODE = 'webrtcMode';
|
static SETTING_WEBRTC_MODE = 'webrtcMode';
|
||||||
|
|
||||||
/** Flag scope/namespace for module-specific user flags. */
|
/** Flag scope/namespace for module-specific user flags. */
|
||||||
static FLAG_SCOPE = 'video-view-manager';
|
static FLAG_SCOPE = 'scrying-pool';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a FoundryAdapter. Side-effect-free — no hooks or listeners registered.
|
* Creates a FoundryAdapter. Side-effect-free — no hooks or listeners registered.
|
||||||
@@ -294,7 +294,7 @@ export class FoundryAdapter {
|
|||||||
this.i18n = {
|
this.i18n = {
|
||||||
/**
|
/**
|
||||||
* Localize a string using the module's translation keys.
|
* Localize a string using the module's translation keys.
|
||||||
* @param {string} key - The translation key (e.g., 'video-view-manager.notifications.gmHid')
|
* @param {string} key - The translation key (e.g., 'scrying-pool.notifications.gmHid')
|
||||||
* @param {object} [data] - Optional data for string interpolation
|
* @param {object} [data] - Optional data for string interpolation
|
||||||
* @returns {string} The localized string
|
* @returns {string} The localized string
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -116,8 +116,8 @@ export class NotificationBus {
|
|||||||
*/
|
*/
|
||||||
_notifyPersonal(newState) {
|
_notifyPersonal(newState) {
|
||||||
const key = newState === 'hidden'
|
const key = newState === 'hidden'
|
||||||
? 'video-view-manager.notifications.personalHidden'
|
? 'scrying-pool.notifications.personalHidden'
|
||||||
: 'video-view-manager.notifications.personalShowed';
|
: 'scrying-pool.notifications.personalShowed';
|
||||||
const msg = this._adapter.i18n.localize(key);
|
const msg = this._adapter.i18n.localize(key);
|
||||||
this._adapter.notifications.info(msg);
|
this._adapter.notifications.info(msg);
|
||||||
}
|
}
|
||||||
@@ -170,8 +170,8 @@ export class NotificationBus {
|
|||||||
const name = this._adapter.users.get(userId)?.name ?? userId;
|
const name = this._adapter.users.get(userId)?.name ?? userId;
|
||||||
const count = entry.changeCount > 1 ? ` (${entry.changeCount} changes)` : '';
|
const count = entry.changeCount > 1 ? ` (${entry.changeCount} changes)` : '';
|
||||||
const key = entry.lastState === 'hidden'
|
const key = entry.lastState === 'hidden'
|
||||||
? 'video-view-manager.notifications.gmHid'
|
? 'scrying-pool.notifications.gmHid'
|
||||||
: 'video-view-manager.notifications.gmShowed';
|
: 'scrying-pool.notifications.gmShowed';
|
||||||
// Note: changeCount is included in the message suffix for AC-3 compliance
|
// Note: changeCount is included in the message suffix for AC-3 compliance
|
||||||
const msg = this._adapter.i18n.localize(key, { name }) + count;
|
const msg = this._adapter.i18n.localize(key, { name }) + count;
|
||||||
|
|
||||||
|
|||||||
@@ -288,14 +288,14 @@ export class ConfirmationBar {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_buildMessage(presetName, counts, variant) {
|
_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);
|
.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('{hidden}', counts.hidden)
|
||||||
.replace('{visible}', counts.visible);
|
.replace('{visible}', counts.visible);
|
||||||
|
|
||||||
if (variant === 'amber') {
|
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}`;
|
return `${baseMsg} ${countMsg} ${suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,7 +313,7 @@ export class ConfirmationBar {
|
|||||||
*/
|
*/
|
||||||
_buildHtml(message, variant) {
|
_buildHtml(message, variant) {
|
||||||
const variantClass = variant === 'amber' ? 'sp-confirmation-bar--amber' : 'sp-confirmation-bar--default';
|
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
|
// Use data-action for event delegation via StripOverlayLayer
|
||||||
// The onclick handler is set up in _setupEventListeners
|
// The onclick handler is set up in _setupEventListeners
|
||||||
|
|||||||
+82
-13
@@ -47,12 +47,12 @@ export class DirectorsBoard extends _AppBase {
|
|||||||
id: 'scrying-pool-directors-board',
|
id: 'scrying-pool-directors-board',
|
||||||
classes: ['scrying-pool', 'directors-board'],
|
classes: ['scrying-pool', 'directors-board'],
|
||||||
window: { title: "Director's Board", resizable: true },
|
window: { title: "Director's Board", resizable: true },
|
||||||
position: { width: 400, height: 300 },
|
position: { width: 420, height: 480 },
|
||||||
};
|
};
|
||||||
|
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
board: {
|
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. */
|
/** Loads saved window position from GM user flag. */
|
||||||
_loadPosition() {
|
_loadPosition() {
|
||||||
try {
|
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) {
|
if (saved?.open === true && saved.left != null && saved.top != null) {
|
||||||
// Ensure options.position exists and is mutable
|
// Ensure options.position exists and is mutable
|
||||||
if (this.options?.position) {
|
if (this.options?.position) {
|
||||||
@@ -124,8 +124,8 @@ export class DirectorsBoard extends _AppBase {
|
|||||||
Object.assign(this.options.position, {
|
Object.assign(this.options.position, {
|
||||||
left: saved.left,
|
left: saved.left,
|
||||||
top: saved.top,
|
top: saved.top,
|
||||||
width: saved.width ?? 400,
|
width: saved.width ?? 420,
|
||||||
height: saved.height ?? 300,
|
height: saved.height ?? 480,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,6 +374,8 @@ export class DirectorsBoard extends _AppBase {
|
|||||||
autoApplyPresetName: autoApplyConfig.presetName,
|
autoApplyPresetName: autoApplyConfig.presetName,
|
||||||
autoApplyPreDelay: autoApplyConfig.preDelay,
|
autoApplyPreDelay: autoApplyConfig.preDelay,
|
||||||
presets: this._scenePresetManager?.list?.() ?? [],
|
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;
|
case 'import-presets': this._onImportPresets(); break;
|
||||||
// Story 3.2: Scene auto-apply panel toggle
|
// Story 3.2: Scene auto-apply panel toggle
|
||||||
case 'toggle-preset-panel': this._togglePresetPanel(); break;
|
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) => {
|
this._focusinHandler = (e) => {
|
||||||
@@ -429,6 +434,28 @@ export class DirectorsBoard extends _AppBase {
|
|||||||
root.addEventListener('focusin', this._focusinHandler);
|
root.addEventListener('focusin', this._focusinHandler);
|
||||||
root.addEventListener('keydown', this._keydownHandler);
|
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
|
// Story 3.2: Append ScenePresetPanel to DOM and refresh
|
||||||
this._appendPresetPanel(root);
|
this._appendPresetPanel(root);
|
||||||
this._refreshPresetPanel();
|
this._refreshPresetPanel();
|
||||||
@@ -542,8 +569,8 @@ export class DirectorsBoard extends _AppBase {
|
|||||||
const localize = (key) => game.i18n?.localize(key) ?? key;
|
const localize = (key) => game.i18n?.localize(key) ?? key;
|
||||||
|
|
||||||
const getBinding = (actionKey) => {
|
const getBinding = (actionKey) => {
|
||||||
// Check both namespaces due to migration from video-view-manager to scrying-pool
|
// Check both namespaces due to migration from scrying-pool to scrying-pool
|
||||||
const namespaces = ['scrying-pool', 'video-view-manager'];
|
const namespaces = ['scrying-pool', 'scrying-pool'];
|
||||||
for (const ns of namespaces) {
|
for (const ns of namespaces) {
|
||||||
const bindings = game.keybindings?.bindings?.get(`${ns}.${actionKey}`);
|
const bindings = game.keybindings?.bindings?.get(`${ns}.${actionKey}`);
|
||||||
if (bindings?.[0]) {
|
if (bindings?.[0]) {
|
||||||
@@ -556,10 +583,10 @@ export class DirectorsBoard extends _AppBase {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const shortcuts = [
|
const shortcuts = [
|
||||||
{ label: localize('video-view-manager.directorsBoard.shortcuts.openBoard'), binding: getBinding('openDirectorsBoard') ?? 'Ctrl+Shift+V' },
|
{ label: localize('scrying-pool.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('scrying-pool.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('scrying-pool.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.spotlight'), binding: getBinding('spotlightParticipant') ?? 'Ctrl+Shift+P' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Escape HTML to prevent injection via localised strings or keybinding labels
|
// Escape HTML to prevent injection via localised strings or keybinding labels
|
||||||
@@ -570,7 +597,7 @@ export class DirectorsBoard extends _AppBase {
|
|||||||
|
|
||||||
if (typeof Dialog !== 'undefined') {
|
if (typeof Dialog !== 'undefined') {
|
||||||
new Dialog({
|
new Dialog({
|
||||||
title: localize('video-view-manager.directorsBoard.shortcuts.title'),
|
title: localize('scrying-pool.directorsBoard.shortcuts.title'),
|
||||||
content,
|
content,
|
||||||
buttons: { close: { label: 'Close' } },
|
buttons: { close: { label: 'Close' } },
|
||||||
default: '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.
|
* Opens the PresetSaveDialog for saving the current visibility matrix as a preset.
|
||||||
*/
|
*/
|
||||||
@@ -749,7 +818,7 @@ export class DirectorsBoard extends _AppBase {
|
|||||||
*/
|
*/
|
||||||
_savePosition(state) {
|
_savePosition(state) {
|
||||||
try {
|
try {
|
||||||
game.user?.setFlag('video-view-manager', 'directorsBoardState', state);
|
game.user?.setFlag('scrying-pool', 'directorsBoardState', state);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ScryingPool] Failed to save directors board position:', 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 id = escapeHtml(user.id ?? '');
|
||||||
const role = user.isGM ? 'GM' : 'Player';
|
const role = user.isGM ? 'GM' : 'Player';
|
||||||
return `
|
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-name">${name}</span>
|
||||||
<span class="sp-user-role">${role}</span>
|
<span class="sp-user-role">${role}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export class PresetExportDialog extends _AppBase {
|
|||||||
|
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
dialog: {
|
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 = {
|
static PARTS = {
|
||||||
dialog: {
|
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 = {
|
static PARTS = {
|
||||||
dialog: {
|
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 {
|
return {
|
||||||
presets: this._presets,
|
presets: this._presets,
|
||||||
hasPresets: this._presets.length > 0,
|
hasPresets: this._presets.length > 0,
|
||||||
loadLabel: i18n.localize('video-view-manager.presets.load.loadButton'),
|
loadLabel: i18n.localize('scrying-pool.presets.load.loadButton'),
|
||||||
cancelLabel: i18n.localize('video-view-manager.presets.load.cancelButton'),
|
cancelLabel: i18n.localize('scrying-pool.presets.load.cancelButton'),
|
||||||
title: i18n.localize('video-view-manager.presets.load.title'),
|
title: i18n.localize('scrying-pool.presets.load.title'),
|
||||||
emptyMessage: i18n.localize('video-view-manager.presets.load.emptyMessage'),
|
emptyMessage: i18n.localize('scrying-pool.presets.load.emptyMessage'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ export class PresetLoadDialog extends _AppBase {
|
|||||||
|
|
||||||
// Show success notification
|
// Show success notification
|
||||||
this._adapter.notifications.info(
|
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)
|
.replace('{name}', presetName)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -35,12 +35,12 @@ export class PresetSaveDialog extends _AppBase {
|
|||||||
id: 'scrying-pool-preset-save-dialog',
|
id: 'scrying-pool-preset-save-dialog',
|
||||||
classes: ['scrying-pool', 'preset-save-dialog'],
|
classes: ['scrying-pool', 'preset-save-dialog'],
|
||||||
window: { title: 'Save Scene Preset', resizable: false },
|
window: { title: 'Save Scene Preset', resizable: false },
|
||||||
position: { width: 320, height: 'auto' },
|
position: { width: 360, height: 'auto' },
|
||||||
};
|
};
|
||||||
|
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
dialog: {
|
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 {
|
return {
|
||||||
defaultName: '',
|
defaultName: '',
|
||||||
saveLabel: i18n.localize('video-view-manager.presets.save.saveButton'),
|
saveLabel: i18n.localize('scrying-pool.presets.save.saveButton'),
|
||||||
cancelLabel: i18n.localize('video-view-manager.presets.save.cancelButton'),
|
cancelLabel: i18n.localize('scrying-pool.presets.save.cancelButton'),
|
||||||
title: i18n.localize('video-view-manager.presets.save.title'),
|
title: i18n.localize('scrying-pool.presets.save.title'),
|
||||||
nameLabel: i18n.localize('video-view-manager.presets.save.nameLabel'),
|
nameLabel: i18n.localize('scrying-pool.presets.save.nameLabel'),
|
||||||
namePlaceholder: i18n.localize('video-view-manager.presets.save.namePlaceholder'),
|
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
|
// Show success notification
|
||||||
this._adapter.notifications.info(
|
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)
|
.replace('{name}', name)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export class ScenePresetPanel {
|
|||||||
this._element = document.createElement('div');
|
this._element = document.createElement('div');
|
||||||
this._element.className = 'directors-board__preset-panel';
|
this._element.className = 'directors-board__preset-panel';
|
||||||
this._element.setAttribute('role', 'region');
|
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');
|
this._element.setAttribute('aria-expanded', 'false');
|
||||||
|
|
||||||
// Initially hidden
|
// Initially hidden
|
||||||
@@ -158,11 +158,11 @@ export class ScenePresetPanel {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_buildEmptyHtml() {
|
_buildEmptyHtml() {
|
||||||
const message = this._adapter.i18n.localize('video-view-manager.scenePresetPanel.noScene');
|
const message = this._adapter.i18n.localize('scrying-pool.scenePresetPanel.noScene');
|
||||||
return `
|
return `
|
||||||
<div class="directors-board__preset-panel-header">
|
<div class="directors-board__preset-panel-header">
|
||||||
<h3 class="directors-board__preset-panel-title">
|
<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>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="directors-board__preset-panel-message">${this._escapeHtml(message)}</p>
|
<p class="directors-board__preset-panel-message">${this._escapeHtml(message)}</p>
|
||||||
@@ -195,14 +195,14 @@ export class ScenePresetPanel {
|
|||||||
// Add default option
|
// Add default option
|
||||||
const defaultOption = `
|
const defaultOption = `
|
||||||
<option value="" ${!presetName ? 'selected' : ''}>
|
<option value="" ${!presetName ? 'selected' : ''}>
|
||||||
${this._escapeHtml(localize('video-view-manager.scenePresetPanel.selectPreset'))}
|
${this._escapeHtml(localize('scrying-pool.scenePresetPanel.selectPreset'))}
|
||||||
</option>
|
</option>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="directors-board__preset-panel-header">
|
<div class="directors-board__preset-panel-header">
|
||||||
<h3 class="directors-board__preset-panel-title">
|
<h3 class="directors-board__preset-panel-title">
|
||||||
${this._escapeHtml(localize('video-view-manager.scenePresetPanel.title'))}
|
${this._escapeHtml(localize('scrying-pool.scenePresetPanel.title'))}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -214,18 +214,18 @@ export class ScenePresetPanel {
|
|||||||
data-action="toggle-auto-apply"
|
data-action="toggle-auto-apply"
|
||||||
${enabled ? 'checked' : ''}
|
${enabled ? 'checked' : ''}
|
||||||
role="switch"
|
role="switch"
|
||||||
aria-label="${this._escapeHtml(localize('video-view-manager.scenePresetPanel.enableAutoApply'))}">
|
aria-label="${this._escapeHtml(localize('scrying-pool.scenePresetPanel.enableAutoApply'))}">
|
||||||
${this._escapeHtml(localize('video-view-manager.scenePresetPanel.enableAutoApply'))}
|
${this._escapeHtml(localize('scrying-pool.scenePresetPanel.enableAutoApply'))}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="directors-board__preset-panel-row">
|
<div class="directors-board__preset-panel-row">
|
||||||
<label class="directors-board__preset-panel-label">
|
<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"
|
<select class="directors-board__preset-panel-select"
|
||||||
data-action="select-preset"
|
data-action="select-preset"
|
||||||
${!presets.length ? 'disabled' : ''}
|
${!presets.length ? 'disabled' : ''}
|
||||||
aria-label="${this._escapeHtml(localize('video-view-manager.scenePresetPanel.selectPreset'))}">
|
aria-label="${this._escapeHtml(localize('scrying-pool.scenePresetPanel.selectPreset'))}">
|
||||||
${defaultOption}
|
${defaultOption}
|
||||||
${presetOptions}
|
${presetOptions}
|
||||||
</select>
|
</select>
|
||||||
@@ -234,7 +234,7 @@ export class ScenePresetPanel {
|
|||||||
|
|
||||||
<div class="directors-board__preset-panel-row">
|
<div class="directors-board__preset-panel-row">
|
||||||
<label class="directors-board__preset-panel-label">
|
<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>
|
<span class="directors-board__preset-panel-delay-value">${preDelay}ms</span>
|
||||||
<input type="range"
|
<input type="range"
|
||||||
class="directors-board__preset-panel-slider"
|
class="directors-board__preset-panel-slider"
|
||||||
@@ -243,7 +243,7 @@ export class ScenePresetPanel {
|
|||||||
max="${this._MAX_PREDELAY}"
|
max="${this._MAX_PREDELAY}"
|
||||||
value="${preDelay}"
|
value="${preDelay}"
|
||||||
step="100"
|
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-valuemin="${this._MIN_PREDELAY}"
|
||||||
aria-valuemax="${this._MAX_PREDELAY}"
|
aria-valuemax="${this._MAX_PREDELAY}"
|
||||||
aria-valuenow="${preDelay}">
|
aria-valuenow="${preDelay}">
|
||||||
@@ -251,7 +251,7 @@ export class ScenePresetPanel {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="directors-board__preset-panel-row directors-board__preset-panel-row--hint">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -338,8 +338,8 @@ export class ScenePresetPanel {
|
|||||||
// Notify
|
// Notify
|
||||||
this._adapter.notifications.info(
|
this._adapter.notifications.info(
|
||||||
isChecked
|
isChecked
|
||||||
? this._adapter.i18n.localize('video-view-manager.scenePresetPanel.notifications.enabled')
|
? this._adapter.i18n.localize('scrying-pool.scenePresetPanel.notifications.enabled')
|
||||||
: this._adapter.i18n.localize('video-view-manager.scenePresetPanel.notifications.disabled')
|
: this._adapter.i18n.localize('scrying-pool.scenePresetPanel.notifications.disabled')
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ScryingPool] ScenePresetPanel: failed to toggle auto-apply', err);
|
console.error('[ScryingPool] ScenePresetPanel: failed to toggle auto-apply', err);
|
||||||
@@ -373,7 +373,7 @@ export class ScenePresetPanel {
|
|||||||
// Notify
|
// Notify
|
||||||
if (presetName) {
|
if (presetName) {
|
||||||
this._adapter.notifications.info(
|
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)
|
.replace('{name}', presetName)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export class ScryingPoolStrip extends _AppBase {
|
|||||||
: super.defaultOptions ?? {};
|
: super.defaultOptions ?? {};
|
||||||
return Object.assign({}, base, {
|
return Object.assign({}, base, {
|
||||||
id: 'scrying-pool-strip',
|
id: 'scrying-pool-strip',
|
||||||
template: 'modules/video-view-manager/templates/roster-strip.hbs',
|
template: 'modules/scrying-pool/templates/roster-strip.hbs',
|
||||||
popOut: true,
|
popOut: true,
|
||||||
resizable: false,
|
resizable: false,
|
||||||
title: 'Scrying Pool',
|
title: 'Scrying Pool',
|
||||||
@@ -140,7 +140,7 @@ export class ScryingPoolStrip extends _AppBase {
|
|||||||
getData() {
|
getData() {
|
||||||
const savedState =
|
const savedState =
|
||||||
typeof game !== 'undefined'
|
typeof game !== 'undefined'
|
||||||
? game.user?.getFlag?.('video-view-manager', 'stripState')
|
? game.user?.getFlag?.('scrying-pool', 'stripState')
|
||||||
: null;
|
: null;
|
||||||
if (savedState?.expanded !== undefined) {
|
if (savedState?.expanded !== undefined) {
|
||||||
this._isExpanded = savedState.expanded;
|
this._isExpanded = savedState.expanded;
|
||||||
@@ -148,17 +148,24 @@ export class ScryingPoolStrip extends _AppBase {
|
|||||||
|
|
||||||
const showFirstOpenTip =
|
const showFirstOpenTip =
|
||||||
typeof game !== 'undefined' &&
|
typeof game !== 'undefined' &&
|
||||||
!game.user?.getFlag?.('video-view-manager', 'firstStripOpen');
|
!game.user?.getFlag?.('scrying-pool', 'firstStripOpen');
|
||||||
|
|
||||||
const userIds = this._adapter.users.all
|
const userIds = this._adapter.users.all
|
||||||
? this._adapter.users.all().map(u => u.id)
|
? 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)
|
// Check if we have stream access for video replacement (full AV replacement mode)
|
||||||
const hasStreamAccess = this._adapter.webrtc?.getMediaStreamForUser !== undefined;
|
const hasStreamAccess = this._adapter.webrtc?.getMediaStreamForUser !== undefined;
|
||||||
|
|
||||||
const participants = buildParticipantList(
|
const participants = buildParticipantList(
|
||||||
userIds,
|
filteredUserIds,
|
||||||
this._stateStore,
|
this._stateStore,
|
||||||
this._controller,
|
this._controller,
|
||||||
this._adapter,
|
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
|
// First open tip: set flag so it doesn't show again
|
||||||
const isFirstOpen =
|
const isFirstOpen =
|
||||||
typeof game !== 'undefined' &&
|
typeof game !== 'undefined' &&
|
||||||
!game.user?.getFlag?.('video-view-manager', 'firstStripOpen');
|
!game.user?.getFlag?.('scrying-pool', 'firstStripOpen');
|
||||||
if (isFirstOpen) {
|
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)
|
// Attach video streams if we have stream access (full AV replacement mode)
|
||||||
if (this._adapter.webrtc?.getMediaStreamForUser !== undefined) {
|
if (this._adapter.webrtc?.getMediaStreamForUser !== undefined) {
|
||||||
this._attachVideoStreams(el);
|
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 */
|
/** @inheritdoc */
|
||||||
@@ -233,7 +278,7 @@ export class ScryingPoolStrip extends _AppBase {
|
|||||||
this._cleanupVideoStreams();
|
this._cleanupVideoStreams();
|
||||||
|
|
||||||
if (typeof game !== 'undefined') {
|
if (typeof game !== 'undefined') {
|
||||||
game.user?.setFlag?.('video-view-manager', 'stripState', {
|
game.user?.setFlag?.('scrying-pool', 'stripState', {
|
||||||
left: this.position?.left,
|
left: this.position?.left,
|
||||||
top: this.position?.top,
|
top: this.position?.top,
|
||||||
open: false,
|
open: false,
|
||||||
@@ -267,7 +312,7 @@ export class ScryingPoolStrip extends _AppBase {
|
|||||||
_toggleExpanded() {
|
_toggleExpanded() {
|
||||||
this._isExpanded = !this._isExpanded;
|
this._isExpanded = !this._isExpanded;
|
||||||
if (typeof game !== 'undefined') {
|
if (typeof game !== 'undefined') {
|
||||||
game.user?.setFlag?.('video-view-manager', 'stripState', {
|
game.user?.setFlag?.('scrying-pool', 'stripState', {
|
||||||
left: this.position?.left,
|
left: this.position?.left,
|
||||||
top: this.position?.top,
|
top: this.position?.top,
|
||||||
open: true,
|
open: true,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export class PlayerPrivacyPanel extends _AppBase {
|
|||||||
|
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
dialog: {
|
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,
|
enabled: settings.reactionCamEnabled,
|
||||||
settingKey: '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
|
// Toggle labels
|
||||||
@@ -187,19 +180,28 @@ export class PlayerPrivacyPanel extends _AppBase {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up event handlers after rendering.
|
* 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) {
|
_onRender(context) {
|
||||||
// Normalize html to element with querySelector
|
// ApplicationV2 passes the template context as the first argument, not the element.
|
||||||
// FoundryVTT ApplicationV2 passes jQuery object, tests pass plain objects
|
// Prefer this.element (the rendered root). Fall back to treating the argument as an element
|
||||||
const element = html instanceof HTMLElement
|
// (for jQuery objects or test mocks that expose querySelector directly).
|
||||||
? html
|
let element;
|
||||||
: (html?.[0] ?? html);
|
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;
|
if (!element || typeof element.querySelector !== 'function') return;
|
||||||
|
|
||||||
// Cache toggle elements
|
// Cache toggle elements
|
||||||
this._reactionCamToggle = element.querySelector('[data-setting="reactionCamEnabled"]');
|
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
|
// Story 4.2: Set up portrait section event handlers
|
||||||
this._setupPortraitHandlers(element);
|
this._setupPortraitHandlers(element);
|
||||||
@@ -208,6 +210,27 @@ export class PlayerPrivacyPanel extends _AppBase {
|
|||||||
this._setupToggleHandlers(element);
|
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.
|
* Sets up event handlers for toggle switches.
|
||||||
* @param {HTMLElement} element - The dialog element.
|
* @param {HTMLElement} element - The dialog element.
|
||||||
@@ -428,6 +451,16 @@ export class PlayerPrivacyPanel extends _AppBase {
|
|||||||
this._currentSettings[settingKey] = newValue;
|
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
|
// Show success notification
|
||||||
this._adapter.notifications.info(
|
this._adapter.notifications.info(
|
||||||
this._adapter.i18n.localize('SCRYING_POOL.PrivacyPanel.savedNotification')
|
this._adapter.i18n.localize('SCRYING_POOL.PrivacyPanel.savedNotification')
|
||||||
@@ -466,7 +499,6 @@ export class PlayerPrivacyPanel extends _AppBase {
|
|||||||
_onClose() {
|
_onClose() {
|
||||||
// Clear cached elements
|
// Clear cached elements
|
||||||
this._reactionCamToggle = null;
|
this._reactionCamToggle = null;
|
||||||
this._hpReactiveCamToggle = null;
|
|
||||||
this._fileInput = null;
|
this._fileInput = null;
|
||||||
this._portraitPreview = null;
|
this._portraitPreview = null;
|
||||||
this._currentSettings = null;
|
this._currentSettings = null;
|
||||||
|
|||||||
@@ -501,7 +501,7 @@ export class VisibilityBadge {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
_getFirstBadgeEncountered() {
|
_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>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async _setFirstBadgeEncountered() {
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
-3
@@ -23,16 +23,30 @@ export function resolveToggleTarget(currentState) {
|
|||||||
* @param {string} userId
|
* @param {string} userId
|
||||||
* @param {object} [user] - Optional user object for additional data
|
* @param {object} [user] - Optional user object for additional data
|
||||||
* @param {object} [privacyManager] - Optional PlayerPrivacyManager for privacy settings
|
* @param {object} [privacyManager] - Optional PlayerPrivacyManager for privacy settings
|
||||||
|
* @param {object} [controller] - Optional ScryingPoolController for pending-op state
|
||||||
* @returns {object} Participant context
|
* @returns {object} Participant context
|
||||||
*/
|
*/
|
||||||
export function buildSimpleParticipantContext(stateStore, userId, user, privacyManager) {
|
export function buildSimpleParticipantContext(stateStore, userId, user, privacyManager, controller) {
|
||||||
const state = stateStore.getState(userId);
|
const state = stateStore.getState(userId);
|
||||||
|
const resolvedState = state ?? 'active';
|
||||||
const context = {
|
const context = {
|
||||||
userId,
|
userId,
|
||||||
state: state ?? 'active',
|
state: resolvedState,
|
||||||
isGhost: state === 'ghost',
|
isGhost: state === 'ghost',
|
||||||
|
isHidden: resolvedState === 'hidden',
|
||||||
|
hasPendingOp: controller?._pendingOps?.has?.(userId) ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Name, avatar and aria labels from the user object
|
||||||
|
if (user && typeof user === 'object') {
|
||||||
|
context.name = user.name ?? '';
|
||||||
|
context.avatarSrc = user.avatar ?? 'icons/svg/mystery-man.svg';
|
||||||
|
context.cardAriaLabel = user.name ?? userId;
|
||||||
|
context.toggleAriaLabel = resolvedState === 'hidden'
|
||||||
|
? `Show ${user.name}`
|
||||||
|
: `Hide ${user.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Add privacy settings if privacyManager is provided
|
// Add privacy settings if privacyManager is provided
|
||||||
if (privacyManager && user) {
|
if (privacyManager && user) {
|
||||||
try {
|
try {
|
||||||
@@ -60,7 +74,7 @@ export function buildBoardContext(stateStore, controller, adapter, privacyManage
|
|||||||
const users = adapter.users.all?.() ?? [];
|
const users = adapter.users.all?.() ?? [];
|
||||||
const participants = users.map(u => {
|
const participants = users.map(u => {
|
||||||
const userId = u.id ?? u;
|
const userId = u.id ?? u;
|
||||||
return buildSimpleParticipantContext(stateStore, userId, u, privacyManager);
|
return buildSimpleParticipantContext(stateStore, userId, u, privacyManager, controller);
|
||||||
});
|
});
|
||||||
return { participants, isEmpty: participants.length === 0 };
|
return { participants, isEmpty: participants.length === 0 };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -10,15 +10,79 @@
|
|||||||
// via DEFAULT_OPTIONS.classes. The content section and footer live inside PARTS.
|
// via DEFAULT_OPTIONS.classes. The content section and footer live inside PARTS.
|
||||||
|
|
||||||
.scrying-pool.directors-board {
|
.scrying-pool.directors-board {
|
||||||
background: var(--sp-surface);
|
background: linear-gradient(175deg, hsl(220, 18%, 13%) 0%, hsl(220, 15%, 10%) 100%);
|
||||||
color: var(--sp-text-primary);
|
color: var(--sp-text-primary);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-top: 2px solid hsl(200, 55%, 40%);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.55), 0 2px 8px rgba(0, 0, 0, 0.35),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||||
|
|
||||||
|
// ── Hide Foundry's default window header ──────────────────────────────────
|
||||||
|
header.window-header { display: none; }
|
||||||
|
|
||||||
|
// The inner container is position:relative so the close btn can be absolute.
|
||||||
|
.directors-board__inner {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Drag grip ─────────────────────────────────────────────────────────────
|
||||||
|
.directors-board__grip {
|
||||||
|
width: 100%;
|
||||||
|
height: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: grab;
|
||||||
|
color: var(--sp-text-muted, hsl(0, 0%, 70%));
|
||||||
|
opacity: 0.35;
|
||||||
|
font-size: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: opacity 0.15s, background 0.15s;
|
||||||
|
user-select: none;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
|
||||||
|
&:hover { opacity: 0.8; background: rgba(255, 255, 255, 0.03); }
|
||||||
|
&:active { cursor: grabbing; opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Custom close button ───────────────────────────────────────────────────
|
||||||
|
.directors-board__close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 6px;
|
||||||
|
z-index: 10;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--sp-text-muted);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--sp-radius-sm, 3px);
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.15s, background 0.15s, color 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: var(--sp-text-primary);
|
||||||
|
}
|
||||||
|
&:active { opacity: 0.75; }
|
||||||
|
}
|
||||||
|
|
||||||
// ── Participant grid ──────────────────────────────────────────────────────
|
// ── Participant grid ──────────────────────────────────────────────────────
|
||||||
.directors-board__content {
|
.directors-board__content {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
grid-template-columns: repeat(auto-fill, 68px);
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
padding: 12px;
|
padding: 8px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
@@ -30,8 +94,8 @@
|
|||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--sp-text-muted);
|
color: var(--sp-text-muted);
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
padding: 24px 0;
|
padding: 20px 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,51 +103,57 @@
|
|||||||
.directors-board__bulk-bar {
|
.directors-board__bulk-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
padding: 6px 12px;
|
padding: 4px 8px;
|
||||||
border-top: 1px solid var(--sp-border);
|
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
background: rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.directors-board__bulk-btn {
|
.directors-board__bulk-btn {
|
||||||
font-size: 12px;
|
display: inline-flex;
|
||||||
background: var(--sp-accent, #4a6f9c);
|
align-items: center;
|
||||||
color: #fff;
|
gap: 4px;
|
||||||
border: none;
|
font-size: 11px;
|
||||||
|
background: rgba(74, 111, 156, 0.7);
|
||||||
|
color: #e8edf2;
|
||||||
|
border: 1px solid rgba(74, 111, 156, 0.4);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 4px 10px;
|
padding: 3px 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: opacity 0.15s;
|
transition: background 0.15s, opacity 0.15s;
|
||||||
|
|
||||||
&:hover { opacity: 0.85; }
|
i { font-size: 10px; }
|
||||||
&:active { opacity: 0.7; }
|
|
||||||
|
&:hover { background: rgba(74, 111, 156, 0.95); }
|
||||||
|
&:active { opacity: 0.8; }
|
||||||
|
|
||||||
// Undo — secondary style
|
|
||||||
&--undo {
|
&--undo {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--sp-text-muted);
|
color: var(--sp-text-muted);
|
||||||
border: 1px solid var(--sp-border);
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
|
||||||
&:hover { color: var(--sp-text, inherit); border-color: currentColor; }
|
&:hover { color: var(--sp-text-primary, #dde2e8); border-color: rgba(255,255,255,0.25); background: rgba(255,255,255,0.05); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore — spotlight accent (distinct from Undo)
|
|
||||||
&--restore {
|
&--restore {
|
||||||
background: var(--sp-spotlight-accent, #7b4fa6);
|
background: rgba(123, 79, 166, 0.7);
|
||||||
|
border-color: rgba(123, 79, 166, 0.4);
|
||||||
|
&:hover { background: rgba(123, 79, 166, 0.95); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Help / shortcut-panel button ───────────────────────────────────────────
|
// ── Help / shortcut-panel button ───────────────────────────────────────────
|
||||||
.directors-board__help-btn {
|
.directors-board__help-btn {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
width: 22px;
|
width: 20px;
|
||||||
height: 22px;
|
height: 20px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 1px solid var(--sp-border);
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--sp-text-muted);
|
color: var(--sp-text-muted);
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -92,37 +162,105 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
&:hover { background: var(--sp-accent, #4a6f9c); color: #fff; border-color: transparent; }
|
&:hover { background: rgba(74, 111, 156, 0.8); color: #fff; border-color: transparent; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Footer (preset actions) ────────────────────────────────────────────
|
// ── Footer (preset actions) ────────────────────────────────────────────────
|
||||||
.directors-board__footer {
|
.directors-board__footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
flex-direction: column;
|
||||||
padding: 8px 12px;
|
gap: 3px;
|
||||||
border-top: 1px solid var(--sp-border);
|
padding: 5px 8px 6px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
&-btn {
|
.directors-board__footer-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
|
||||||
|
&--presets { flex-wrap: wrap; }
|
||||||
|
|
||||||
|
&--controls {
|
||||||
|
padding-top: 3px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.directors-board__footer-sep {
|
||||||
|
width: 1px;
|
||||||
|
align-self: stretch;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
margin: 0 3px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directors-board__footer-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 12px;
|
display: inline-flex;
|
||||||
background: var(--sp-accent, #4a6f9c);
|
align-items: center;
|
||||||
color: #fff;
|
justify-content: center;
|
||||||
border: none;
|
gap: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
background: rgba(74, 111, 156, 0.65);
|
||||||
|
color: #d8e4f0;
|
||||||
|
border: 1px solid rgba(74, 111, 156, 0.35);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 4px 8px;
|
padding: 4px 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: opacity 0.15s;
|
transition: background 0.15s, opacity 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
i { font-size: 0.9em; }
|
||||||
|
|
||||||
|
&:hover { background: rgba(74, 111, 156, 0.9); }
|
||||||
|
&:active { opacity: 0.75; }
|
||||||
|
|
||||||
&:hover { opacity: 0.85; }
|
|
||||||
&:active { opacity: 0.7; }
|
|
||||||
&[disabled] {
|
&[disabled] {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.5;
|
opacity: 0.4;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--sp-text-muted);
|
color: var(--sp-text-muted);
|
||||||
border: 1px solid var(--sp-border);
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--secondary {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: var(--sp-text-secondary, #9aa4b0);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
&:hover { background: rgba(255, 255, 255, 0.1); color: #dde2e8; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&--auto-apply {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: var(--sp-text-secondary, #9aa4b0);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
&:hover { background: rgba(255, 255, 255, 0.1); color: #dde2e8; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&--av {
|
||||||
|
background: rgba(30, 35, 45, 0.8);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: var(--sp-text-muted, #aaa);
|
||||||
|
&:hover { background: rgba(50, 55, 70, 0.9); }
|
||||||
|
}
|
||||||
|
|
||||||
|
&--av-active {
|
||||||
|
background: rgba(90, 42, 42, 0.85);
|
||||||
|
border-color: rgba(160, 64, 64, 0.6);
|
||||||
|
color: #f0a0a0;
|
||||||
|
&:hover { background: rgba(110, 50, 50, 0.95); }
|
||||||
|
}
|
||||||
|
|
||||||
|
&--av-config {
|
||||||
|
background: rgba(30, 35, 45, 0.8);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: var(--sp-text-muted, #aaa);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
&:hover { background: rgba(50, 55, 70, 0.9); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,10 @@
|
|||||||
&__toggle {
|
&__toggle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
// Foundry applies an explicit height to <button> elements which overrides
|
||||||
|
// the bottom:0 stretch constraint. Force 100%×100% to fill the card.
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -10,9 +10,10 @@
|
|||||||
|
|
||||||
.scrying-pool {
|
.scrying-pool {
|
||||||
// Dialog root element
|
// Dialog root element
|
||||||
.player-privacy-panel {
|
&.player-privacy-panel {
|
||||||
background: var(--sp-surface);
|
background: var(--sp-surface);
|
||||||
color: var(--sp-text-primary);
|
color: var(--sp-text-primary);
|
||||||
|
font-family: var(--font-primary, inherit);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Container
|
// Container
|
||||||
@@ -23,57 +24,50 @@
|
|||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header
|
|
||||||
.player-privacy-panel__header {
|
|
||||||
padding: var(--sp-spacing-sm, 8px) var(--sp-spacing-md, 12px);
|
|
||||||
border-bottom: 1px solid var(--sp-border);
|
|
||||||
background: var(--sp-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-privacy-panel__title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.1em;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--sp-text-primary);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Body
|
// Body
|
||||||
.player-privacy-panel__body {
|
.player-privacy-panel__body {
|
||||||
padding: var(--sp-spacing-md, 12px);
|
padding: 12px;
|
||||||
background: var(--sp-surface);
|
background: var(--sp-surface);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notice (read-only)
|
// Notice (read-only)
|
||||||
.player-privacy-panel__notice {
|
.player-privacy-panel__notice {
|
||||||
padding: var(--sp-spacing-sm, 8px) var(--sp-spacing-md, 12px);
|
padding: 8px 12px;
|
||||||
margin-bottom: var(--sp-spacing-md, 12px);
|
margin-bottom: 10px;
|
||||||
border-radius: var(--sp-border-radius, 4px);
|
border-radius: 4px;
|
||||||
font-size: 0.85em;
|
font-size: 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-privacy-panel__notice--readonly {
|
.player-privacy-panel__notice--readonly {
|
||||||
background: var(--sp-urgency-awareness);
|
background: hsla(48, 88%, 55%, 0.10);
|
||||||
color: var(--sp-text-secondary);
|
color: hsl(48, 88%, 60%);
|
||||||
border: 1px solid var(--sp-border);
|
border: 1px solid hsla(48, 88%, 55%, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Section
|
// Section
|
||||||
.player-privacy-panel__section {
|
.player-privacy-panel__section {
|
||||||
margin-bottom: var(--sp-spacing-md, 12px);
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Section header — force UI font, no fantasy
|
||||||
.player-privacy-panel__section-header {
|
.player-privacy-panel__section-header {
|
||||||
margin: 0 0 var(--sp-spacing-xs, 4px) 0;
|
margin: 0 0 4px 0;
|
||||||
font-size: 0.95em;
|
font-family: var(--font-primary, ui-sans-serif, system-ui, sans-serif);
|
||||||
font-weight: 600;
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
color: var(--sp-text-primary);
|
color: var(--sp-text-primary);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-privacy-panel__section-description {
|
.player-privacy-panel__section-description {
|
||||||
margin: 0 0 var(--sp-spacing-md, 12px) 0;
|
margin: 0 0 10px 0;
|
||||||
font-size: 0.85em;
|
font-size: 12px;
|
||||||
color: var(--sp-text-secondary);
|
color: var(--sp-text-secondary);
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
@@ -82,40 +76,51 @@
|
|||||||
.player-privacy-panel__effects-list {
|
.player-privacy-panel__effects-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--sp-spacing-md, 12px);
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Individual effect
|
// Individual effect card
|
||||||
.player-privacy-panel__effect {
|
.player-privacy-panel__effect {
|
||||||
padding: var(--sp-spacing-sm, 8px);
|
padding: 8px 10px;
|
||||||
border: 1px solid var(--sp-border);
|
border: 1px solid var(--sp-border);
|
||||||
border-radius: var(--sp-border-radius, 4px);
|
border-radius: 5px;
|
||||||
background: var(--sp-surface-elevated);
|
background: linear-gradient(135deg, hsl(215,25%,11%) 0%, hsl(215,22%,9%) 100%);
|
||||||
|
transition: border-color 120ms ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--sp-border-hover, rgba(255,255,255,0.15));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-privacy-panel__effect-header {
|
.player-privacy-panel__effect-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: var(--sp-spacing-xs, 4px);
|
margin-bottom: 4px;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Effect label — force clean UI font
|
||||||
.player-privacy-panel__effect-label {
|
.player-privacy-panel__effect-label {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.9em;
|
font-family: var(--font-primary, ui-sans-serif, system-ui, sans-serif);
|
||||||
font-weight: 500;
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
color: var(--sp-text-primary);
|
color: var(--sp-text-primary);
|
||||||
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-privacy-panel__effect-description {
|
.player-privacy-panel__effect-description {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.8em;
|
font-size: 11px;
|
||||||
color: var(--sp-text-secondary);
|
color: var(--sp-text-secondary);
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle switch
|
// Toggle container
|
||||||
.player-privacy-panel__toggle {
|
.player-privacy-panel__toggle {
|
||||||
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -123,13 +128,13 @@
|
|||||||
.player-privacy-panel__toggle-label {
|
.player-privacy-panel__toggle-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--sp-spacing-xs, 4px);
|
gap: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
font-size: 0.85em;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle input - visually hidden, uses custom styling
|
// Toggle input — visually hidden
|
||||||
.player-privacy-panel__toggle-input {
|
.player-privacy-panel__toggle-input {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -142,53 +147,66 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:disabled + .player-privacy-panel__toggle-text {
|
&:disabled + .player-privacy-panel__toggle-text {
|
||||||
opacity: 0.6;
|
opacity: 0.45;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Toggle badge — clearly different ON vs OFF
|
||||||
.player-privacy-panel__toggle-text {
|
.player-privacy-panel__toggle-text {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
padding: var(--sp-spacing-xs, 4px) var(--sp-spacing-sm, 8px);
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 3px 9px;
|
||||||
border: 1px solid var(--sp-border);
|
border: 1px solid var(--sp-border);
|
||||||
border-radius: var(--sp-border-radius, 4px);
|
border-radius: 20px;
|
||||||
background: var(--sp-surface);
|
font-family: var(--font-primary, ui-sans-serif, system-ui, sans-serif);
|
||||||
color: var(--sp-text-primary);
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
transition: all 0.15s ease;
|
letter-spacing: 0.03em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--sp-text-secondary);
|
||||||
|
background: rgba(255,255,255,0.04);
|
||||||
|
transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
i { font-size: 9px; opacity: 0.7; }
|
||||||
|
|
||||||
|
// Enabled state
|
||||||
.player-privacy-panel__toggle-input:checked + & {
|
.player-privacy-panel__toggle-input:checked + & {
|
||||||
background: var(--sp-accent);
|
background: rgba(46, 160, 67, 0.18);
|
||||||
color: white;
|
color: hsl(134, 61%, 60%);
|
||||||
|
border-color: rgba(46, 160, 67, 0.4);
|
||||||
|
|
||||||
|
i { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hover (when not disabled)
|
||||||
|
.player-privacy-panel__toggle-label:hover & {
|
||||||
border-color: var(--sp-accent);
|
border-color: var(--sp-accent);
|
||||||
}
|
}
|
||||||
|
.player-privacy-panel__toggle-input:checked + &:hover,
|
||||||
.player-privacy-panel__toggle-input:disabled + & {
|
.player-privacy-panel__toggle-label:hover .player-privacy-panel__toggle-input:checked + & {
|
||||||
background: var(--sp-surface);
|
background: rgba(46, 160, 67, 0.25);
|
||||||
border-color: var(--sp-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-privacy-panel__toggle-input:checked:disabled + & {
|
|
||||||
background: var(--sp-accent);
|
|
||||||
color: white;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Story 4.2: Portrait fallback section
|
// Portrait section
|
||||||
.player-privacy-panel__portrait-container {
|
.player-privacy-panel__portrait-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: flex-start;
|
||||||
gap: var(--sp-spacing-sm, 8px);
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-privacy-panel__portrait-preview {
|
.player-privacy-panel__portrait-preview {
|
||||||
width: 100px;
|
flex-shrink: 0;
|
||||||
height: 100px;
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
border: 2px solid var(--sp-border);
|
border: 2px solid var(--sp-border);
|
||||||
border-radius: var(--sp-border-radius, 4px);
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--sp-surface-elevated);
|
background: hsl(215,25%,11%);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -202,13 +220,123 @@
|
|||||||
|
|
||||||
.player-privacy-panel__portrait-actions {
|
.player-privacy-panel__portrait-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--sp-spacing-sm, 8px);
|
flex-direction: column;
|
||||||
flex-wrap: wrap;
|
gap: 6px;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-privacy-panel__portrait-choose,
|
.player-privacy-panel__portrait-choose,
|
||||||
.player-privacy-panel__portrait-remove {
|
.player-privacy-panel__portrait-remove {
|
||||||
font-size: 0.85em;
|
font-size: 12px;
|
||||||
padding: var(--sp-spacing-xs, 4px) var(--sp-spacing-sm, 8px);
|
padding: 5px 10px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GM Privacy Selector dialog
|
||||||
|
&.gm-privacy-selector-dialog {
|
||||||
|
background: var(--sp-surface);
|
||||||
|
border: 1px solid var(--sp-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
|
||||||
|
min-width: 320px;
|
||||||
|
max-width: 480px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-gm-privacy-selector {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--sp-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-dialog-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-bottom: 1px solid var(--sp-border);
|
||||||
|
background: var(--sp-surface-raised);
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-primary, ui-sans-serif, system-ui, sans-serif);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--sp-text-primary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-close-button {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--sp-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
line-height: 1;
|
||||||
|
transition: color 120ms ease, background 120ms ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--sp-text-primary);
|
||||||
|
background: var(--sp-surface-interactive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-gm-privacy-selector > p {
|
||||||
|
padding: 7px 14px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--sp-text-secondary);
|
||||||
|
background: var(--sp-surface);
|
||||||
|
border-bottom: 1px solid var(--sp-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-user-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 8px;
|
||||||
|
gap: 4px;
|
||||||
|
background: var(--sp-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-user-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 7px 12px;
|
||||||
|
border: 1px solid var(--sp-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: linear-gradient(135deg, hsl(215,25%,11%) 0%, hsl(215,22%,9%) 100%);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 120ms ease, border-color 120ms ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: hsl(215,25%,14%);
|
||||||
|
border-color: var(--sp-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-user-name {
|
||||||
|
font-family: var(--font-primary, ui-sans-serif, system-ui, sans-serif);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--sp-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-user-role {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--sp-text-secondary);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border: 1px solid var(--sp-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
background: rgba(255,255,255,0.04);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
// ============================================================================
|
// Wrapper divs we added to satisfy the single-root-element requirement
|
||||||
// Preset Import/Export Dialogs
|
.sp-preset-export-dialog,
|
||||||
|
.sp-preset-import-dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Story 3.3: Preset Import & Export
|
// Story 3.3: Preset Import & Export
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
@@ -159,9 +166,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
// Prevent pointer events from bubbling through
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sp-file-label {
|
.sp-file-label {
|
||||||
|
|||||||
@@ -1,14 +1,3 @@
|
|||||||
/**
|
|
||||||
* styles/components/_preset-load-dialog.less
|
|
||||||
*
|
|
||||||
* Layout for the Preset Load Dialog.
|
|
||||||
* All selectors scoped under .scrying-pool.
|
|
||||||
* Uses --sp-* tokens only — no Foundry --color-* / --font-* / --border-* tokens.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// The ApplicationV2 window root already has .scrying-pool .preset-load-dialog applied
|
|
||||||
// via DEFAULT_OPTIONS.classes. The content lives inside PARTS.
|
|
||||||
|
|
||||||
.scrying-pool.preset-load-dialog {
|
.scrying-pool.preset-load-dialog {
|
||||||
background: var(--sp-surface);
|
background: var(--sp-surface);
|
||||||
color: var(--sp-text-primary);
|
color: var(--sp-text-primary);
|
||||||
@@ -21,23 +10,9 @@
|
|||||||
gap: 0;
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Header ────────────────────────────────────────────────────────────
|
|
||||||
.preset-load-dialog__header {
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-bottom: 1px solid var(--sp-border);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preset-load-dialog__title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--sp-text, inherit);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Body ──────────────────────────────────────────────────────────────
|
// ── Body ──────────────────────────────────────────────────────────────
|
||||||
.preset-load-dialog__body {
|
.preset-load-dialog__body {
|
||||||
padding: 16px;
|
padding: 12px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
@@ -49,6 +24,7 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
padding: 24px 0;
|
padding: 24px 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Preset list ────────────────────────────────────────────────────────
|
// ── Preset list ────────────────────────────────────────────────────────
|
||||||
@@ -71,18 +47,28 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.15s, border-color 0.15s;
|
transition: background-color 0.15s, border-color 0.15s;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
// Load button — primary
|
i { font-size: 0.85em; opacity: 0.7; flex-shrink: 0; }
|
||||||
|
|
||||||
|
// Load button — styled as a row item, not a loud primary button
|
||||||
&--load {
|
&--load {
|
||||||
background: var(--sp-accent, #4a6f9c);
|
background: var(--sp-surface-elevated, rgba(255,255,255,0.06));
|
||||||
color: #fff;
|
color: var(--sp-text-primary);
|
||||||
border: none;
|
border: 1px solid var(--sp-border);
|
||||||
|
|
||||||
&:hover { opacity: 0.85; }
|
&:hover {
|
||||||
&:active { opacity: 0.7; }
|
background: var(--sp-accent, #4a6f9c);
|
||||||
|
border-color: var(--sp-accent, #4a6f9c);
|
||||||
|
color: #fff;
|
||||||
|
i { opacity: 1; }
|
||||||
|
}
|
||||||
|
&:active { opacity: 0.85; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel button — secondary
|
// Cancel button — secondary
|
||||||
@@ -90,7 +76,6 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--sp-text-muted);
|
color: var(--sp-text-muted);
|
||||||
border: 1px solid var(--sp-border);
|
border: 1px solid var(--sp-border);
|
||||||
|
|
||||||
&:hover { color: var(--sp-text, inherit); border-color: currentColor; }
|
&:hover { color: var(--sp-text, inherit); border-color: currentColor; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,7 +85,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 12px 16px;
|
padding: 10px 12px;
|
||||||
border-top: 1px solid var(--sp-border);
|
border-top: 1px solid var(--sp-border);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
/**
|
/**
|
||||||
* styles/components/_preset-save-dialog.less
|
* Preset Save Dialog — compact, polished design.
|
||||||
*
|
* Follows SP token system; mirrors Directors Board aesthetic.
|
||||||
* Layout for the Preset Save Dialog.
|
|
||||||
* All selectors scoped under .scrying-pool.
|
|
||||||
* Uses --sp-* tokens only — no Foundry --color-* / --font-* / --border-* tokens.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// The ApplicationV2 window root already has .scrying-pool .preset-save-dialog applied
|
@import "../tokens/_base.less";
|
||||||
// via DEFAULT_OPTIONS.classes. The content lives inside PARTS.
|
|
||||||
|
|
||||||
.scrying-pool.preset-save-dialog {
|
.scrying-pool.preset-save-dialog {
|
||||||
background: var(--sp-surface);
|
background: linear-gradient(175deg, hsl(220, 18%, 13%) 0%, hsl(220, 15%, 10%) 100%);
|
||||||
color: var(--sp-text-primary);
|
color: var(--sp-text-primary);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-top: 2px solid var(--sp-accent);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.55), 0 2px 8px rgba(0, 0, 0, 0.35),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||||
|
|
||||||
// ── Form ───────────────────────────────────────────────────────────────
|
// ── Form wrapper ──────────────────────────────────────────────────────────
|
||||||
.preset-save-dialog__form {
|
.preset-save-dialog__form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -21,21 +22,33 @@
|
|||||||
gap: 0;
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Header ────────────────────────────────────────────────────────────
|
// ── Header ────────────────────────────────────────────────────────────────
|
||||||
.preset-save-dialog__header {
|
.preset-save-dialog__header {
|
||||||
padding: 12px 16px;
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px 16px 10px;
|
||||||
border-bottom: 1px solid var(--sp-border);
|
border-bottom: 1px solid var(--sp-border);
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-save-dialog__header-icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--sp-accent);
|
||||||
|
margin-top: 1px;
|
||||||
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preset-save-dialog__title {
|
.preset-save-dialog__description {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-family: var(--font-primary, ui-sans-serif, system-ui, sans-serif);
|
||||||
font-weight: bold;
|
font-size: 12px;
|
||||||
color: var(--sp-text, inherit);
|
color: var(--sp-text-secondary);
|
||||||
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Body ──────────────────────────────────────────────────────────────
|
// ── Body ──────────────────────────────────────────────────────────────────
|
||||||
.preset-save-dialog__body {
|
.preset-save-dialog__body {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -45,69 +58,93 @@
|
|||||||
.preset-save-dialog__field {
|
.preset-save-dialog__field {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preset-save-dialog__label {
|
.preset-save-dialog__label {
|
||||||
font-size: 12px;
|
font-family: var(--font-primary, ui-sans-serif, system-ui, sans-serif);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
color: var(--sp-text-muted);
|
color: var(--sp-text-muted);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preset-save-dialog__input {
|
.preset-save-dialog__input {
|
||||||
|
width: 100%;
|
||||||
|
font-family: var(--font-primary, ui-sans-serif, system-ui, sans-serif);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 6px 10px;
|
padding: 9px 12px;
|
||||||
border: 1px solid var(--sp-border);
|
border: 1px solid var(--sp-border);
|
||||||
border-radius: 3px;
|
border-radius: 5px;
|
||||||
background: var(--sp-bg, #fff);
|
background: var(--sp-control-bg, #1a1d20);
|
||||||
color: var(--sp-text, inherit);
|
color: var(--sp-text-primary);
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color 0.15s, box-shadow 0.15s;
|
transition: border-color 0.15s, box-shadow 0.15s;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: var(--sp-accent, #4a6f9c);
|
border-color: var(--sp-accent);
|
||||||
box-shadow: 0 0 0 1px var(--sp-accent, #4a6f9c);
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--sp-accent) 22%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: var(--sp-text-muted);
|
color: var(--sp-text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Footer ────────────────────────────────────────────────────────────
|
// ── Footer ────────────────────────────────────────────────────────────────
|
||||||
.preset-save-dialog__footer {
|
.preset-save-dialog__footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 12px 16px;
|
padding: 10px 16px;
|
||||||
border-top: 1px solid var(--sp-border);
|
border-top: 1px solid var(--sp-border);
|
||||||
|
background: rgba(0, 0, 0, 0.15);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preset-save-dialog__btn {
|
.preset-save-dialog__btn {
|
||||||
|
font-family: var(--font-primary, ui-sans-serif, system-ui, sans-serif);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 6px 14px;
|
font-weight: 600;
|
||||||
border-radius: 3px;
|
padding: 7px 14px;
|
||||||
|
border-radius: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: opacity 0.15s;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
transition: background 0.15s, border-color 0.15s, opacity 0.15s;
|
||||||
|
|
||||||
|
i { font-size: 10px; opacity: 0.85; }
|
||||||
|
|
||||||
// Save button — primary
|
|
||||||
&--save {
|
&--save {
|
||||||
background: var(--sp-accent, #4a6f9c);
|
background: var(--sp-accent);
|
||||||
color: #fff;
|
color: var(--sp-accent-text, #fff);
|
||||||
border: none;
|
border: 1px solid transparent;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
|
||||||
&:hover { opacity: 0.85; }
|
&:hover { background: var(--sp-accent-hover); }
|
||||||
&:active { opacity: 0.7; }
|
&:active { background: var(--sp-accent-active); }
|
||||||
|
|
||||||
|
i { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel button — secondary
|
|
||||||
&--cancel {
|
&--cancel {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--sp-text-muted);
|
color: var(--sp-text-secondary);
|
||||||
border: 1px solid var(--sp-border);
|
border: 1px solid var(--sp-border);
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
&:hover { color: var(--sp-text, inherit); border-color: currentColor; }
|
&:hover {
|
||||||
|
color: var(--sp-text-primary);
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,27 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// ScryingPoolStrip Layout
|
// ScryingPoolStrip Layout
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
// Outer Foundry Application window (has .scrying-pool-strip via defaultOptions.classes).
|
||||||
|
// Only visual appearance — sizing is controlled by JS setPosition().
|
||||||
|
// WARNING: do NOT add max-width or overflow here; the outer window also carries this class
|
||||||
|
// and would clip the expanded inner content.
|
||||||
.scrying-pool-strip {
|
.scrying-pool-strip {
|
||||||
|
background: var(--sp-bg, hsl(220, 15%, 12%));
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
// Hide Foundry's default window header — replaced by a lightweight in-content button.
|
||||||
|
header.window-header { display: none; }
|
||||||
|
|
||||||
|
// Remove window-content padding so the strip fills the frame edge-to-edge.
|
||||||
|
.window-content {
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inner template div (has BOTH .scrying-pool AND .scrying-pool-strip).
|
||||||
|
// Controls the expand/collapse behaviour; safe to use max-width + overflow here.
|
||||||
|
.scrying-pool.scrying-pool-strip {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -26,17 +46,72 @@
|
|||||||
max-width: 44px;
|
max-width: 44px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: max-width 200ms ease-in-out;
|
transition: max-width 200ms ease-in-out;
|
||||||
background: var(--sp-bg, hsl(220, 15%, 12%));
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
&.is-expanded {
|
&.is-expanded {
|
||||||
max-width: 240px;
|
max-width: 240px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Drag grip (top bar, replaces window header drag affordance) ────────────────
|
||||||
|
.sp-strip__grip {
|
||||||
|
width: 100%;
|
||||||
|
height: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: grab;
|
||||||
|
color: var(--sp-text, hsl(0, 0%, 80%));
|
||||||
|
opacity: 0.35;
|
||||||
|
font-size: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover { opacity: 0.75; }
|
||||||
|
&:active { cursor: grabbing; opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lightweight close button (replaces window header) ─────────────────────────
|
||||||
|
.sp-strip__close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
z-index: 10;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 18px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 400;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--sp-text, hsl(0, 0%, 80%));
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.45;
|
||||||
|
transition: opacity 0.15s, background 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
&:active { opacity: 0.75; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Toolbar row: toggle + Director's Board on the same line ───────────────────
|
||||||
|
.sp-strip__toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
.sp-strip__toggle {
|
.sp-strip__toggle {
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 44px;
|
min-width: 44px;
|
||||||
|
height: 28px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -44,7 +119,39 @@
|
|||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--sp-text, hsl(0, 0%, 80%));
|
color: var(--sp-text, hsl(0, 0%, 80%));
|
||||||
|
font-size: 11px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
opacity: 0.6;
|
||||||
|
|
||||||
|
&:hover { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Director's Board CTA button (shown when sidebar injection not available) ──
|
||||||
|
.sp-strip__directors-board-cta {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 8px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-left: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--sp-text-secondary, #7a8390);
|
||||||
|
font-size: 11px;
|
||||||
|
text-align: left;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
i { font-size: 12px; flex-shrink: 0; }
|
||||||
|
span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
color: var(--sp-text-primary, #dde2e8);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sp-strip__participants {
|
.sp-strip__participants {
|
||||||
@@ -89,7 +196,59 @@
|
|||||||
|
|
||||||
.is-expanded & {
|
.is-expanded & {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 6px 8px;
|
height: 135px; // 16:9 at 240 px strip width
|
||||||
|
padding: 0;
|
||||||
|
align-items: flex-end;
|
||||||
|
background: hsl(220, 15%, 14%);
|
||||||
|
|
||||||
|
// Gradient scrim so name text is legible over any video
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 48px;
|
||||||
|
background: linear-gradient(transparent, hsla(0, 0%, 0%, 0.72));
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-avatar__img {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 1;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-avatar__name {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 8px;
|
||||||
|
right: 8px;
|
||||||
|
z-index: 3;
|
||||||
|
color: hsl(0, 0%, 95%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-avatar__state-label {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 6px;
|
||||||
|
left: 8px;
|
||||||
|
z-index: 3;
|
||||||
|
color: hsla(0, 0%, 85%, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-avatar__corner-badge {
|
||||||
|
bottom: 6px;
|
||||||
|
right: 6px;
|
||||||
|
z-index: 4;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,239 +6,201 @@
|
|||||||
|
|
||||||
// Panel container
|
// Panel container
|
||||||
.directors-board__preset-panel {
|
.directors-board__preset-panel {
|
||||||
// Base styles
|
background: linear-gradient(160deg, hsl(215,28%,13%) 0%, hsl(215,25%,10%) 100%);
|
||||||
background-color: var(--sp-surface);
|
|
||||||
border: 1px solid var(--sp-border);
|
border: 1px solid var(--sp-border);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 12px;
|
padding: 10px 12px 12px;
|
||||||
margin-top: 12px;
|
margin-top: 8px;
|
||||||
|
|
||||||
// Layout
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 0;
|
||||||
|
|
||||||
// Typography
|
font-size: 13px;
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
color: var(--sp-text-primary);
|
color: var(--sp-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Panel header
|
// Panel header
|
||||||
.directors-board__preset-panel-header {
|
.directors-board__preset-panel-header {
|
||||||
display: flex;
|
margin-bottom: 8px;
|
||||||
align-items: center;
|
padding-bottom: 7px;
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
border-bottom: 1px solid var(--sp-border-subtle);
|
border-bottom: 1px solid var(--sp-border-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.directors-board__preset-panel-title {
|
.directors-board__preset-panel-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 15px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--sp-text-primary);
|
color: var(--sp-text-primary);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Panel body
|
// Panel body
|
||||||
.directors-board__preset-panel-body {
|
.directors-board__preset-panel-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Panel row
|
// Panel row — single horizontal line
|
||||||
.directors-board__preset-panel-row {
|
.directors-board__preset-panel-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
&--hint {
|
&--hint {
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
color: var(--sp-text-secondary);
|
color: var(--sp-text-secondary);
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
border-top: 1px solid var(--sp-border-subtle);
|
border-top: 1px solid var(--sp-border-subtle);
|
||||||
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Panel label
|
// Panel label — wraps control + text in a horizontal flex row
|
||||||
.directors-board__preset-panel-label {
|
.directors-board__preset-panel-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
// Ensure proper spacing when label wraps
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle switch
|
// Toggle checkbox — compact and styled
|
||||||
.directors-board__preset-panel-toggle {
|
.directors-board__preset-panel-toggle {
|
||||||
// Button reset
|
flex-shrink: 0;
|
||||||
background: transparent;
|
appearance: none;
|
||||||
border: none;
|
-webkit-appearance: none;
|
||||||
padding: 0;
|
width: 14px;
|
||||||
margin: 0;
|
height: 14px;
|
||||||
cursor: pointer;
|
border: 1.5px solid var(--sp-border);
|
||||||
|
border-radius: 3px;
|
||||||
// Appearance
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
border: 2px solid var(--sp-border);
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: var(--sp-surface);
|
background-color: var(--sp-surface);
|
||||||
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: background-color 150ms ease, border-color 150ms ease;
|
transition: background-color 120ms ease, border-color 120ms ease;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
// Checked state
|
|
||||||
&:checked {
|
&:checked {
|
||||||
background-color: var(--sp-accent);
|
background-color: var(--sp-accent);
|
||||||
border-color: var(--sp-accent);
|
border-color: var(--sp-accent);
|
||||||
}
|
|
||||||
|
|
||||||
// After pseudo-element for toggle effect
|
&::after {
|
||||||
&:after {
|
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1px;
|
top: 1px;
|
||||||
left: 1px;
|
left: 3px;
|
||||||
width: 12px;
|
width: 5px;
|
||||||
height: 12px;
|
height: 8px;
|
||||||
background-color: var(--sp-surface);
|
border: 2px solid white;
|
||||||
border-radius: 2px;
|
border-top: none;
|
||||||
transition: transform 150ms ease, background-color 150ms ease;
|
border-left: none;
|
||||||
|
transform: rotate(40deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:checked:after {
|
|
||||||
transform: translateX(100%);
|
|
||||||
background-color: var(--sp-surface-inverse, white);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Focus state
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: 2px solid var(--sp-focus);
|
outline: 2px solid var(--sp-focus);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hover state
|
|
||||||
&:hover:not(:checked) {
|
|
||||||
background-color: var(--sp-surface-hover, rgba(0, 0, 0, 0.05));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preset selector
|
// Preset selector — fills remaining space
|
||||||
.directors-board__preset-panel-select {
|
.directors-board__preset-panel-select {
|
||||||
// Button reset
|
flex: 1;
|
||||||
background: transparent;
|
min-width: 0;
|
||||||
|
background: var(--sp-surface);
|
||||||
border: 1px solid var(--sp-border);
|
border: 1px solid var(--sp-border);
|
||||||
padding: 6px 8px;
|
|
||||||
margin: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
// Typography
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--sp-text-primary);
|
|
||||||
|
|
||||||
// Border and radius
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--sp-text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 120ms ease;
|
||||||
|
|
||||||
// Transition
|
|
||||||
transition: border-color 150ms ease, box-shadow 150ms ease;
|
|
||||||
|
|
||||||
// Hover state
|
|
||||||
&:hover:not(:disabled) {
|
&:hover:not(:disabled) {
|
||||||
border-color: var(--sp-border-hover, var(--sp-accent));
|
border-color: var(--sp-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Focus state
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--sp-focus);
|
border-color: var(--sp-focus);
|
||||||
box-shadow: 0 0 0 2px rgba(var(--sp-focus-rgb, 0, 0, 255), 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disabled state
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.45;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-delay slider container
|
// Pre-delay section — label | value badge | slider
|
||||||
|
.directors-board__preset-panel-delay-value {
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 38px;
|
||||||
|
text-align: right;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--sp-text-secondary);
|
||||||
|
background: rgba(0,0,0,0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 1px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.directors-board__preset-panel-slider {
|
.directors-board__preset-panel-slider {
|
||||||
// Remove default slider styling
|
flex: 1;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 120px;
|
height: 4px;
|
||||||
height: 6px;
|
background: rgba(255,255,255,0.12);
|
||||||
background: var(--sp-surface-subtle, rgba(0, 0, 0, 0.1));
|
border-radius: 2px;
|
||||||
border-radius: 3px;
|
|
||||||
outline: none;
|
outline: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
// Webkit slider thumb
|
|
||||||
&::-webkit-slider-thumb {
|
&::-webkit-slider-thumb {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 16px;
|
width: 12px;
|
||||||
height: 16px;
|
height: 12px;
|
||||||
background: var(--sp-accent);
|
background: var(--sp-accent);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 150ms ease;
|
transition: transform 120ms ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover { transform: scale(1.2); }
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Firefox slider thumb
|
|
||||||
&::-moz-range-thumb {
|
&::-moz-range-thumb {
|
||||||
width: 16px;
|
width: 12px;
|
||||||
height: 16px;
|
height: 12px;
|
||||||
background: var(--sp-accent);
|
background: var(--sp-accent);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: none;
|
border: none;
|
||||||
transition: transform 150ms ease;
|
transition: transform 120ms ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover { transform: scale(1.2); }
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Focus state
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delay value display
|
// Panel message (when no scene active)
|
||||||
.directors-board__preset-panel-delay-value {
|
|
||||||
display: inline-block;
|
|
||||||
min-width: 40px;
|
|
||||||
text-align: right;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--sp-text-secondary);
|
|
||||||
padding: 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Panel message (when no scene)
|
|
||||||
.directors-board__preset-panel-message {
|
.directors-board__preset-panel-message {
|
||||||
margin: 0;
|
margin: 4px 0 0;
|
||||||
color: var(--sp-text-secondary);
|
color: var(--sp-text-secondary);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 8px 0;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reduced motion: disable transitions
|
// Reduced motion
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.directors-board__preset-panel-toggle,
|
.directors-board__preset-panel-toggle,
|
||||||
.directors-board__preset-panel-select,
|
.directors-board__preset-panel-select,
|
||||||
@@ -246,9 +208,9 @@
|
|||||||
.directors-board__preset-panel-slider::-moz-range-thumb {
|
.directors-board__preset-panel-slider::-moz-range-thumb {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.directors-board__preset-panel-slider::-webkit-slider-thumb:hover,
|
.directors-board__preset-panel-slider::-webkit-slider-thumb:hover,
|
||||||
.directors-board__preset-panel-slider::-moz-range-thumb:hover {
|
.directors-board__preset-panel-slider::-moz-range-thumb:hover {
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+844
-186
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,9 @@
|
|||||||
// Story 3.2: ConfirmationBar and StripOverlayLayer
|
// Story 3.2: ConfirmationBar and StripOverlayLayer
|
||||||
@import "components/_strip-overlay-layer.less";
|
@import "components/_strip-overlay-layer.less";
|
||||||
@import "components/_confirmation-bar.less";
|
@import "components/_confirmation-bar.less";
|
||||||
|
// Story 3.1: Preset Save/Load Dialogs
|
||||||
|
@import "components/_preset-save-dialog.less";
|
||||||
|
@import "components/_preset-load-dialog.less";
|
||||||
// Story 3.3: Preset Import/Export Dialogs
|
// Story 3.3: Preset Import/Export Dialogs
|
||||||
@import "components/_preset-import-export.less";
|
@import "components/_preset-import-export.less";
|
||||||
// Story 4.1: Player Privacy Panel
|
// Story 4.1: Player Privacy Panel
|
||||||
@@ -75,6 +78,11 @@
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide Foundry's native camera dock — replaced by ScryingPoolCameraViews + our strip
|
||||||
|
#camera-views {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
// Also hide individual camera views in case they're rendered elsewhere
|
// Also hide individual camera views in case they're rendered elsewhere
|
||||||
.camera-view {
|
.camera-view {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
|
|||||||
@@ -21,18 +21,39 @@
|
|||||||
--sp-surface: var(--sp-theme-surface, var(--color-bg-option, #141618));
|
--sp-surface: var(--sp-theme-surface, var(--color-bg-option, #141618));
|
||||||
--sp-surface-raised: var(--sp-theme-surface-raised, #1c1f22);
|
--sp-surface-raised: var(--sp-theme-surface-raised, #1c1f22);
|
||||||
--sp-surface-elevated: var(--sp-theme-surface-elevated, var(--sp-surface-raised, #1c1f22));
|
--sp-surface-elevated: var(--sp-theme-surface-elevated, var(--sp-surface-raised, #1c1f22));
|
||||||
|
--sp-surface-subtle: var(--sp-theme-surface-subtle, rgba(255, 255, 255, 0.05));
|
||||||
|
--sp-surface-hover: var(--sp-theme-surface-hover, rgba(255, 255, 255, 0.08));
|
||||||
--sp-border: var(--sp-theme-border, var(--color-border, #282c30));
|
--sp-border: var(--sp-theme-border, var(--color-border, #282c30));
|
||||||
|
--sp-border-subtle: var(--sp-theme-border-subtle, rgba(255, 255, 255, 0.07));
|
||||||
|
|
||||||
/* Typography */
|
/* Typography */
|
||||||
--sp-text-primary: var(--sp-theme-text-primary, var(--color-text-primary, #dde2e8));
|
--sp-text-primary: var(--sp-theme-text-primary, var(--color-text-primary, #dde2e8));
|
||||||
--sp-text-secondary: var(--sp-theme-text-secondary,var(--color-text-secondary, #7a8390));
|
--sp-text-secondary: var(--sp-theme-text-secondary,var(--color-text-secondary, #7a8390));
|
||||||
--sp-text-muted: var(--sp-theme-text-muted, #555d66);
|
--sp-text-muted: var(--sp-theme-text-muted, #555d66);
|
||||||
|
--sp-font-size-sm: 11px;
|
||||||
|
|
||||||
/* Accent & interaction */
|
/* Accent & interaction */
|
||||||
--sp-accent: var(--sp-theme-accent, var(--color-warm-2, #4a9e6b));
|
--sp-accent: var(--sp-theme-accent, var(--color-warm-2, #4a9e6b));
|
||||||
|
--sp-accent-hover: var(--sp-theme-accent-hover, #3d8f5e);
|
||||||
|
--sp-accent-active: var(--sp-theme-accent-active, #2d7a4f);
|
||||||
|
--sp-accent-text: #fff;
|
||||||
--sp-surface-interactive: var(--sp-theme-interactive, #242830);
|
--sp-surface-interactive: var(--sp-theme-interactive, #242830);
|
||||||
--sp-control-bg: var(--sp-theme-control, #1a1d20);
|
--sp-control-bg: var(--sp-theme-control, #1a1d20);
|
||||||
|
|
||||||
|
/* Semantic colours */
|
||||||
|
--sp-color-red: hsl(0, 65%, 55%);
|
||||||
|
--sp-color-red-dark: hsl(0, 65%, 42%);
|
||||||
|
--sp-color-red-rgb: 204, 77, 77;
|
||||||
|
--sp-color-red-subtle: hsla(0, 65%, 55%, 0.12);
|
||||||
|
--sp-color-green: hsl(140, 55%, 50%);
|
||||||
|
--sp-color-green-subtle: hsla(140, 55%, 50%, 0.12);
|
||||||
|
--sp-color-yellow: hsl(48, 88%, 55%);
|
||||||
|
--sp-color-yellow-subtle: hsla(48, 88%, 55%, 0.12);
|
||||||
|
|
||||||
|
/* Shape & motion */
|
||||||
|
--sp-radius-sm: 3px;
|
||||||
|
--sp-radius-md: 4px;
|
||||||
|
|
||||||
/* Focus ring — module-wide keyboard navigation anchor */
|
/* Focus ring — module-wide keyboard navigation anchor */
|
||||||
--sp-focus: var(--sp-theme-focus, var(--color-focus-outline, #63c287));
|
--sp-focus: var(--sp-theme-focus, var(--color-focus-outline, #63c287));
|
||||||
--sp-focus-ring: 0 0 0 2px var(--sp-focus);
|
--sp-focus-ring: 0 0 0 2px var(--sp-focus);
|
||||||
@@ -51,11 +72,16 @@
|
|||||||
--sp-theme-surface: #141618;
|
--sp-theme-surface: #141618;
|
||||||
--sp-theme-surface-raised: #1c1f22;
|
--sp-theme-surface-raised: #1c1f22;
|
||||||
--sp-theme-surface-elevated: #1c1f22;
|
--sp-theme-surface-elevated: #1c1f22;
|
||||||
|
--sp-theme-surface-subtle: rgba(255, 255, 255, 0.05);
|
||||||
|
--sp-theme-surface-hover: rgba(255, 255, 255, 0.08);
|
||||||
--sp-theme-border: #282c30;
|
--sp-theme-border: #282c30;
|
||||||
|
--sp-theme-border-subtle: rgba(255, 255, 255, 0.07);
|
||||||
--sp-theme-text-primary: #dde2e8;
|
--sp-theme-text-primary: #dde2e8;
|
||||||
--sp-theme-text-secondary: #7a8390;
|
--sp-theme-text-secondary: #7a8390;
|
||||||
--sp-theme-text-muted: #555d66;
|
--sp-theme-text-muted: #555d66;
|
||||||
--sp-theme-accent: #4a9e6b;
|
--sp-theme-accent: #4a9e6b;
|
||||||
|
--sp-theme-accent-hover: #3d8f5e;
|
||||||
|
--sp-theme-accent-active: #2d7a4f;
|
||||||
--sp-theme-interactive: #242830;
|
--sp-theme-interactive: #242830;
|
||||||
--sp-theme-control: #1a1d20;
|
--sp-theme-control: #1a1d20;
|
||||||
--sp-theme-focus: #63c287;
|
--sp-theme-focus: #63c287;
|
||||||
|
|||||||
@@ -1,58 +1,110 @@
|
|||||||
{{!-- Director's Board — GM camera-management overview window --}}
|
{{!-- Director's Board — GM camera-management overview window --}}
|
||||||
<div class="directors-board__inner">
|
<div class="directors-board__inner">
|
||||||
|
|
||||||
|
{{!-- Drag grip --}}
|
||||||
|
<div class="directors-board__grip" data-action="drag-grip" aria-hidden="true">
|
||||||
|
<i class="fas fa-grip-lines"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button"
|
||||||
|
class="directors-board__close-btn"
|
||||||
|
data-action="close"
|
||||||
|
aria-label="{{localize "scrying-pool.directorsBoard.close"}}"
|
||||||
|
data-tooltip="{{localize "scrying-pool.directorsBoard.close"}}">×</button>
|
||||||
|
|
||||||
<section class="scrying-pool directors-board__content"
|
<section class="scrying-pool directors-board__content"
|
||||||
role="list"
|
role="list"
|
||||||
aria-label="{{localize "video-view-manager.directorsBoard.title"}}">
|
aria-label="{{localize "scrying-pool.directorsBoard.title"}}">
|
||||||
|
|
||||||
{{#unless isEmpty}}
|
{{#unless isEmpty}}
|
||||||
{{#each participants}}
|
{{#each participants}}
|
||||||
{{> "modules/video-view-manager/templates/participant-card.hbs"}}
|
{{> "modules/scrying-pool/templates/participant-card.hbs"}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="directors-board__empty" role="listitem">
|
<p class="directors-board__empty" role="listitem">
|
||||||
{{localize "video-view-manager.directorsBoard.empty"}}
|
{{localize "scrying-pool.directorsBoard.empty"}}
|
||||||
</p>
|
</p>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="directors-board__bulk-bar">
|
<div class="directors-board__bulk-bar">
|
||||||
<button type="button" class="directors-board__bulk-btn" data-action="show-all">
|
<button type="button" class="directors-board__bulk-btn" data-action="show-all"
|
||||||
{{localize "video-view-manager.directorsBoard.bulk.showAll"}}
|
data-tooltip="{{localize "scrying-pool.directorsBoard.bulk.showAll"}}">
|
||||||
|
<i class="fas fa-eye" aria-hidden="true"></i>
|
||||||
|
{{localize "scrying-pool.directorsBoard.bulk.showAll"}}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="directors-board__bulk-btn" data-action="hide-all">
|
<button type="button" class="directors-board__bulk-btn" data-action="hide-all"
|
||||||
{{localize "video-view-manager.directorsBoard.bulk.hideAll"}}
|
data-tooltip="{{localize "scrying-pool.directorsBoard.bulk.hideAll"}}">
|
||||||
|
<i class="fas fa-eye-slash" aria-hidden="true"></i>
|
||||||
|
{{localize "scrying-pool.directorsBoard.bulk.hideAll"}}
|
||||||
</button>
|
</button>
|
||||||
{{#if hasUndo}}
|
{{#if hasUndo}}
|
||||||
<button type="button" class="directors-board__bulk-btn directors-board__bulk-btn--undo" data-action="undo">
|
<button type="button" class="directors-board__bulk-btn directors-board__bulk-btn--undo" data-action="undo"
|
||||||
{{localize "video-view-manager.directorsBoard.bulk.undo"}}
|
data-tooltip="{{localize "scrying-pool.directorsBoard.bulk.undo"}}">
|
||||||
|
<i class="fas fa-undo" aria-hidden="true"></i>
|
||||||
|
{{localize "scrying-pool.directorsBoard.bulk.undo"}}
|
||||||
</button>
|
</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if hasRestore}}
|
{{#if hasRestore}}
|
||||||
<button type="button" class="directors-board__bulk-btn directors-board__bulk-btn--restore" data-action="restore-spotlight">
|
<button type="button" class="directors-board__bulk-btn directors-board__bulk-btn--restore" data-action="restore-spotlight"
|
||||||
{{localize "video-view-manager.directorsBoard.bulk.restore"}}
|
data-tooltip="{{localize "scrying-pool.directorsBoard.bulk.restore"}}">
|
||||||
|
<i class="fas fa-star" aria-hidden="true"></i>
|
||||||
|
{{localize "scrying-pool.directorsBoard.bulk.restore"}}
|
||||||
</button>
|
</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<button type="button" class="directors-board__help-btn" data-action="open-shortcut-panel" aria-label="{{localize "video-view-manager.directorsBoard.shortcuts.openPanel"}}">?</button>
|
<button type="button" class="directors-board__help-btn" data-action="open-shortcut-panel"
|
||||||
|
aria-label="{{localize "scrying-pool.directorsBoard.shortcuts.openPanel"}}"
|
||||||
|
data-tooltip="{{localize "scrying-pool.directorsBoard.shortcuts.openPanel"}}">?</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="directors-board__footer">
|
<footer class="directors-board__footer">
|
||||||
<button type="button" class="directors-board__footer-btn" data-action="save-preset">
|
<div class="directors-board__footer-group directors-board__footer-group--presets">
|
||||||
{{localize "video-view-manager.directorsBoard.footer.savePreset"}}
|
<button type="button" class="directors-board__footer-btn" data-action="save-preset"
|
||||||
|
data-tooltip="{{localize "scrying-pool.directorsBoard.footer.savePreset"}}">
|
||||||
|
<i class="fas fa-save" aria-hidden="true"></i>
|
||||||
|
{{localize "scrying-pool.directorsBoard.footer.savePreset"}}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="directors-board__footer-btn" data-action="load-preset" {{#unless hasPresets}}disabled{{/unless}}>
|
<button type="button" class="directors-board__footer-btn" data-action="load-preset"
|
||||||
{{localize "video-view-manager.directorsBoard.footer.loadPreset"}}
|
data-tooltip="{{localize "scrying-pool.directorsBoard.footer.loadPreset"}}"
|
||||||
|
{{#unless hasPresets}}disabled{{/unless}}>
|
||||||
|
<i class="fas fa-folder-open" aria-hidden="true"></i>
|
||||||
|
{{localize "scrying-pool.directorsBoard.footer.loadPreset"}}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="directors-board__footer-btn" data-action="export-presets">
|
<button type="button" class="directors-board__footer-btn directors-board__footer-btn--secondary" data-action="export-presets"
|
||||||
{{localize "video-view-manager.directorsBoard.footer.exportPresets"}}
|
data-tooltip="{{localize "scrying-pool.directorsBoard.footer.exportPresets"}}">
|
||||||
|
<i class="fas fa-file-export" aria-hidden="true"></i>
|
||||||
|
{{localize "scrying-pool.directorsBoard.footer.exportPresets"}}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="directors-board__footer-btn" data-action="import-presets">
|
<button type="button" class="directors-board__footer-btn directors-board__footer-btn--secondary" data-action="import-presets"
|
||||||
{{localize "video-view-manager.directorsBoard.footer.importPresets"}}
|
data-tooltip="{{localize "scrying-pool.directorsBoard.footer.importPresets"}}">
|
||||||
|
<i class="fas fa-file-import" aria-hidden="true"></i>
|
||||||
|
{{localize "scrying-pool.directorsBoard.footer.importPresets"}}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="directors-board__footer-btn" data-action="toggle-preset-panel">
|
</div>
|
||||||
{{localize "video-view-manager.directorsBoard.footer.autoApplySettings"}}
|
<div class="directors-board__footer-group directors-board__footer-group--controls">
|
||||||
|
<button type="button" class="directors-board__footer-btn directors-board__footer-btn--auto-apply" data-action="toggle-preset-panel"
|
||||||
|
data-tooltip="{{localize "scrying-pool.directorsBoard.footer.autoApplySettings"}}">
|
||||||
|
<i class="fas fa-magic" aria-hidden="true"></i>
|
||||||
|
{{localize "scrying-pool.directorsBoard.footer.autoApplySettings"}}
|
||||||
</button>
|
</button>
|
||||||
|
<div class="directors-board__footer-sep" role="separator" aria-hidden="true"></div>
|
||||||
|
<button type="button" class="directors-board__footer-btn directors-board__footer-btn--av{{#if avModeEnabled}} directors-board__footer-btn--av-active{{/if}}" data-action="toggle-av-mode"
|
||||||
|
data-tooltip="{{#if avModeEnabled}}{{localize "scrying-pool.directorsBoard.footer.avModeDisable"}}{{else}}{{localize "scrying-pool.directorsBoard.footer.avModeEnable"}}{{/if}}">
|
||||||
|
{{#if avModeEnabled}}
|
||||||
|
<i class="fas fa-video-slash" aria-hidden="true"></i>
|
||||||
|
{{localize "scrying-pool.directorsBoard.footer.avModeDisable"}}
|
||||||
|
{{else}}
|
||||||
|
<i class="fas fa-video" aria-hidden="true"></i>
|
||||||
|
{{localize "scrying-pool.directorsBoard.footer.avModeEnable"}}
|
||||||
|
{{/if}}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="directors-board__footer-btn directors-board__footer-btn--av-config" data-action="open-av-config"
|
||||||
|
data-tooltip="{{localize "scrying-pool.directorsBoard.footer.avConfigTitle"}}">
|
||||||
|
<i class="fas fa-sliders-h" aria-hidden="true"></i>
|
||||||
|
{{localize "scrying-pool.directorsBoard.footer.avConfig"}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
{{!-- Scene Preset Panel - rendered via JavaScript, not Handlebars --}}
|
{{!-- Scene Preset Panel - rendered via JavaScript, not Handlebars --}}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
data-user-id="{{userId}}"
|
data-user-id="{{userId}}"
|
||||||
role="button"
|
role="button"
|
||||||
aria-label="{{toggleAriaLabel}}"
|
aria-label="{{toggleAriaLabel}}"
|
||||||
|
data-tooltip="{{toggleAriaLabel}}"
|
||||||
tabindex="-1">
|
tabindex="-1">
|
||||||
<i class="fas {{#if isHidden}}fa-eye{{else}}fa-eye-slash{{/if}}" aria-hidden="true"></i>
|
<i class="fas {{#if isHidden}}fa-eye{{else}}fa-eye-slash{{/if}}" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
{{!-- Player Privacy Panel --}}
|
{{!-- Player Privacy Panel --}}
|
||||||
<div class="player-privacy-panel__container">
|
<div class="player-privacy-panel__container">
|
||||||
<header class="player-privacy-panel__header">
|
|
||||||
<h2 class="player-privacy-panel__title">{{title}}</h2>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="player-privacy-panel__body">
|
<div class="player-privacy-panel__body">
|
||||||
{{#if isReadOnly}}
|
{{#if isReadOnly}}
|
||||||
<div class="player-privacy-panel__notice player-privacy-panel__notice--readonly">
|
<div class="player-privacy-panel__notice player-privacy-panel__notice--readonly">
|
||||||
@@ -15,7 +11,7 @@
|
|||||||
<h3 class="player-privacy-panel__section-header">{{sectionHeader}}</h3>
|
<h3 class="player-privacy-panel__section-header">{{sectionHeader}}</h3>
|
||||||
|
|
||||||
<p class="player-privacy-panel__section-description">
|
<p class="player-privacy-panel__section-description">
|
||||||
{{SCRYING_POOL.PrivacyPanel.sectionDescription}}
|
{{localize "SCRYING_POOL.PrivacyPanel.sectionDescription"}}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="player-privacy-panel__effects-list">
|
<div class="player-privacy-panel__effects-list">
|
||||||
@@ -24,18 +20,22 @@
|
|||||||
<div class="player-privacy-panel__effect-header">
|
<div class="player-privacy-panel__effect-header">
|
||||||
<h4 class="player-privacy-panel__effect-label">{{label}}</h4>
|
<h4 class="player-privacy-panel__effect-label">{{label}}</h4>
|
||||||
<div class="player-privacy-panel__toggle">
|
<div class="player-privacy-panel__toggle">
|
||||||
<label class="player-privacy-panel__toggle-label">
|
<label class="player-privacy-panel__toggle-label"
|
||||||
|
data-tooltip="{{description}}">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
{{#if enabled}}checked{{/if}}
|
{{#if enabled}}checked{{/if}}
|
||||||
{{#if ../isReadOnly}}disabled{{/if}}
|
{{#if ../isReadOnly}}disabled{{/if}}
|
||||||
data-setting="{{settingKey}}"
|
data-setting="{{settingKey}}"
|
||||||
class="player-privacy-panel__toggle-input"
|
class="player-privacy-panel__toggle-input"
|
||||||
|
aria-label="{{label}}"
|
||||||
>
|
>
|
||||||
<span class="player-privacy-panel__toggle-text">
|
<span class="player-privacy-panel__toggle-text">
|
||||||
{{#if enabled}}
|
{{#if enabled}}
|
||||||
|
<i class="fas fa-check" aria-hidden="true"></i>
|
||||||
{{../toggleOnLabel}}
|
{{../toggleOnLabel}}
|
||||||
{{else}}
|
{{else}}
|
||||||
|
<i class="fas fa-times" aria-hidden="true"></i>
|
||||||
{{../toggleOffLabel}}
|
{{../toggleOffLabel}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</span>
|
</span>
|
||||||
@@ -64,7 +64,10 @@
|
|||||||
|
|
||||||
{{#unless isReadOnly}}
|
{{#unless isReadOnly}}
|
||||||
<div class="player-privacy-panel__portrait-actions">
|
<div class="player-privacy-panel__portrait-actions">
|
||||||
<button type="button" class="player-privacy-panel__portrait-choose sp-btn sp-btn--primary">
|
<button type="button"
|
||||||
|
class="player-privacy-panel__portrait-choose sp-btn sp-btn--primary"
|
||||||
|
data-tooltip="{{chooseImageLabel}}">
|
||||||
|
<i class="fas fa-image" aria-hidden="true"></i>
|
||||||
{{chooseImageLabel}}
|
{{chooseImageLabel}}
|
||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
@@ -74,7 +77,10 @@
|
|||||||
style="display: none;"
|
style="display: none;"
|
||||||
>
|
>
|
||||||
{{#if hasCustomPortrait}}
|
{{#if hasCustomPortrait}}
|
||||||
<button type="button" class="player-privacy-panel__portrait-remove sp-btn sp-btn--secondary">
|
<button type="button"
|
||||||
|
class="player-privacy-panel__portrait-remove sp-btn sp-btn--secondary"
|
||||||
|
data-tooltip="{{removeImageLabel}}">
|
||||||
|
<i class="fas fa-trash-alt" aria-hidden="true"></i>
|
||||||
{{removeImageLabel}}
|
{{removeImageLabel}}
|
||||||
</button>
|
</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -1,29 +1,31 @@
|
|||||||
<div class="sp-dialog-content">
|
<div class="sp-preset-export-dialog">
|
||||||
|
<div class="sp-dialog-content">
|
||||||
<p class="sp-export-description">
|
<p class="sp-export-description">
|
||||||
{{localize "video-view-manager.presetExport.description"}}
|
{{localize "scrying-pool.presetExport.description"}}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="sp-export-info">
|
<div class="sp-export-info">
|
||||||
<span class="sp-info-label">{{localize "video-view-manager.presetExport.scene"}}:</span>
|
<span class="sp-info-label">{{localize "scrying-pool.presetExport.scene"}}:</span>
|
||||||
<span class="sp-info-value">{{sceneName}}</span>
|
<span class="sp-info-value">{{sceneName}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sp-export-info">
|
<div class="sp-export-info">
|
||||||
<span class="sp-info-label">{{localize "video-view-manager.presetExport.presetCount"}}:</span>
|
<span class="sp-info-label">{{localize "scrying-pool.presetExport.presetCount"}}:</span>
|
||||||
<span class="sp-info-value">{{presetCount}}</span>
|
<span class="sp-info-value">{{presetCount}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sp-export-info">
|
<div class="sp-export-info">
|
||||||
<span class="sp-info-label">{{localize "video-view-manager.presetExport.filename"}}:</span>
|
<span class="sp-info-label">{{localize "scrying-pool.presetExport.filename"}}:</span>
|
||||||
<span class="sp-info-value sp-filename">{{filename}}</span>
|
<span class="sp-info-value sp-filename">{{filename}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sp-dialog-buttons">
|
<div class="sp-dialog-buttons">
|
||||||
<button type="button" class="sp-btn sp-btn-primary sp-export-btn">
|
<button type="button" class="sp-btn sp-btn-primary sp-export-btn">
|
||||||
<i class="fas fa-download"></i> {{localize "video-view-manager.presetExport.export"}}
|
<i class="fas fa-download"></i> {{localize "scrying-pool.presetExport.export"}}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="sp-btn sp-btn-secondary" data-action="close">
|
<button type="button" class="sp-btn sp-btn-secondary" data-action="close">
|
||||||
{{localize "video-view-manager.presetExport.cancel"}}
|
{{localize "scrying-pool.presetExport.cancel"}}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+17
-15
@@ -1,23 +1,24 @@
|
|||||||
<div class="sp-dialog-content">
|
<div class="sp-preset-import-dialog">
|
||||||
|
<div class="sp-dialog-content">
|
||||||
<p class="sp-import-description">
|
<p class="sp-import-description">
|
||||||
{{localize "video-view-manager.presetImport.description"}}
|
{{localize "scrying-pool.presetImport.description"}}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{{#if hasExistingPresets}}
|
{{#if hasExistingPresets}}
|
||||||
<div class="sp-warning-box">
|
<div class="sp-warning-box">
|
||||||
<i class="fas fa-exclamation-triangle"></i>
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
<span>{{localize "video-view-manager.presetImport.existingPresetsWarning" existingPresetCount=existingPresetCount}}</span>
|
<span>{{localize "scrying-pool.presetImport.existingPresetsWarning" existingPresetCount=existingPresetCount}}</span>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{!-- File Selection --}}
|
{{!-- File Selection --}}
|
||||||
<div class="sp-form-group">
|
<div class="sp-form-group">
|
||||||
<label class="sp-form-label">{{localize "video-view-manager.presetImport.selectFile"}}</label>
|
<label class="sp-form-label">{{localize "scrying-pool.presetImport.selectFile"}}</label>
|
||||||
<div class="sp-file-upload">
|
<div class="sp-file-upload">
|
||||||
<input type="file" class="sp-file-input" accept=".json" />
|
<input type="file" class="sp-file-input" accept=".json" />
|
||||||
<label class="sp-file-label">
|
<label class="sp-file-label">
|
||||||
<i class="fas fa-upload"></i>
|
<i class="fas fa-upload"></i>
|
||||||
<span class="sp-file-text">{{localize "video-view-manager.presetImport.chooseFile"}}</span>
|
<span class="sp-file-text">{{localize "scrying-pool.presetImport.chooseFile"}}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{{#if selectedFileName}}
|
{{#if selectedFileName}}
|
||||||
@@ -30,17 +31,17 @@
|
|||||||
|
|
||||||
{{!-- Mode Selection --}}
|
{{!-- Mode Selection --}}
|
||||||
<div class="sp-form-group">
|
<div class="sp-form-group">
|
||||||
<label class="sp-form-label">{{localize "video-view-manager.presetImport.importMode"}}</label>
|
<label class="sp-form-label">{{localize "scrying-pool.presetImport.importMode"}}</label>
|
||||||
<div class="sp-radio-group">
|
<div class="sp-radio-group">
|
||||||
<label class="sp-radio-label">
|
<label class="sp-radio-label">
|
||||||
<input type="radio" name="import-mode" class="sp-mode-merge" value="merge" {{checked (eq mode "merge")}} />
|
<input type="radio" name="import-mode" class="sp-mode-merge" value="merge" {{checked (eq mode "merge")}} />
|
||||||
<span class="sp-radio-text">{{mergeLabel}}</span>
|
<span class="sp-radio-text">{{mergeLabel}}</span>
|
||||||
<span class="sp-radio-hint">{{localize "video-view-manager.presetImport.importModeMergeHint"}}</span>
|
<span class="sp-radio-hint">{{localize "scrying-pool.presetImport.importModeMergeHint"}}</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="sp-radio-label">
|
<label class="sp-radio-label">
|
||||||
<input type="radio" name="import-mode" class="sp-mode-replace" value="replace" {{checked (eq mode "replace")}} />
|
<input type="radio" name="import-mode" class="sp-mode-replace" value="replace" {{checked (eq mode "replace")}} />
|
||||||
<span class="sp-radio-text">{{replaceLabel}}</span>
|
<span class="sp-radio-text">{{replaceLabel}}</span>
|
||||||
<span class="sp-radio-hint">{{localize "video-view-manager.presetImport.importModeReplaceHint"}}</span>
|
<span class="sp-radio-hint">{{localize "scrying-pool.presetImport.importModeReplaceHint"}}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
{{!-- Preview Section --}}
|
{{!-- Preview Section --}}
|
||||||
{{#if previewItems.length}}
|
{{#if previewItems.length}}
|
||||||
<div class="sp-preview-section">
|
<div class="sp-preview-section">
|
||||||
<h3 class="sp-preview-title">{{localize "video-view-manager.presetImport.previewTitle"}}</h3>
|
<h3 class="sp-preview-title">{{localize "scrying-pool.presetImport.previewTitle"}}</h3>
|
||||||
<ul class="sp-preview-list">
|
<ul class="sp-preview-list">
|
||||||
{{#each previewItems as |item|}}
|
{{#each previewItems as |item|}}
|
||||||
<li class="sp-preview-item {{unless item.valid 'sp-preview-item--invalid'}}">
|
<li class="sp-preview-item {{unless item.valid 'sp-preview-item--invalid'}}">
|
||||||
@@ -68,23 +69,24 @@
|
|||||||
<div class="sp-confirmation-section">
|
<div class="sp-confirmation-section">
|
||||||
<div class="sp-confirmation-warning">
|
<div class="sp-confirmation-warning">
|
||||||
<i class="fas fa-exclamation-triangle"></i>
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
<span>{{localize "video-view-manager.presetImport.replaceConfirmation" existingPresetCount=existingPresetCount}}</span>
|
<span>{{localize "scrying-pool.presetImport.replaceConfirmation" existingPresetCount=existingPresetCount}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sp-dialog-buttons">
|
<div class="sp-dialog-buttons">
|
||||||
{{#unless requiresConfirmation}}
|
{{#unless requiresConfirmation}}
|
||||||
<button type="button" class="sp-btn sp-btn-primary sp-import-btn" {{disabled (not previewItems.length) }}>
|
<button type="button" class="sp-btn sp-btn-primary sp-import-btn" {{disabled (not previewItems.length) }}>
|
||||||
<i class="fas fa-file-import"></i> {{localize "video-view-manager.presetImport.import"}}
|
<i class="fas fa-file-import"></i> {{localize "scrying-pool.presetImport.import"}}
|
||||||
</button>
|
</button>
|
||||||
{{else}}
|
{{else}}
|
||||||
<button type="button" class="sp-btn sp-btn-danger sp-confirm-btn">
|
<button type="button" class="sp-btn sp-btn-danger sp-confirm-btn">
|
||||||
<i class="fas fa-check"></i> {{localize "video-view-manager.presetImport.confirmReplace"}}
|
<i class="fas fa-check"></i> {{localize "scrying-pool.presetImport.confirmReplace"}}
|
||||||
</button>
|
</button>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
<button type="button" class="sp-btn sp-btn-secondary sp-cancel-btn">
|
<button type="button" class="sp-btn sp-btn-secondary sp-cancel-btn">
|
||||||
{{localize "video-view-manager.presetImport.cancel"}}
|
{{localize "scrying-pool.presetImport.cancel"}}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
{{!-- Load Scene Preset Dialog --}}
|
{{!-- Load Camera Layout Dialog --}}
|
||||||
<div class="preset-load-dialog__content">
|
<div class="preset-load-dialog__content">
|
||||||
<header class="preset-load-dialog__header">
|
|
||||||
<h2 class="preset-load-dialog__title">{{title}}</h2>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="preset-load-dialog__body">
|
<div class="preset-load-dialog__body">
|
||||||
{{#if hasPresets}}
|
{{#if hasPresets}}
|
||||||
@@ -15,6 +12,7 @@
|
|||||||
data-action="load"
|
data-action="load"
|
||||||
data-preset-name="{{name}}"
|
data-preset-name="{{name}}"
|
||||||
>
|
>
|
||||||
|
<i class="fas fa-layer-group" aria-hidden="true"></i>
|
||||||
{{name}}
|
{{name}}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
{{!-- Save Scene Preset Dialog --}}
|
{{!-- Save Camera Layout Dialog --}}
|
||||||
<form class="preset-save-dialog__form">
|
<form class="preset-save-dialog__form">
|
||||||
<header class="preset-save-dialog__header">
|
|
||||||
<h2 class="preset-save-dialog__title">{{title}}</h2>
|
<div class="preset-save-dialog__header">
|
||||||
</header>
|
<i class="fas fa-bookmark preset-save-dialog__header-icon"></i>
|
||||||
|
<p class="preset-save-dialog__description">{{descriptionHint}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="preset-save-dialog__body">
|
<div class="preset-save-dialog__body">
|
||||||
<div class="preset-save-dialog__field">
|
<div class="preset-save-dialog__field">
|
||||||
@@ -25,9 +27,11 @@
|
|||||||
|
|
||||||
<footer class="preset-save-dialog__footer">
|
<footer class="preset-save-dialog__footer">
|
||||||
<button type="button" class="preset-save-dialog__btn preset-save-dialog__btn--cancel" data-action="cancel">
|
<button type="button" class="preset-save-dialog__btn preset-save-dialog__btn--cancel" data-action="cancel">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
{{cancelLabel}}
|
{{cancelLabel}}
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" class="preset-save-dialog__btn preset-save-dialog__btn--save">
|
<button type="submit" class="preset-save-dialog__btn preset-save-dialog__btn--save">
|
||||||
|
<i class="fas fa-bookmark"></i>
|
||||||
{{saveLabel}}
|
{{saveLabel}}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -3,6 +3,16 @@
|
|||||||
role="complementary"
|
role="complementary"
|
||||||
aria-label="Scrying Pool">
|
aria-label="Scrying Pool">
|
||||||
|
|
||||||
|
{{!-- Drag grip (top bar, replaces window header drag affordance) --}}
|
||||||
|
<div class="sp-strip__grip" data-action="drag-grip" aria-hidden="true">
|
||||||
|
<i class="fas fa-grip-lines"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{!-- Lightweight close button (top-right corner, replaces window header) --}}
|
||||||
|
<button class="sp-strip__close-btn" data-action="close-strip"
|
||||||
|
aria-label="Close Scrying Pool"
|
||||||
|
data-tooltip="Close">×</button>
|
||||||
|
|
||||||
{{!-- First-open tip (right-click affordance) --}}
|
{{!-- First-open tip (right-click affordance) --}}
|
||||||
{{#if showFirstOpenTip}}
|
{{#if showFirstOpenTip}}
|
||||||
<p class="sp-strip__first-tip">
|
<p class="sp-strip__first-tip">
|
||||||
@@ -11,19 +21,22 @@
|
|||||||
</p>
|
</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{!-- Expand/collapse toggle --}}
|
{{!-- Toolbar row: toggle + Director's Board side by side --}}
|
||||||
|
<div class="sp-strip__toolbar">
|
||||||
<button class="sp-strip__toggle" data-action="toggle-expanded"
|
<button class="sp-strip__toggle" data-action="toggle-expanded"
|
||||||
aria-label="{{#if isExpanded}}Collapse Scrying Pool{{else}}Expand Scrying Pool{{/if}}"
|
aria-label="{{#if isExpanded}}Collapse Scrying Pool{{else}}Expand Scrying Pool{{/if}}"
|
||||||
aria-expanded="{{isExpanded}}">
|
aria-expanded="{{isExpanded}}"
|
||||||
|
data-tooltip="{{#if isExpanded}}Collapse{{else}}Expand{{/if}}">
|
||||||
<i class="fas fa-chevron-{{#if isExpanded}}left{{else}}right{{/if}}"></i>
|
<i class="fas fa-chevron-{{#if isExpanded}}left{{else}}right{{/if}}"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{{!-- Director's Board CTA button (fallback when sidebar API unavailable) --}}
|
|
||||||
<button class="sp-strip__directors-board-cta" data-action="open-directors-board"
|
<button class="sp-strip__directors-board-cta" data-action="open-directors-board"
|
||||||
aria-label="Open Director's Board">
|
aria-label="Open Director's Board"
|
||||||
|
data-tooltip="Director's Board">
|
||||||
<i class="fas fa-border-all" aria-hidden="true"></i>
|
<i class="fas fa-border-all" aria-hidden="true"></i>
|
||||||
<span>Director's Board</span>
|
<span>Director's Board</span>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{!-- Participant list --}}
|
{{!-- Participant list --}}
|
||||||
<ul class="sp-strip__participants" role="list">
|
<ul class="sp-strip__participants" role="list">
|
||||||
@@ -42,6 +55,7 @@
|
|||||||
data-action="open-popover"
|
data-action="open-popover"
|
||||||
role="button"
|
role="button"
|
||||||
aria-label="{{name}} — {{stateLabel}}"
|
aria-label="{{name}} — {{stateLabel}}"
|
||||||
|
data-tooltip="{{name}} — {{stateLabel}}"
|
||||||
aria-pressed="false">
|
aria-pressed="false">
|
||||||
|
|
||||||
{{!-- Video container for stream-access mode (full AV replacement) --}}
|
{{!-- Video container for stream-access mode (full AV replacement) --}}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{{!-- Scene Preset Panel - per-scene auto-apply configuration --}}
|
{{!-- Camera Layout Auto-Apply Panel - per-scene configuration --}}
|
||||||
<div class="directors-board__preset-panel-header">
|
<div class="directors-board__preset-panel-header">
|
||||||
<h3 class="directors-board__preset-panel-title">{{localize "video-view-manager.scenePresetPanel.title"}}</h3>
|
<h3 class="directors-board__preset-panel-title">{{localize "scrying-pool.scenePresetPanel.title"}}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#if hasScene}}
|
{{#if hasScene}}
|
||||||
@@ -12,20 +12,20 @@
|
|||||||
data-action="toggle-auto-apply"
|
data-action="toggle-auto-apply"
|
||||||
{{#if enabled}}checked{{/if}}
|
{{#if enabled}}checked{{/if}}
|
||||||
role="switch"
|
role="switch"
|
||||||
aria-label="{{localize 'video-view-manager.scenePresetPanel.enableAutoApply'}}">
|
aria-label="{{localize 'scrying-pool.scenePresetPanel.enableAutoApply'}}">
|
||||||
{{localize "video-view-manager.scenePresetPanel.enableAutoApply"}}
|
{{localize "scrying-pool.scenePresetPanel.enableAutoApply"}}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="directors-board__preset-panel-row">
|
<div class="directors-board__preset-panel-row">
|
||||||
<label class="directors-board__preset-panel-label">
|
<label class="directors-board__preset-panel-label">
|
||||||
{{localize "video-view-manager.scenePresetPanel.preset"}}
|
{{localize "scrying-pool.scenePresetPanel.preset"}}
|
||||||
<select class="directors-board__preset-panel-select"
|
<select class="directors-board__preset-panel-select"
|
||||||
data-action="select-preset"
|
data-action="select-preset"
|
||||||
{{#unless hasPresets}}disabled{{/unless}}
|
{{#unless hasPresets}}disabled{{/unless}}
|
||||||
aria-label="{{localize 'video-view-manager.scenePresetPanel.selectPreset'}}">
|
aria-label="{{localize 'scrying-pool.scenePresetPanel.selectPreset'}}">
|
||||||
<option value="" {{#unless presetName}}selected{{/unless}}>
|
<option value="" {{#unless presetName}}selected{{/unless}}>
|
||||||
{{localize "video-view-manager.scenePresetPanel.selectPreset"}}
|
{{localize "scrying-pool.scenePresetPanel.selectPreset"}}
|
||||||
</option>
|
</option>
|
||||||
{{#each presets}}
|
{{#each presets}}
|
||||||
<option value="{{this.name}}" {{#if (eq this.name ../presetName)}}selected{{/if}}>
|
<option value="{{this.name}}" {{#if (eq this.name ../presetName)}}selected{{/if}}>
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
<div class="directors-board__preset-panel-row">
|
<div class="directors-board__preset-panel-row">
|
||||||
<label class="directors-board__preset-panel-label">
|
<label class="directors-board__preset-panel-label">
|
||||||
{{localize "video-view-manager.scenePresetPanel.preDelay"}}
|
{{localize "scrying-pool.scenePresetPanel.preDelay"}}
|
||||||
<span class="directors-board__preset-panel-delay-value">{{preDelay}}ms</span>
|
<span class="directors-board__preset-panel-delay-value">{{preDelay}}ms</span>
|
||||||
<input type="range"
|
<input type="range"
|
||||||
class="directors-board__preset-panel-slider"
|
class="directors-board__preset-panel-slider"
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
max="5000"
|
max="5000"
|
||||||
value="{{preDelay}}"
|
value="{{preDelay}}"
|
||||||
step="100"
|
step="100"
|
||||||
aria-label="{{localize 'video-view-manager.scenePresetPanel.preDelay'}}"
|
aria-label="{{localize 'scrying-pool.scenePresetPanel.preDelay'}}"
|
||||||
aria-valuemin="0"
|
aria-valuemin="0"
|
||||||
aria-valuemax="5000"
|
aria-valuemax="5000"
|
||||||
aria-valuenow="{{preDelay}}">
|
aria-valuenow="{{preDelay}}">
|
||||||
@@ -55,9 +55,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="directors-board__preset-panel-row directors-board__preset-panel-row--hint">
|
<div class="directors-board__preset-panel-row directors-board__preset-panel-row--hint">
|
||||||
<span>{{localize "video-view-manager.scenePresetPanel.globalSettingsHint"}}</span>
|
<span>{{localize "scrying-pool.scenePresetPanel.globalSettingsHint"}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="directors-board__preset-panel-message">{{localize "video-view-manager.scenePresetPanel.noScene"}}</p>
|
<p class="directors-board__preset-panel-message">{{localize "scrying-pool.scenePresetPanel.noScene"}}</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* - Serveur FoundryVTT sur https://localhost:31000
|
* - Serveur FoundryVTT sur https://localhost:31000
|
||||||
* - Monde déjà disponible
|
* - Monde déjà disponible
|
||||||
* - Utilisateur: gamemaster (pas de mot de passe)
|
* - Utilisateur: gamemaster (pas de mot de passe)
|
||||||
* - Module Video View Manager déjà installé
|
* - Module Scrying Pool déjà installé
|
||||||
*
|
*
|
||||||
* Ce setup vérifie simplement que tout est accessible.
|
* Ce setup vérifie simplement que tout est accessible.
|
||||||
*/
|
*/
|
||||||
@@ -41,33 +41,33 @@ async function verifyFoundryAccessible(page) {
|
|||||||
console.error('💡 Vérifiez que:');
|
console.error('💡 Vérifiez que:');
|
||||||
console.error(' - FoundryVTT est en cours d\'exécution sur https://localhost:31000');
|
console.error(' - FoundryVTT est en cours d\'exécution sur https://localhost:31000');
|
||||||
console.error(' - Le monde est accessible');
|
console.error(' - Le monde est accessible');
|
||||||
console.error(' - Le module Video View Manager est installé');
|
console.error(' - Le module Scrying Pool est installé');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vérifie que le module Video View Manager est actif
|
* Vérifie que le module Scrying Pool est actif
|
||||||
*/
|
*/
|
||||||
async function verifyModuleActive(page) {
|
async function verifyModuleActive(page) {
|
||||||
console.log('📦 Vérification du module Video View Manager...');
|
console.log('📦 Vérification du module Scrying Pool...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Attendre que le module soit initialisé (check pour un élément spécifique)
|
// Attendre que le module soit initialisé (check pour un élément spécifique)
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
return typeof game !== 'undefined' &&
|
return typeof game !== 'undefined' &&
|
||||||
game.modules?.get?.('video-view-manager')?.active;
|
game.modules?.get?.('scrying-pool')?.active;
|
||||||
}, { timeout: 15000 });
|
}, { timeout: 15000 });
|
||||||
|
|
||||||
const isActive = await page.evaluate(() => {
|
const isActive = await page.evaluate(() => {
|
||||||
const module = game.modules.get('video-view-manager');
|
const module = game.modules.get('scrying-pool');
|
||||||
return module?.active || false;
|
return module?.active || false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
console.log('✅ Module Video View Manager est actif');
|
console.log('✅ Module Scrying Pool est actif');
|
||||||
} else {
|
} else {
|
||||||
console.warn('⚠️ Module Video View Manager n\'est pas actif');
|
console.warn('⚠️ Module Scrying Pool n\'est pas actif');
|
||||||
console.warn(' Essayez de recharger la page (Ctrl+R)');
|
console.warn(' Essayez de recharger la page (Ctrl+R)');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* - Démarre le serveur FoundryVTT (si non déjà démarré)
|
* - Démarre le serveur FoundryVTT (si non déjà démarré)
|
||||||
* - Crée un monde de test
|
* - Crée un monde de test
|
||||||
* - Configure les utilisateurs de test
|
* - Configure les utilisateurs de test
|
||||||
* - Installe le module Video View Manager
|
* - Installe le module Scrying Pool
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { chromium } from '@playwright/test';
|
import { chromium } from '@playwright/test';
|
||||||
@@ -100,20 +100,20 @@ async function configureTestUsers(page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Installe le module Video View Manager
|
* Installe le module Scrying Pool
|
||||||
*/
|
*/
|
||||||
async function installVVMModule(page) {
|
async function installVVMModule(page) {
|
||||||
console.log('📦 Installing Video View Manager module...');
|
console.log('📦 Installing Scrying Pool module...');
|
||||||
|
|
||||||
// Naviguer vers la gestion des modules
|
// Naviguer vers la gestion des modules
|
||||||
await page.goto(`${FOUNDRY_BASE_URL}/setup/modules`);
|
await page.goto(`${FOUNDRY_BASE_URL}/setup/modules`);
|
||||||
await page.waitForSelector('#modules-list', { timeout: 30000 });
|
await page.waitForSelector('#modules-list', { timeout: 30000 });
|
||||||
|
|
||||||
// Vérifier si le module est déjà installé
|
// Vérifier si le module est déjà installé
|
||||||
const moduleInstalled = await page.locator(`#modules-list [data-module-id="video-view-manager"]`).count();
|
const moduleInstalled = await page.locator(`#modules-list [data-module-id="scrying-pool"]`).count();
|
||||||
|
|
||||||
if (moduleInstalled > 0) {
|
if (moduleInstalled > 0) {
|
||||||
console.log('✅ Video View Manager module already installed');
|
console.log('✅ Scrying Pool module already installed');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ async function installVVMModule(page) {
|
|||||||
// Note: En environnement de test, le module devrait déjà être dans le dossier modules/
|
// Note: En environnement de test, le module devrait déjà être dans le dossier modules/
|
||||||
// Sinon, il faut le copier manuellement
|
// Sinon, il faut le copier manuellement
|
||||||
console.log('⚠️ Module must be manually placed in FoundryVTT modules/ folder');
|
console.log('⚠️ Module must be manually placed in FoundryVTT modules/ folder');
|
||||||
console.log(' Copy video-view-manager/ to foundrydata-dev/Data/modules/');
|
console.log(' Copy scrying-pool/ to foundrydata-dev/Data/modules/');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -173,7 +173,7 @@ async function globalSetup() {
|
|||||||
console.log(` - Player User: ${TEST_PLAYER_USER}`);
|
console.log(` - Player User: ${TEST_PLAYER_USER}`);
|
||||||
console.log(` - Foundry URL: ${FOUNDRY_BASE_URL}`);
|
console.log(` - Foundry URL: ${FOUNDRY_BASE_URL}`);
|
||||||
console.log('\n💡 Ensure FoundryVTT server is running on localhost:30000');
|
console.log('\n💡 Ensure FoundryVTT server is running on localhost:30000');
|
||||||
console.log('💡 Ensure Video View Manager module is in modules/ folder\n');
|
console.log('💡 Ensure Scrying Pool module is in modules/ folder\n');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Setup failed:', error);
|
console.error('❌ Setup failed:', error);
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "video-view-manager-e2e",
|
"name": "scrying-pool-e2e",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "video-view-manager-e2e",
|
"name": "scrying-pool-e2e",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/test": "^1.40.0",
|
"@playwright/test": "^1.40.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "video-view-manager-e2e",
|
"name": "scrying-pool-e2e",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "E2E tests for Video View Manager FoundryVTT module",
|
"description": "E2E tests for Scrying Pool FoundryVTT module",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "playwright test",
|
"test": "playwright test",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Playwright configuration for Video View Manager E2E tests
|
* Playwright configuration for Scrying Pool E2E tests
|
||||||
*
|
*
|
||||||
* Tests FoundryVTT module in a live browser environment
|
* Tests FoundryVTT module in a live browser environment
|
||||||
* Configuration adaptée pour l'environnement local :
|
* Configuration adaptée pour l'environnement local :
|
||||||
@@ -14,7 +14,7 @@ import { defineConfig, devices } from '@playwright/test';
|
|||||||
* Configuration pour les tests E2E avec FoundryVTT
|
* Configuration pour les tests E2E avec FoundryVTT
|
||||||
*
|
*
|
||||||
* Environnement :
|
* Environnement :
|
||||||
* - FoundryVTT v14 avec Video View Manager installé
|
* - FoundryVTT v14 avec Scrying Pool installé
|
||||||
* - Serveur FoundryVTT en cours sur https://localhost:31000
|
* - Serveur FoundryVTT en cours sur https://localhost:31000
|
||||||
* - Monde déjà disponible avec utilisateur "gamemaster"
|
* - Monde déjà disponible avec utilisateur "gamemaster"
|
||||||
* - Pas de mot de passe requis
|
* - Pas de mot de passe requis
|
||||||
|
|||||||
@@ -134,9 +134,9 @@ test.describe('Epic 1: Core Camera Visibility Control', () => {
|
|||||||
// Note: Cela nécessite une implémentation spécifique ou un mock
|
// Note: Cela nécessite une implémentation spécifique ou un mock
|
||||||
// Pour les tests E2E, on peut utiliser l'API du module directement
|
// Pour les tests E2E, on peut utiliser l'API du module directement
|
||||||
await page.evaluate((userId) => {
|
await page.evaluate((userId) => {
|
||||||
if (game.modules.get('video-view-manager')) {
|
if (game.modules.get('scrying-pool')) {
|
||||||
// Appeler l'API interne si disponible
|
// Appeler l'API interne si disponible
|
||||||
const controller = game.modules.get('video-view-manager').api?.controller;
|
const controller = game.modules.get('scrying-pool').api?.controller;
|
||||||
if (controller) {
|
if (controller) {
|
||||||
controller.action('test', userId, 'self-muted');
|
controller.action('test', userId, 'self-muted');
|
||||||
}
|
}
|
||||||
@@ -219,8 +219,8 @@ test.describe('Epic 1: Core Camera Visibility Control', () => {
|
|||||||
test('Cam-lost participants show portrait fallback', async ({ page }) => {
|
test('Cam-lost participants show portrait fallback', async ({ page }) => {
|
||||||
// Simuler la perte de caméra
|
// Simuler la perte de caméra
|
||||||
await page.evaluate((userId) => {
|
await page.evaluate((userId) => {
|
||||||
if (game.modules.get('video-view-manager')) {
|
if (game.modules.get('scrying-pool')) {
|
||||||
const controller = game.modules.get('video-view-manager').api?.controller;
|
const controller = game.modules.get('scrying-pool').api?.controller;
|
||||||
if (controller) {
|
if (controller) {
|
||||||
controller.action('test', userId, 'cam-lost');
|
controller.action('test', userId, 'cam-lost');
|
||||||
}
|
}
|
||||||
@@ -258,7 +258,7 @@ test.describe('Epic 1: Core Camera Visibility Control', () => {
|
|||||||
test('First encounter shows explanatory tooltip', async ({ page }) => {
|
test('First encounter shows explanatory tooltip', async ({ page }) => {
|
||||||
// Effacer le flag de premier badge
|
// Effacer le flag de premier badge
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
game.user?.unsetFlag('video-view-manager', 'firstBadgeEncounter');
|
game.user?.unsetFlag('scrying-pool', 'firstBadgeEncounter');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Recharger la page pour déclencher le first encounter
|
// Recharger la page pour déclencher le first encounter
|
||||||
|
|||||||
@@ -499,8 +499,8 @@ test.describe('Epic 2: Player Notifications & Director\'s Board', () => {
|
|||||||
await page.locator('button[aria-label="Configure Settings"]').click();
|
await page.locator('button[aria-label="Configure Settings"]').click();
|
||||||
await page.waitForSelector('.app-v2.settings', { state: 'visible' });
|
await page.waitForSelector('.app-v2.settings', { state: 'visible' });
|
||||||
|
|
||||||
// Sélectionner Video View Manager
|
// Sélectionner Scrying Pool
|
||||||
await page.locator('button:has-text("Video View Manager")').click();
|
await page.locator('button:has-text("Scrying Pool")').click();
|
||||||
|
|
||||||
// Trouver le paramètre de verbosité
|
// Trouver le paramètre de verbosité
|
||||||
await page.waitForSelector('.sp-notification-verbosity-select', { timeout: 5000 });
|
await page.waitForSelector('.sp-notification-verbosity-select', { timeout: 5000 });
|
||||||
@@ -518,7 +518,7 @@ test.describe('Epic 2: Player Notifications & Director\'s Board', () => {
|
|||||||
await page.locator('button[aria-label="Configure Settings"]').click();
|
await page.locator('button[aria-label="Configure Settings"]').click();
|
||||||
await page.waitForSelector('.app-v2.settings', { state: 'visible' });
|
await page.waitForSelector('.app-v2.settings', { state: 'visible' });
|
||||||
|
|
||||||
await page.locator('button:has-text("Video View Manager")').click();
|
await page.locator('button:has-text("Scrying Pool")').click();
|
||||||
|
|
||||||
await page.waitForSelector('.sp-notification-verbosity-select', { timeout: 5000 });
|
await page.waitForSelector('.sp-notification-verbosity-select', { timeout: 5000 });
|
||||||
await page.locator('.sp-notification-verbosity-select').selectOption('GM Only');
|
await page.locator('.sp-notification-verbosity-select').selectOption('GM Only');
|
||||||
@@ -533,7 +533,7 @@ test.describe('Epic 2: Player Notifications & Director\'s Board', () => {
|
|||||||
await page.locator('button[aria-label="Configure Settings"]').click();
|
await page.locator('button[aria-label="Configure Settings"]').click();
|
||||||
await page.waitForSelector('.app-v2.settings', { state: 'visible' });
|
await page.waitForSelector('.app-v2.settings', { state: 'visible' });
|
||||||
|
|
||||||
await page.locator('button:has-text("Video View Manager")').click();
|
await page.locator('button:has-text("Scrying Pool")').click();
|
||||||
|
|
||||||
await page.waitForSelector('.sp-notification-verbosity-select', { timeout: 5000 });
|
await page.waitForSelector('.sp-notification-verbosity-select', { timeout: 5000 });
|
||||||
await page.locator('.sp-notification-verbosity-select').selectOption('Silent');
|
await page.locator('.sp-notification-verbosity-select').selectOption('Silent');
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ test.describe('FR-15: Save Scene Preset', () => {
|
|||||||
|
|
||||||
// Vérifier que le preset contient la matrice
|
// Vérifier que le preset contient la matrice
|
||||||
const presetData = await page.evaluate((presetName) => {
|
const presetData = await page.evaluate((presetName) => {
|
||||||
const module = game.modules.get('video-view-manager');
|
const module = game.modules.get('scrying-pool');
|
||||||
if (module && module.api?.scenePresetManager) {
|
if (module && module.api?.scenePresetManager) {
|
||||||
const preset = module.api.scenePresetManager.getPreset(presetName);
|
const preset = module.api.scenePresetManager.getPreset(presetName);
|
||||||
return preset?.matrix;
|
return preset?.matrix;
|
||||||
@@ -289,7 +289,7 @@ test.describe('FR-17: Scene Auto-Apply', () => {
|
|||||||
|
|
||||||
// Associer le preset à la scène actuelle
|
// Associer le preset à la scène actuelle
|
||||||
await page.evaluate((presetName) => {
|
await page.evaluate((presetName) => {
|
||||||
const module = game.modules.get('video-view-manager');
|
const module = game.modules.get('scrying-pool');
|
||||||
if (module && module.api?.scenePresetManager) {
|
if (module && module.api?.scenePresetManager) {
|
||||||
const currentScene = game.scenes?.active;
|
const currentScene = game.scenes?.active;
|
||||||
if (currentScene) {
|
if (currentScene) {
|
||||||
@@ -326,7 +326,7 @@ test.describe('FR-17: Scene Auto-Apply', () => {
|
|||||||
// Ce test vérifie que le délai est configurable
|
// Ce test vérifie que le délai est configurable
|
||||||
// La valeur par défaut est 0ms
|
// La valeur par défaut est 0ms
|
||||||
const delay = await page.evaluate(() => {
|
const delay = await page.evaluate(() => {
|
||||||
const module = game.modules.get('video-view-manager');
|
const module = game.modules.get('scrying-pool');
|
||||||
if (module && module.api?.scenePresetManager) {
|
if (module && module.api?.scenePresetManager) {
|
||||||
return module.api.scenePresetManager.autoApplyDelay;
|
return module.api.scenePresetManager.autoApplyDelay;
|
||||||
}
|
}
|
||||||
@@ -345,7 +345,7 @@ test.describe('FR-17: Scene Auto-Apply', () => {
|
|||||||
await saveScenePreset(page, TEST_PRESET_NAME);
|
await saveScenePreset(page, TEST_PRESET_NAME);
|
||||||
|
|
||||||
await page.evaluate((presetName) => {
|
await page.evaluate((presetName) => {
|
||||||
const module = game.modules.get('video-view-manager');
|
const module = game.modules.get('scrying-pool');
|
||||||
if (module && module.api?.scenePresetManager) {
|
if (module && module.api?.scenePresetManager) {
|
||||||
const currentScene = game.scenes?.active;
|
const currentScene = game.scenes?.active;
|
||||||
if (currentScene) {
|
if (currentScene) {
|
||||||
@@ -389,7 +389,7 @@ test.describe('FR-18: Disable Auto-Apply', () => {
|
|||||||
|
|
||||||
// Désactiver l'auto-apply pour la scène actuelle
|
// Désactiver l'auto-apply pour la scène actuelle
|
||||||
await page.evaluate((presetName) => {
|
await page.evaluate((presetName) => {
|
||||||
const module = game.modules.get('video-view-manager');
|
const module = game.modules.get('scrying-pool');
|
||||||
if (module && module.api?.scenePresetManager) {
|
if (module && module.api?.scenePresetManager) {
|
||||||
const currentScene = game.scenes?.active;
|
const currentScene = game.scenes?.active;
|
||||||
if (currentScene) {
|
if (currentScene) {
|
||||||
@@ -427,7 +427,7 @@ test.describe('FR-18: Disable Auto-Apply', () => {
|
|||||||
test('Auto-apply can be disabled globally', async ({ page }) => {
|
test('Auto-apply can be disabled globally', async ({ page }) => {
|
||||||
// Désactiver l'auto-apply globalement
|
// Désactiver l'auto-apply globalement
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const module = game.modules.get('video-view-manager');
|
const module = game.modules.get('scrying-pool');
|
||||||
if (module && module.api?.scenePresetManager) {
|
if (module && module.api?.scenePresetManager) {
|
||||||
module.api.scenePresetManager.setAutoApplyEnabled(false);
|
module.api.scenePresetManager.setAutoApplyEnabled(false);
|
||||||
}
|
}
|
||||||
@@ -437,7 +437,7 @@ test.describe('FR-18: Disable Auto-Apply', () => {
|
|||||||
await saveScenePreset(page, TEST_PRESET_NAME);
|
await saveScenePreset(page, TEST_PRESET_NAME);
|
||||||
|
|
||||||
await page.evaluate((presetName) => {
|
await page.evaluate((presetName) => {
|
||||||
const module = game.modules.get('video-view-manager');
|
const module = game.modules.get('scrying-pool');
|
||||||
if (module && module.api?.scenePresetManager) {
|
if (module && module.api?.scenePresetManager) {
|
||||||
const currentScene = game.scenes?.active;
|
const currentScene = game.scenes?.active;
|
||||||
if (currentScene) {
|
if (currentScene) {
|
||||||
@@ -472,7 +472,7 @@ test.describe('FR-18: Disable Auto-Apply', () => {
|
|||||||
|
|
||||||
// Réactiver l'auto-apply global
|
// Réactiver l'auto-apply global
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const module = game.modules.get('video-view-manager');
|
const module = game.modules.get('scrying-pool');
|
||||||
if (module && module.api?.scenePresetManager) {
|
if (module && module.api?.scenePresetManager) {
|
||||||
module.api.scenePresetManager.setAutoApplyEnabled(true);
|
module.api.scenePresetManager.setAutoApplyEnabled(true);
|
||||||
}
|
}
|
||||||
@@ -482,7 +482,7 @@ test.describe('FR-18: Disable Auto-Apply', () => {
|
|||||||
test('Director\'s Board always provides manual override', async ({ page }) => {
|
test('Director\'s Board always provides manual override', async ({ page }) => {
|
||||||
// Désactiver l'auto-apply
|
// Désactiver l'auto-apply
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const module = game.modules.get('video-view-manager');
|
const module = game.modules.get('scrying-pool');
|
||||||
if (module && module.api?.scenePresetManager) {
|
if (module && module.api?.scenePresetManager) {
|
||||||
module.api.scenePresetManager.setAutoApplyEnabled(false);
|
module.api.scenePresetManager.setAutoApplyEnabled(false);
|
||||||
}
|
}
|
||||||
@@ -510,7 +510,7 @@ test.describe('FR-18: Disable Auto-Apply', () => {
|
|||||||
|
|
||||||
// Réactiver
|
// Réactiver
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const module = game.modules.get('video-view-manager');
|
const module = game.modules.get('scrying-pool');
|
||||||
if (module && module.api?.scenePresetManager) {
|
if (module && module.api?.scenePresetManager) {
|
||||||
module.api.scenePresetManager.setAutoApplyEnabled(true);
|
module.api.scenePresetManager.setAutoApplyEnabled(true);
|
||||||
}
|
}
|
||||||
@@ -558,7 +558,7 @@ test.describe('FR-19: Preset Import/Export', () => {
|
|||||||
|
|
||||||
// Capturer le contenu JSON
|
// Capturer le contenu JSON
|
||||||
const jsonContent = await page.evaluate(() => {
|
const jsonContent = await page.evaluate(() => {
|
||||||
const module = game.modules.get('video-view-manager');
|
const module = game.modules.get('scrying-pool');
|
||||||
if (module && module.api?.scenePresetManager) {
|
if (module && module.api?.scenePresetManager) {
|
||||||
return JSON.stringify(module.api.scenePresetManager.getAllPresets(), null, 2);
|
return JSON.stringify(module.api.scenePresetManager.getAllPresets(), null, 2);
|
||||||
}
|
}
|
||||||
@@ -596,7 +596,7 @@ test.describe('FR-19: Preset Import/Export', () => {
|
|||||||
|
|
||||||
// Importer via l'API
|
// Importer via l'API
|
||||||
await page.evaluate((data) => {
|
await page.evaluate((data) => {
|
||||||
const module = game.modules.get('video-view-manager');
|
const module = game.modules.get('scrying-pool');
|
||||||
if (module && module.api?.scenePresetManager) {
|
if (module && module.api?.scenePresetManager) {
|
||||||
module.api.scenePresetManager.importPresets(data, { merge: true });
|
module.api.scenePresetManager.importPresets(data, { merge: true });
|
||||||
}
|
}
|
||||||
@@ -604,7 +604,7 @@ test.describe('FR-19: Preset Import/Export', () => {
|
|||||||
|
|
||||||
// Vérifier que les nouveaux presets existent
|
// Vérifier que les nouveaux presets existent
|
||||||
const presetNames = await page.evaluate(() => {
|
const presetNames = await page.evaluate(() => {
|
||||||
const module = game.modules.get('video-view-manager');
|
const module = game.modules.get('scrying-pool');
|
||||||
if (module && module.api?.scenePresetManager) {
|
if (module && module.api?.scenePresetManager) {
|
||||||
return Object.keys(module.api.scenePresetManager.getAllPresets());
|
return Object.keys(module.api.scenePresetManager.getAllPresets());
|
||||||
}
|
}
|
||||||
@@ -624,7 +624,7 @@ test.describe('FR-19: Preset Import/Export', () => {
|
|||||||
|
|
||||||
// Essayer d'importer du JSON invalide
|
// Essayer d'importer du JSON invalide
|
||||||
const result = await page.evaluate(() => {
|
const result = await page.evaluate(() => {
|
||||||
const module = game.modules.get('video-view-manager');
|
const module = game.modules.get('scrying-pool');
|
||||||
if (module && module.api?.scenePresetManager) {
|
if (module && module.api?.scenePresetManager) {
|
||||||
try {
|
try {
|
||||||
module.api.scenePresetManager.importPresets('invalid json', { merge: true });
|
module.api.scenePresetManager.importPresets('invalid json', { merge: true });
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
*
|
*
|
||||||
* FR-23: Player Privacy Panel accessible from module settings
|
* FR-23: Player Privacy Panel accessible from module settings
|
||||||
* FR-24: Reaction Cam automation requires explicit opt-in
|
* FR-24: Reaction Cam automation requires explicit opt-in
|
||||||
* FR-25: HP-Reactive Cam Styling requires explicit opt-in
|
|
||||||
* FR-26: Custom Portrait Fallback settable via file picker
|
* FR-26: Custom Portrait Fallback settable via file picker
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -53,14 +52,11 @@ test.describe('FR-23: Player Privacy Panel Accessibility', () => {
|
|||||||
|
|
||||||
// Vérifier que les sections existent
|
// Vérifier que les sections existent
|
||||||
const reactionCamSection = page.locator('.sp-automation-item:has-text("Reaction Cam")');
|
const reactionCamSection = page.locator('.sp-automation-item:has-text("Reaction Cam")');
|
||||||
const hpStylingSection = page.locator('.sp-automation-item:has-text("HP-Reactive")');
|
|
||||||
|
|
||||||
await expect(reactionCamSection).toBeVisible({ timeout: 5000 });
|
await expect(reactionCamSection).toBeVisible({ timeout: 5000 });
|
||||||
await expect(hpStylingSection).toBeVisible({ timeout: 5000 });
|
|
||||||
|
|
||||||
// Vérifier les toggles
|
// Vérifier les toggles
|
||||||
const toggles = page.locator('.sp-toggle-switch');
|
const toggles = page.locator('.sp-toggle-switch');
|
||||||
expect(await toggles.count()).toBeGreaterThanOrEqual(2);
|
await expect(toggles).toHaveCount(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Panel shows opt-in status as ON/OFF for each automation', async ({ page }) => {
|
test('Panel shows opt-in status as ON/OFF for each automation', async ({ page }) => {
|
||||||
@@ -81,7 +77,7 @@ test.describe('FR-23: Player Privacy Panel Accessibility', () => {
|
|||||||
|
|
||||||
// Les contrôles devraient être désactivés
|
// Les contrôles devraient être désactivés
|
||||||
const toggles = page.locator('.sp-toggle-switch:disabled');
|
const toggles = page.locator('.sp-toggle-switch:disabled');
|
||||||
await expect(toggles).toHaveCount(2); // Reaction Cam et HP-Reactive
|
await expect(toggles).toHaveCount(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Settings persist in world-level user flags', async ({ page }) => {
|
test('Settings persist in world-level user flags', async ({ page }) => {
|
||||||
@@ -163,7 +159,7 @@ test.describe('FR-24: Reaction Cam Automation', () => {
|
|||||||
// Si désactivé, le trigger ne devrait pas fonctionner
|
// Si désactivé, le trigger ne devrait pas fonctionner
|
||||||
// Simuler un trigger
|
// Simuler un trigger
|
||||||
const triggerResult = await page.evaluate(() => {
|
const triggerResult = await page.evaluate(() => {
|
||||||
const module = game.modules.get('video-view-manager');
|
const module = game.modules.get('scrying-pool');
|
||||||
if (module && module.api?.reactionCam) {
|
if (module && module.api?.reactionCam) {
|
||||||
// Vérifier que Reaction Cam est désactivé
|
// Vérifier que Reaction Cam est désactivé
|
||||||
return module.api.reactionCam.isEnabled();
|
return module.api.reactionCam.isEnabled();
|
||||||
@@ -229,7 +225,7 @@ test.describe('FR-24: Reaction Cam Automation', () => {
|
|||||||
|
|
||||||
// Simuler un trigger
|
// Simuler un trigger
|
||||||
const result = await page.evaluate(() => {
|
const result = await page.evaluate(() => {
|
||||||
const module = game.modules.get('video-view-manager');
|
const module = game.modules.get('scrying-pool');
|
||||||
if (module && module.api?.reactionCam) {
|
if (module && module.api?.reactionCam) {
|
||||||
// Le trigger devrait skipper ce joueur
|
// Le trigger devrait skipper ce joueur
|
||||||
return { skipped: true, reason: 'opted-out' };
|
return { skipped: true, reason: 'opted-out' };
|
||||||
@@ -242,77 +238,6 @@ test.describe('FR-24: Reaction Cam Automation', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// FR-25: HP-Reactive Cam Styling Opt-In
|
|
||||||
// ============================================================================
|
|
||||||
test.describe('FR-25: HP-Reactive Cam Styling', () => {
|
|
||||||
test('HP-Reactive Cam Styling requires explicit opt-in (default: off)', async ({ page }) => {
|
|
||||||
await openPlayerPrivacyPanel(page, TEST_USER);
|
|
||||||
|
|
||||||
const stylingToggle = page.locator('.sp-automation-item:has-text("HP-Reactive") .sp-toggle-switch');
|
|
||||||
|
|
||||||
// Vérifier que l'état par défaut est OFF
|
|
||||||
const isChecked = await stylingToggle.evaluate(el => el.checked);
|
|
||||||
expect(isChecked).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('HP-Reactive Styling remains disabled until explicitly enabled', async ({ page }) => {
|
|
||||||
await openPlayerPrivacyPanel(page, TEST_USER);
|
|
||||||
|
|
||||||
const stylingToggle = page.locator('.sp-automation-item:has-text("HP-Reactive") .sp-toggle-switch');
|
|
||||||
|
|
||||||
// Simuler un changement de HP
|
|
||||||
const result = await page.evaluate(() => {
|
|
||||||
const module = game.modules.get('video-view-manager');
|
|
||||||
if (module && module.api?.hpStyling) {
|
|
||||||
// Vérifier que le styling est désactivé
|
|
||||||
return module.api.hpStyling.isEnabled();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GM is not notified of individual styling opt-in statuses', async ({ page }) => {
|
|
||||||
// Activer HP-Reactive pour un joueur
|
|
||||||
await openPlayerPrivacyPanel(page, TEST_USER);
|
|
||||||
const stylingToggle = page.locator('.sp-automation-item:has-text("HP-Reactive") .sp-toggle-switch');
|
|
||||||
await stylingToggle.click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
// Vérifier qu'il n'y a pas de notification pour le GM
|
|
||||||
// (Contrairement à Reaction Cam qui montre un badge)
|
|
||||||
const notification = page.locator('.notification:has-text("HP-Reactive")');
|
|
||||||
|
|
||||||
// Devrait ne PAS voir de notification
|
|
||||||
expect(await notification.count()).toBe(0);
|
|
||||||
|
|
||||||
// Nettoyer
|
|
||||||
await stylingToggle.click();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Styling opt-in flag persists across sessions', async ({ page }) => {
|
|
||||||
// Activer
|
|
||||||
await openPlayerPrivacyPanel(page, TEST_USER);
|
|
||||||
const stylingToggle = page.locator('.sp-automation-item:has-text("HP-Reactive") .sp-toggle-switch');
|
|
||||||
await stylingToggle.click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
// Recharger
|
|
||||||
await page.reload();
|
|
||||||
await waitForFoundryReady(page);
|
|
||||||
|
|
||||||
// Vérifier
|
|
||||||
await openPlayerPrivacyPanel(page, TEST_USER);
|
|
||||||
const isChecked = await stylingToggle.evaluate(el => el.checked);
|
|
||||||
expect(isChecked).toBe(true);
|
|
||||||
|
|
||||||
// Désactiver
|
|
||||||
await stylingToggle.click();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// FR-26: Custom Portrait Fallback
|
// FR-26: Custom Portrait Fallback
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -388,7 +313,7 @@ test.describe('FR-26: Custom Portrait Fallback', () => {
|
|||||||
|
|
||||||
// Via l'API, définir un portrait
|
// Via l'API, définir un portrait
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const module = game.modules.get('video-view-manager');
|
const module = game.modules.get('scrying-pool');
|
||||||
if (module && module.api?.portraitFallbackHandler) {
|
if (module && module.api?.portraitFallbackHandler) {
|
||||||
const dataURL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
const dataURL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
||||||
module.api.portraitFallbackHandler.setPortraitFallback('gamemaster', dataURL);
|
module.api.portraitFallbackHandler.setPortraitFallback('gamemaster', dataURL);
|
||||||
@@ -412,7 +337,7 @@ test.describe('FR-26: Custom Portrait Fallback', () => {
|
|||||||
|
|
||||||
// Définir un portrait personnalisé
|
// Définir un portrait personnalisé
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const module = game.modules.get('video-view-manager');
|
const module = game.modules.get('scrying-pool');
|
||||||
if (module && module.api?.portraitFallbackHandler) {
|
if (module && module.api?.portraitFallbackHandler) {
|
||||||
const dataURL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
const dataURL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
||||||
module.api.portraitFallbackHandler.setPortraitFallback('gamemaster', dataURL);
|
module.api.portraitFallbackHandler.setPortraitFallback('gamemaster', dataURL);
|
||||||
@@ -421,7 +346,7 @@ test.describe('FR-26: Custom Portrait Fallback', () => {
|
|||||||
|
|
||||||
// Forcer l'état never-connected pour un utilisateur
|
// Forcer l'état never-connected pour un utilisateur
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const module = game.modules.get('video-view-manager');
|
const module = game.modules.get('scrying-pool');
|
||||||
if (module && module.api?.controller) {
|
if (module && module.api?.controller) {
|
||||||
module.api.controller.setState('gamemaster', 'never-connected');
|
module.api.controller.setState('gamemaster', 'never-connected');
|
||||||
}
|
}
|
||||||
@@ -462,7 +387,7 @@ async function testPortraitUpload(page, filename, shouldSucceed) {
|
|||||||
|
|
||||||
// Pour l'instant, on simule via l'API
|
// Pour l'instant, on simule via l'API
|
||||||
const result = await page.evaluate(({ filename, shouldSucceed }) => {
|
const result = await page.evaluate(({ filename, shouldSucceed }) => {
|
||||||
const module = game.modules.get('video-view-manager');
|
const module = game.modules.get('scrying-pool');
|
||||||
if (module && module.api?.portraitFallbackHandler) {
|
if (module && module.api?.portraitFallbackHandler) {
|
||||||
if (shouldSucceed) {
|
if (shouldSucceed) {
|
||||||
// Fichier valide
|
// Fichier valide
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Module Initialization Tests - Tests E2E
|
* Module Initialization Tests - Tests E2E
|
||||||
*
|
*
|
||||||
* Vérifie que le module Video View Manager s'initialise correctement
|
* Vérifie que le module Scrying Pool s'initialise correctement
|
||||||
* dans FoundryVTT et que toutes les fonctionnalités de base sont disponibles.
|
* dans FoundryVTT et que toutes les fonctionnalités de base sont disponibles.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -12,8 +12,8 @@ import {
|
|||||||
clickFoundryButton,
|
clickFoundryButton,
|
||||||
} from '../utils/foundry-helpers';
|
} from '../utils/foundry-helpers';
|
||||||
|
|
||||||
const MODULE_ID = 'video-view-manager';
|
const MODULE_ID = 'scrying-pool';
|
||||||
const MODULE_NAME = 'Video View Manager';
|
const MODULE_NAME = 'Scrying Pool';
|
||||||
|
|
||||||
test.describe('Module Initialization', () => {
|
test.describe('Module Initialization', () => {
|
||||||
test.setTimeout(60000);
|
test.setTimeout(60000);
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ export async function waitForFoundryReady(page, timeout = 30000) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attend que le module Video View Manager soit actif
|
* Attend que le module Scrying Pool soit actif
|
||||||
* @param {import('@playwright/test').Page} page - La page Playwright
|
* @param {import('@playwright/test').Page} page - La page Playwright
|
||||||
* @param {number} timeout - Timeout en ms (défaut: 15000)
|
* @param {number} timeout - Timeout en ms (défaut: 15000)
|
||||||
*/
|
*/
|
||||||
export async function waitForVVMModule(page, timeout = 15000) {
|
export async function waitForVVMModule(page, timeout = 15000) {
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
const module = game.modules?.get?.('video-view-manager');
|
const module = game.modules?.get?.('scrying-pool');
|
||||||
return module?.active === true;
|
return module?.active === true;
|
||||||
}, { timeout });
|
}, { timeout });
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,7 @@ export async function openPlayerPrivacyPanel(page, userId) {
|
|||||||
await openFoundrySidebar(page);
|
await openFoundrySidebar(page);
|
||||||
|
|
||||||
// Naviguer vers les paramètres du module
|
// Naviguer vers les paramètres du module
|
||||||
await clickFoundryButton(page, 'button:has-text("Video View Manager")');
|
await clickFoundryButton(page, 'button:has-text("Scrying Pool")');
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
// Cliquer sur le bouton Player Privacy
|
// Cliquer sur le bouton Player Privacy
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ export function createPlayerPrivacyManagerMock(overrides = {}) {
|
|||||||
// Settings retrieval
|
// Settings retrieval
|
||||||
getSettings: vi.fn(() => ({
|
getSettings: vi.fn(() => ({
|
||||||
reactionCamEnabled: false,
|
reactionCamEnabled: false,
|
||||||
hpReactiveCamStylingEnabled: false,
|
|
||||||
customPortraitFallback: null,
|
customPortraitFallback: null,
|
||||||
})),
|
})),
|
||||||
// Portrait methods
|
// Portrait methods
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ describe("privacy-settings contract", () => {
|
|||||||
it("should export PRIVACY_SETTINGS_DEFAULT with all false and null portrait", () => {
|
it("should export PRIVACY_SETTINGS_DEFAULT with all false and null portrait", () => {
|
||||||
expect(PRIVACY_SETTINGS_DEFAULT).toEqual({
|
expect(PRIVACY_SETTINGS_DEFAULT).toEqual({
|
||||||
reactionCamEnabled: false,
|
reactionCamEnabled: false,
|
||||||
hpReactiveCamStylingEnabled: false,
|
|
||||||
customPortraitFallback: null,
|
customPortraitFallback: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -37,7 +36,6 @@ describe("privacy-settings contract", () => {
|
|||||||
it("should export PRIVACY_SETTING_KEYS as frozen array including portrait fallback", () => {
|
it("should export PRIVACY_SETTING_KEYS as frozen array including portrait fallback", () => {
|
||||||
expect(PRIVACY_SETTING_KEYS).toEqual([
|
expect(PRIVACY_SETTING_KEYS).toEqual([
|
||||||
"reactionCamEnabled",
|
"reactionCamEnabled",
|
||||||
"hpReactiveCamStylingEnabled",
|
|
||||||
"customPortraitFallback",
|
"customPortraitFallback",
|
||||||
]);
|
]);
|
||||||
expect(Object.isFrozen(PRIVACY_SETTING_KEYS)).toBe(true);
|
expect(Object.isFrozen(PRIVACY_SETTING_KEYS)).toBe(true);
|
||||||
@@ -46,7 +44,6 @@ describe("privacy-settings contract", () => {
|
|||||||
it("should export FEATURE_NAME_MAP as frozen object", () => {
|
it("should export FEATURE_NAME_MAP as frozen object", () => {
|
||||||
expect(FEATURE_NAME_MAP).toEqual({
|
expect(FEATURE_NAME_MAP).toEqual({
|
||||||
reactionCam: "reactionCamEnabled",
|
reactionCam: "reactionCamEnabled",
|
||||||
hpReactiveCamStyling: "hpReactiveCamStylingEnabled",
|
|
||||||
});
|
});
|
||||||
expect(Object.isFrozen(FEATURE_NAME_MAP)).toBe(true);
|
expect(Object.isFrozen(FEATURE_NAME_MAP)).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -62,19 +59,6 @@ describe("privacy-settings contract", () => {
|
|||||||
const result = createPrivacySettings({ reactionCamEnabled: true });
|
const result = createPrivacySettings({ reactionCamEnabled: true });
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
reactionCamEnabled: true,
|
reactionCamEnabled: true,
|
||||||
hpReactiveCamStylingEnabled: false,
|
|
||||||
customPortraitFallback: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should allow both boolean settings to be overridden", () => {
|
|
||||||
const result = createPrivacySettings({
|
|
||||||
reactionCamEnabled: true,
|
|
||||||
hpReactiveCamStylingEnabled: true,
|
|
||||||
});
|
|
||||||
expect(result).toEqual({
|
|
||||||
reactionCamEnabled: true,
|
|
||||||
hpReactiveCamStylingEnabled: true,
|
|
||||||
customPortraitFallback: null,
|
customPortraitFallback: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -84,7 +68,6 @@ describe("privacy-settings contract", () => {
|
|||||||
const result = createPrivacySettings({ customPortraitFallback: dataURL });
|
const result = createPrivacySettings({ customPortraitFallback: dataURL });
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
reactionCamEnabled: false,
|
reactionCamEnabled: false,
|
||||||
hpReactiveCamStylingEnabled: false,
|
|
||||||
customPortraitFallback: dataURL,
|
customPortraitFallback: dataURL,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -102,7 +85,6 @@ describe("privacy-settings contract", () => {
|
|||||||
});
|
});
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
reactionCamEnabled: true,
|
reactionCamEnabled: true,
|
||||||
hpReactiveCamStylingEnabled: false,
|
|
||||||
customPortraitFallback: null,
|
customPortraitFallback: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -112,7 +94,6 @@ describe("privacy-settings contract", () => {
|
|||||||
it("should accept valid settings with all false", () => {
|
it("should accept valid settings with all false", () => {
|
||||||
const valid = {
|
const valid = {
|
||||||
reactionCamEnabled: false,
|
reactionCamEnabled: false,
|
||||||
hpReactiveCamStylingEnabled: false,
|
|
||||||
};
|
};
|
||||||
expect(isValidPrivacySettings(valid)).toEqual(valid);
|
expect(isValidPrivacySettings(valid)).toEqual(valid);
|
||||||
});
|
});
|
||||||
@@ -120,7 +101,6 @@ describe("privacy-settings contract", () => {
|
|||||||
it("should accept valid settings with all true", () => {
|
it("should accept valid settings with all true", () => {
|
||||||
const valid = {
|
const valid = {
|
||||||
reactionCamEnabled: true,
|
reactionCamEnabled: true,
|
||||||
hpReactiveCamStylingEnabled: true,
|
|
||||||
};
|
};
|
||||||
expect(isValidPrivacySettings(valid)).toEqual(valid);
|
expect(isValidPrivacySettings(valid)).toEqual(valid);
|
||||||
});
|
});
|
||||||
@@ -128,7 +108,6 @@ describe("privacy-settings contract", () => {
|
|||||||
it("should accept valid settings with mixed values", () => {
|
it("should accept valid settings with mixed values", () => {
|
||||||
const valid = {
|
const valid = {
|
||||||
reactionCamEnabled: true,
|
reactionCamEnabled: true,
|
||||||
hpReactiveCamStylingEnabled: false,
|
|
||||||
};
|
};
|
||||||
expect(isValidPrivacySettings(valid)).toEqual(valid);
|
expect(isValidPrivacySettings(valid)).toEqual(valid);
|
||||||
});
|
});
|
||||||
@@ -164,7 +143,6 @@ describe("privacy-settings contract", () => {
|
|||||||
it("should throw TypeError for unknown keys", () => {
|
it("should throw TypeError for unknown keys", () => {
|
||||||
const invalid = {
|
const invalid = {
|
||||||
reactionCamEnabled: false,
|
reactionCamEnabled: false,
|
||||||
hpReactiveCamStylingEnabled: false,
|
|
||||||
extraKey: "invalid",
|
extraKey: "invalid",
|
||||||
};
|
};
|
||||||
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
|
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
|
||||||
@@ -174,7 +152,6 @@ describe("privacy-settings contract", () => {
|
|||||||
it("should throw TypeError when reactionCamEnabled is not boolean", () => {
|
it("should throw TypeError when reactionCamEnabled is not boolean", () => {
|
||||||
const invalid = {
|
const invalid = {
|
||||||
reactionCamEnabled: "not a boolean",
|
reactionCamEnabled: "not a boolean",
|
||||||
hpReactiveCamStylingEnabled: false,
|
|
||||||
};
|
};
|
||||||
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
|
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
|
||||||
expect(() => isValidPrivacySettings(invalid)).toThrow(
|
expect(() => isValidPrivacySettings(invalid)).toThrow(
|
||||||
@@ -185,7 +162,6 @@ describe("privacy-settings contract", () => {
|
|||||||
it("should throw TypeError when reactionCamEnabled is a number", () => {
|
it("should throw TypeError when reactionCamEnabled is a number", () => {
|
||||||
const invalid = {
|
const invalid = {
|
||||||
reactionCamEnabled: 1,
|
reactionCamEnabled: 1,
|
||||||
hpReactiveCamStylingEnabled: false,
|
|
||||||
};
|
};
|
||||||
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
|
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
|
||||||
expect(() => isValidPrivacySettings(invalid)).toThrow(
|
expect(() => isValidPrivacySettings(invalid)).toThrow(
|
||||||
@@ -193,28 +169,6 @@ describe("privacy-settings contract", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw TypeError when hpReactiveCamStylingEnabled is not boolean", () => {
|
|
||||||
const invalid = {
|
|
||||||
reactionCamEnabled: false,
|
|
||||||
hpReactiveCamStylingEnabled: "not a boolean",
|
|
||||||
};
|
|
||||||
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
|
|
||||||
expect(() => isValidPrivacySettings(invalid)).toThrow(
|
|
||||||
"hpReactiveCamStylingEnabled must be a boolean"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw TypeError when hpReactiveCamStylingEnabled is null", () => {
|
|
||||||
const invalid = {
|
|
||||||
reactionCamEnabled: false,
|
|
||||||
hpReactiveCamStylingEnabled: null,
|
|
||||||
};
|
|
||||||
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
|
|
||||||
expect(() => isValidPrivacySettings(invalid)).toThrow(
|
|
||||||
"hpReactiveCamStylingEnabled must be a boolean"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should accept settings with only reactionCamEnabled (backward compatible)", () => {
|
it("should accept settings with only reactionCamEnabled (backward compatible)", () => {
|
||||||
// Backward compatibility: settings without all keys are accepted
|
// Backward compatibility: settings without all keys are accepted
|
||||||
const valid = {
|
const valid = {
|
||||||
@@ -223,14 +177,6 @@ describe("privacy-settings contract", () => {
|
|||||||
expect(() => isValidPrivacySettings(valid)).not.toThrow();
|
expect(() => isValidPrivacySettings(valid)).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should accept settings with only hpReactiveCamStylingEnabled (backward compatible)", () => {
|
|
||||||
// Backward compatibility: settings without all keys are accepted
|
|
||||||
const valid = {
|
|
||||||
hpReactiveCamStylingEnabled: false,
|
|
||||||
};
|
|
||||||
expect(() => isValidPrivacySettings(valid)).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should accept empty object (backward compatible)", () => {
|
it("should accept empty object (backward compatible)", () => {
|
||||||
// Backward compatibility: empty object is accepted
|
// Backward compatibility: empty object is accepted
|
||||||
expect(() => isValidPrivacySettings({})).not.toThrow();
|
expect(() => isValidPrivacySettings({})).not.toThrow();
|
||||||
@@ -242,12 +188,6 @@ describe("privacy-settings contract", () => {
|
|||||||
expect(validateSettingKey("reactionCamEnabled")).toBe("reactionCamEnabled");
|
expect(validateSettingKey("reactionCamEnabled")).toBe("reactionCamEnabled");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should accept valid key: hpReactiveCamStylingEnabled", () => {
|
|
||||||
expect(validateSettingKey("hpReactiveCamStylingEnabled")).toBe(
|
|
||||||
"hpReactiveCamStylingEnabled"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw TypeError for empty string", () => {
|
it("should throw TypeError for empty string", () => {
|
||||||
expect(() => validateSettingKey("")).toThrow(TypeError);
|
expect(() => validateSettingKey("")).toThrow(TypeError);
|
||||||
expect(() => validateSettingKey("")).toThrow(
|
expect(() => validateSettingKey("")).toThrow(
|
||||||
@@ -332,12 +272,6 @@ describe("privacy-settings contract", () => {
|
|||||||
expect(validateFeatureName("reactionCam")).toBe("reactionCam");
|
expect(validateFeatureName("reactionCam")).toBe("reactionCam");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should accept valid feature: hpReactiveCamStyling", () => {
|
|
||||||
expect(validateFeatureName("hpReactiveCamStyling")).toBe(
|
|
||||||
"hpReactiveCamStyling"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw TypeError for empty string", () => {
|
it("should throw TypeError for empty string", () => {
|
||||||
expect(() => validateFeatureName("")).toThrow(TypeError);
|
expect(() => validateFeatureName("")).toThrow(TypeError);
|
||||||
expect(() => validateFeatureName("")).toThrow(
|
expect(() => validateFeatureName("")).toThrow(
|
||||||
@@ -399,7 +333,6 @@ describe("privacy-settings contract", () => {
|
|||||||
|
|
||||||
it("should retain existing boolean settings", () => {
|
it("should retain existing boolean settings", () => {
|
||||||
expect(PRIVACY_SETTINGS_DEFAULT.reactionCamEnabled).toBe(false);
|
expect(PRIVACY_SETTINGS_DEFAULT.reactionCamEnabled).toBe(false);
|
||||||
expect(PRIVACY_SETTINGS_DEFAULT.hpReactiveCamStylingEnabled).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -410,7 +343,6 @@ describe("privacy-settings contract", () => {
|
|||||||
|
|
||||||
it("should retain existing keys", () => {
|
it("should retain existing keys", () => {
|
||||||
expect(PRIVACY_SETTING_KEYS).toContain("reactionCamEnabled");
|
expect(PRIVACY_SETTING_KEYS).toContain("reactionCamEnabled");
|
||||||
expect(PRIVACY_SETTING_KEYS).toContain("hpReactiveCamStylingEnabled");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -443,7 +375,6 @@ describe("privacy-settings contract", () => {
|
|||||||
it("should accept valid settings with customPortraitFallback as string", () => {
|
it("should accept valid settings with customPortraitFallback as string", () => {
|
||||||
const valid = {
|
const valid = {
|
||||||
reactionCamEnabled: false,
|
reactionCamEnabled: false,
|
||||||
hpReactiveCamStylingEnabled: false,
|
|
||||||
customPortraitFallback: "data:image/png;base64,test",
|
customPortraitFallback: "data:image/png;base64,test",
|
||||||
};
|
};
|
||||||
expect(isValidPrivacySettings(valid)).toEqual(valid);
|
expect(isValidPrivacySettings(valid)).toEqual(valid);
|
||||||
@@ -452,7 +383,6 @@ describe("privacy-settings contract", () => {
|
|||||||
it("should accept valid settings with customPortraitFallback as null", () => {
|
it("should accept valid settings with customPortraitFallback as null", () => {
|
||||||
const valid = {
|
const valid = {
|
||||||
reactionCamEnabled: false,
|
reactionCamEnabled: false,
|
||||||
hpReactiveCamStylingEnabled: false,
|
|
||||||
customPortraitFallback: null,
|
customPortraitFallback: null,
|
||||||
};
|
};
|
||||||
expect(isValidPrivacySettings(valid)).toEqual(valid);
|
expect(isValidPrivacySettings(valid)).toEqual(valid);
|
||||||
@@ -462,7 +392,6 @@ describe("privacy-settings contract", () => {
|
|||||||
// Backward compatibility - may not have the key
|
// Backward compatibility - may not have the key
|
||||||
const valid = {
|
const valid = {
|
||||||
reactionCamEnabled: false,
|
reactionCamEnabled: false,
|
||||||
hpReactiveCamStylingEnabled: false,
|
|
||||||
};
|
};
|
||||||
// This should still work - null/undefined is acceptable
|
// This should still work - null/undefined is acceptable
|
||||||
expect(() => isValidPrivacySettings(valid)).not.toThrow();
|
expect(() => isValidPrivacySettings(valid)).not.toThrow();
|
||||||
@@ -471,7 +400,6 @@ describe("privacy-settings contract", () => {
|
|||||||
it("should throw TypeError when customPortraitFallback is not string or null", () => {
|
it("should throw TypeError when customPortraitFallback is not string or null", () => {
|
||||||
const invalid = {
|
const invalid = {
|
||||||
reactionCamEnabled: false,
|
reactionCamEnabled: false,
|
||||||
hpReactiveCamStylingEnabled: false,
|
|
||||||
customPortraitFallback: 123,
|
customPortraitFallback: 123,
|
||||||
};
|
};
|
||||||
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
|
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
|
||||||
@@ -483,7 +411,6 @@ describe("privacy-settings contract", () => {
|
|||||||
it("should throw TypeError when customPortraitFallback is a boolean", () => {
|
it("should throw TypeError when customPortraitFallback is a boolean", () => {
|
||||||
const invalid = {
|
const invalid = {
|
||||||
reactionCamEnabled: false,
|
reactionCamEnabled: false,
|
||||||
hpReactiveCamStylingEnabled: false,
|
|
||||||
customPortraitFallback: true,
|
customPortraitFallback: true,
|
||||||
};
|
};
|
||||||
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
|
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
|
||||||
@@ -495,7 +422,6 @@ describe("privacy-settings contract", () => {
|
|||||||
it("should throw TypeError when customPortraitFallback is an object", () => {
|
it("should throw TypeError when customPortraitFallback is an object", () => {
|
||||||
const invalid = {
|
const invalid = {
|
||||||
reactionCamEnabled: false,
|
reactionCamEnabled: false,
|
||||||
hpReactiveCamStylingEnabled: false,
|
|
||||||
customPortraitFallback: {},
|
customPortraitFallback: {},
|
||||||
};
|
};
|
||||||
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
|
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
|
||||||
|
|||||||
@@ -98,10 +98,10 @@ describe("PlayerPrivacyManager", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return saved settings when flag exists", () => {
|
it("should return saved settings when flag exists", () => {
|
||||||
const savedSettings = { reactionCamEnabled: true, hpReactiveCamStylingEnabled: false, customPortraitFallback: null };
|
const savedSettings = { reactionCamEnabled: true, customPortraitFallback: null };
|
||||||
adapter.users.get.mockReturnValue({
|
adapter.users.get.mockReturnValue({
|
||||||
getFlag: vi.fn((scope, key) => {
|
getFlag: vi.fn((scope, key) => {
|
||||||
if (scope === "video-view-manager") {
|
if (scope === "scrying-pool") {
|
||||||
return savedSettings[key];
|
return savedSettings[key];
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -114,7 +114,7 @@ describe("PlayerPrivacyManager", () => {
|
|||||||
it("should return partial settings merged with defaults", () => {
|
it("should return partial settings merged with defaults", () => {
|
||||||
adapter.users.get.mockReturnValue({
|
adapter.users.get.mockReturnValue({
|
||||||
getFlag: vi.fn((scope, key) => {
|
getFlag: vi.fn((scope, key) => {
|
||||||
if (scope === "video-view-manager" && key === "reactionCamEnabled") {
|
if (scope === "scrying-pool" && key === "reactionCamEnabled") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -123,7 +123,6 @@ describe("PlayerPrivacyManager", () => {
|
|||||||
const result = manager.getSettings("user1");
|
const result = manager.getSettings("user1");
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
reactionCamEnabled: true,
|
reactionCamEnabled: true,
|
||||||
hpReactiveCamStylingEnabled: false,
|
|
||||||
customPortraitFallback: null,
|
customPortraitFallback: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -182,7 +181,7 @@ describe("PlayerPrivacyManager", () => {
|
|||||||
adapter.users.get.mockReturnValue(mockUser);
|
adapter.users.get.mockReturnValue(mockUser);
|
||||||
await manager.setSetting("user1", "reactionCamEnabled", true);
|
await manager.setSetting("user1", "reactionCamEnabled", true);
|
||||||
expect(mockUser.setFlag).toHaveBeenCalledWith(
|
expect(mockUser.setFlag).toHaveBeenCalledWith(
|
||||||
"video-view-manager",
|
"scrying-pool",
|
||||||
"reactionCamEnabled",
|
"reactionCamEnabled",
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
@@ -268,15 +267,6 @@ describe("PlayerPrivacyManager", () => {
|
|||||||
expect(manager.isOptedIn("nonexistent", "reactionCam")).toBe(false);
|
expect(manager.isOptedIn("nonexistent", "reactionCam")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should work for hpReactiveCamStyling feature", () => {
|
|
||||||
adapter.users.get.mockReturnValue({
|
|
||||||
getFlag: vi.fn((scope, key) => {
|
|
||||||
if (key === "hpReactiveCamStylingEnabled") return true;
|
|
||||||
return false;
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
expect(manager.isOptedIn("user1", "hpReactiveCamStyling")).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getAllSettings", () => {
|
describe("getAllSettings", () => {
|
||||||
@@ -295,7 +285,6 @@ describe("PlayerPrivacyManager", () => {
|
|||||||
id: "user1",
|
id: "user1",
|
||||||
getFlag: vi.fn((scope, key) => {
|
getFlag: vi.fn((scope, key) => {
|
||||||
if (key === "reactionCamEnabled") return true;
|
if (key === "reactionCamEnabled") return true;
|
||||||
if (key === "hpReactiveCamStylingEnabled") return false;
|
|
||||||
return undefined; // customPortraitFallback and other keys
|
return undefined; // customPortraitFallback and other keys
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -303,7 +292,6 @@ describe("PlayerPrivacyManager", () => {
|
|||||||
id: "user2",
|
id: "user2",
|
||||||
getFlag: vi.fn((scope, key) => {
|
getFlag: vi.fn((scope, key) => {
|
||||||
if (key === "reactionCamEnabled") return false;
|
if (key === "reactionCamEnabled") return false;
|
||||||
if (key === "hpReactiveCamStylingEnabled") return true;
|
|
||||||
return undefined; // customPortraitFallback and other keys
|
return undefined; // customPortraitFallback and other keys
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -318,12 +306,10 @@ describe("PlayerPrivacyManager", () => {
|
|||||||
expect(result.size).toBe(2);
|
expect(result.size).toBe(2);
|
||||||
expect(result.get("user1")).toEqual({
|
expect(result.get("user1")).toEqual({
|
||||||
reactionCamEnabled: true,
|
reactionCamEnabled: true,
|
||||||
hpReactiveCamStylingEnabled: false,
|
|
||||||
customPortraitFallback: null,
|
customPortraitFallback: null,
|
||||||
});
|
});
|
||||||
expect(result.get("user2")).toEqual({
|
expect(result.get("user2")).toEqual({
|
||||||
reactionCamEnabled: false,
|
reactionCamEnabled: false,
|
||||||
hpReactiveCamStylingEnabled: true,
|
|
||||||
customPortraitFallback: null,
|
customPortraitFallback: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -429,34 +415,6 @@ describe("PlayerPrivacyManager", () => {
|
|||||||
expect(manager.isOptedIn("player1", "reactionCam")).toBe(true);
|
expect(manager.isOptedIn("player1", "reactionCam")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle player disabling HP-Reactive Cam Styling", async () => {
|
|
||||||
const mockUser = {
|
|
||||||
id: "player1",
|
|
||||||
getFlag: vi.fn((scope, key) => {
|
|
||||||
if (key === "hpReactiveCamStylingEnabled") return true;
|
|
||||||
return false;
|
|
||||||
}),
|
|
||||||
setFlag: vi.fn().mockResolvedValue(undefined),
|
|
||||||
};
|
|
||||||
adapter.users.get.mockReturnValue(mockUser);
|
|
||||||
|
|
||||||
// Initially opted in
|
|
||||||
expect(manager.isOptedIn("player1", "hpReactiveCamStyling")).toBe(true);
|
|
||||||
|
|
||||||
// Disable HP-Reactive Cam Styling
|
|
||||||
await manager.setSetting("player1", "hpReactiveCamStylingEnabled", false);
|
|
||||||
// After setting, the mock should return false for hpReactiveCamStylingEnabled
|
|
||||||
adapter.users.get.mockReturnValue({
|
|
||||||
id: "player1",
|
|
||||||
getFlag: vi.fn((scope, key) => {
|
|
||||||
if (key === "hpReactiveCamStylingEnabled") return false;
|
|
||||||
return false;
|
|
||||||
}),
|
|
||||||
setFlag: vi.fn().mockResolvedValue(undefined),
|
|
||||||
});
|
|
||||||
expect(manager.isOptedIn("player1", "hpReactiveCamStyling")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should allow GM to view all players' settings", () => {
|
it("should allow GM to view all players' settings", () => {
|
||||||
const gm = { id: "gm1", isGM: true, getFlag: vi.fn(() => false) };
|
const gm = { id: "gm1", isGM: true, getFlag: vi.fn(() => false) };
|
||||||
const player1 = {
|
const player1 = {
|
||||||
@@ -465,7 +423,7 @@ describe("PlayerPrivacyManager", () => {
|
|||||||
};
|
};
|
||||||
const player2 = {
|
const player2 = {
|
||||||
id: "player2",
|
id: "player2",
|
||||||
getFlag: vi.fn((scope, key) => (key === "hpReactiveCamStylingEnabled" ? true : false)),
|
getFlag: vi.fn((scope, key) => (key === "reactionCamEnabled" ? false : false)),
|
||||||
};
|
};
|
||||||
|
|
||||||
adapter.users.all.mockReturnValue([gm, player1, player2]);
|
adapter.users.all.mockReturnValue([gm, player1, player2]);
|
||||||
@@ -518,7 +476,7 @@ describe("PlayerPrivacyManager", () => {
|
|||||||
).resolves.not.toThrow();
|
).resolves.not.toThrow();
|
||||||
|
|
||||||
expect(mockUser.setFlag).toHaveBeenCalledWith(
|
expect(mockUser.setFlag).toHaveBeenCalledWith(
|
||||||
"video-view-manager",
|
"scrying-pool",
|
||||||
"customPortraitFallback",
|
"customPortraitFallback",
|
||||||
dataURL
|
dataURL
|
||||||
);
|
);
|
||||||
@@ -648,7 +606,7 @@ describe("PlayerPrivacyManager", () => {
|
|||||||
await manager.removePortraitFallback("player1");
|
await manager.removePortraitFallback("player1");
|
||||||
|
|
||||||
expect(mockUser.unsetFlag).toHaveBeenCalledWith(
|
expect(mockUser.unsetFlag).toHaveBeenCalledWith(
|
||||||
"video-view-manager",
|
"scrying-pool",
|
||||||
"customPortraitFallback"
|
"customPortraitFallback"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -686,7 +644,6 @@ describe("PlayerPrivacyManager", () => {
|
|||||||
getFlag: vi.fn((scope, key) => {
|
getFlag: vi.fn((scope, key) => {
|
||||||
if (key === "customPortraitFallback") return dataURL;
|
if (key === "customPortraitFallback") return dataURL;
|
||||||
if (key === "reactionCamEnabled") return true;
|
if (key === "reactionCamEnabled") return true;
|
||||||
if (key === "hpReactiveCamStylingEnabled") return false;
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -815,7 +815,7 @@ describe('ScenePresetManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(mockScene.setFlag).toHaveBeenCalledWith(
|
expect(mockScene.setFlag).toHaveBeenCalledWith(
|
||||||
'video-view-manager',
|
'scrying-pool',
|
||||||
'presets',
|
'presets',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
_version: 1,
|
_version: 1,
|
||||||
|
|||||||
@@ -84,14 +84,15 @@ describe('ScryingPoolController', () => {
|
|||||||
// ── AC-2: action() happy path ─────────────────────────────────────────────
|
// ── AC-2: action() happy path ─────────────────────────────────────────────
|
||||||
|
|
||||||
describe('action() happy path (AC-2)', () => {
|
describe('action() happy path (AC-2)', () => {
|
||||||
it('stores a PendingOp in _pendingOps keyed by participantId', () => {
|
it('registers a PendingOp via socketHandler.registerPendingOp with correct shape', () => {
|
||||||
|
// With self-confirm, _pendingOps is cleared synchronously after action().
|
||||||
|
// Verify the op was passed to registerPendingOp before being confirmed.
|
||||||
controller.action('ui', 'user-1', 'hidden', 'op-1', 0);
|
controller.action('ui', 'user-1', 'hidden', 'op-1', 0);
|
||||||
expect(controller._pendingOps.has('user-1')).toBe(true);
|
expect(socketHandler.registerPendingOp).toHaveBeenCalledWith(
|
||||||
expect(controller._pendingOps.get('user-1')).toMatchObject({
|
expect.objectContaining({ opId: 'op-1', userId: 'user-1', targetState: 'hidden' }),
|
||||||
opId: 'op-1',
|
'scrying-pool.visibility.set',
|
||||||
userId: 'user-1',
|
expect.objectContaining({ opId: 'op-1' })
|
||||||
targetState: 'hidden',
|
);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls stateStore.setVisibility with the target state (optimistic update)', () => {
|
it('calls stateStore.setVisibility with the target state (optimistic update)', () => {
|
||||||
@@ -117,18 +118,24 @@ describe('ScryingPoolController', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fires Hooks.callAll scrying-pool:controllerAction with correct payload', () => {
|
it('fires Hooks.callAll scrying-pool:controllerAction after self-confirm', () => {
|
||||||
|
// Self-confirm calls _onEcho which fires the hook with source: 'echo'.
|
||||||
controller.action('ui', 'user-1', 'hidden', 'op-1', 0);
|
controller.action('ui', 'user-1', 'hidden', 'op-1', 0);
|
||||||
expect(hooksStub.callAll).toHaveBeenCalledWith(
|
expect(hooksStub.callAll).toHaveBeenCalledWith(
|
||||||
'scrying-pool:controllerAction',
|
'scrying-pool:controllerAction',
|
||||||
expect.objectContaining({ participantId: 'user-1', targetState: 'hidden', source: 'ui', opId: 'op-1' })
|
expect.objectContaining({ participantId: 'user-1', targetState: 'hidden', source: 'echo', opId: 'op-1' })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets previousState to null-coalesced "never-connected" when participant is new', () => {
|
it('sets previousState to "active" when participant is new (not yet in matrix)', () => {
|
||||||
|
// With self-confirm, _pendingOps is cleared synchronously. Verify via registerPendingOp arg.
|
||||||
controller.action('ui', 'new-user', 'hidden', 'op-1', 0);
|
controller.action('ui', 'new-user', 'hidden', 'op-1', 0);
|
||||||
const op = controller._pendingOps.get('new-user');
|
// 'active' is the render-time default for users not in the matrix.
|
||||||
expect(op.previousState).toBe('never-connected');
|
expect(socketHandler.registerPendingOp).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ previousState: 'active' }),
|
||||||
|
expect.any(String),
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -226,23 +233,30 @@ describe('ScryingPoolController', () => {
|
|||||||
return adapter.socket.on.mock.calls[0][1];
|
return adapter.socket.on.mock.calls[0][1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper: directly register a pending op (bypasses action() self-confirm)
|
||||||
|
function seedPendingOp(userId, opId, targetState = 'hidden') {
|
||||||
|
const op = { opId, userId, targetState, previousState: 'active' };
|
||||||
|
controller._pendingOps.set(userId, op);
|
||||||
|
socketHandler.registerPendingOp(op, 'scrying-pool.visibility.set', {});
|
||||||
|
}
|
||||||
|
|
||||||
it('calls socketHandler.confirmPendingOp with the opId', () => {
|
it('calls socketHandler.confirmPendingOp with the opId', () => {
|
||||||
controller.action('ui', 'user-1', 'hidden', 'op-1', 0);
|
|
||||||
const echoHandler = getEchoHandler();
|
const echoHandler = getEchoHandler();
|
||||||
|
seedPendingOp('user-1', 'op-1');
|
||||||
echoHandler({ opId: 'op-1', userId: 'user-1', state: 'hidden', revision: 1 });
|
echoHandler({ opId: 'op-1', userId: 'user-1', state: 'hidden', revision: 1 });
|
||||||
expect(socketHandler.confirmPendingOp).toHaveBeenCalledWith('op-1');
|
expect(socketHandler.confirmPendingOp).toHaveBeenCalledWith('op-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('stores the echo revision in _revisions for the userId', () => {
|
it('stores the echo revision in _revisions for the userId', () => {
|
||||||
controller.action('ui', 'user-1', 'hidden', 'op-2', 0);
|
|
||||||
const echoHandler = getEchoHandler();
|
const echoHandler = getEchoHandler();
|
||||||
|
seedPendingOp('user-1', 'op-2');
|
||||||
echoHandler({ opId: 'op-2', userId: 'user-1', state: 'hidden', revision: 7 });
|
echoHandler({ opId: 'op-2', userId: 'user-1', state: 'hidden', revision: 7 });
|
||||||
expect(controller._revisions.get('user-1')).toBe(7);
|
expect(controller._revisions.get('user-1')).toBe(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls stateStore.setVisibility with the authoritative state', () => {
|
it('calls stateStore.setVisibility with the authoritative state', () => {
|
||||||
controller.action('ui', 'user-1', 'active', 'op-3', 0);
|
|
||||||
const echoHandler = getEchoHandler();
|
const echoHandler = getEchoHandler();
|
||||||
|
seedPendingOp('user-1', 'op-3', 'active');
|
||||||
const setSpy = vi.spyOn(stateStore, 'setVisibility');
|
const setSpy = vi.spyOn(stateStore, 'setVisibility');
|
||||||
|
|
||||||
echoHandler({ opId: 'op-3', userId: 'user-1', state: 'active', revision: 2 });
|
echoHandler({ opId: 'op-3', userId: 'user-1', state: 'active', revision: 2 });
|
||||||
@@ -251,8 +265,8 @@ describe('ScryingPoolController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('fires Hooks.callAll scrying-pool:controllerAction with source: echo', () => {
|
it('fires Hooks.callAll scrying-pool:controllerAction with source: echo', () => {
|
||||||
controller.action('ui', 'user-1', 'hidden', 'op-4', 0);
|
|
||||||
const echoHandler = getEchoHandler();
|
const echoHandler = getEchoHandler();
|
||||||
|
seedPendingOp('user-1', 'op-4');
|
||||||
echoHandler({ opId: 'op-4', userId: 'user-1', state: 'hidden', revision: 1 });
|
echoHandler({ opId: 'op-4', userId: 'user-1', state: 'hidden', revision: 1 });
|
||||||
|
|
||||||
expect(hooksStub.callAll).toHaveBeenCalledWith(
|
expect(hooksStub.callAll).toHaveBeenCalledWith(
|
||||||
@@ -262,20 +276,18 @@ describe('ScryingPoolController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('removes the participant from _pendingOps after echo', () => {
|
it('removes the participant from _pendingOps after echo', () => {
|
||||||
// Register a pending op first
|
const echoHandler = getEchoHandler();
|
||||||
controller.action('ui', 'user-1', 'hidden', 'op-1', 0);
|
seedPendingOp('user-1', 'op-1');
|
||||||
expect(controller._pendingOps.has('user-1')).toBe(true);
|
expect(controller._pendingOps.has('user-1')).toBe(true);
|
||||||
|
|
||||||
const echoHandler = getEchoHandler();
|
|
||||||
echoHandler({ opId: 'op-1', userId: 'user-1', state: 'hidden', revision: 1 });
|
echoHandler({ opId: 'op-1', userId: 'user-1', state: 'hidden', revision: 1 });
|
||||||
|
|
||||||
expect(controller._pendingOps.has('user-1')).toBe(false);
|
expect(controller._pendingOps.has('user-1')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('defaults revision to 0 when echo payload omits revision field', () => {
|
it('defaults revision to 0 when echo payload omits revision field', () => {
|
||||||
// Register a pending op first (required by new validation)
|
|
||||||
controller.action('ui', 'user-1', 'hidden', 'op-1', 0);
|
|
||||||
const echoHandler = getEchoHandler();
|
const echoHandler = getEchoHandler();
|
||||||
|
seedPendingOp('user-1', 'op-1');
|
||||||
echoHandler({ opId: 'op-1', userId: 'user-1', state: 'hidden' }); // no revision
|
echoHandler({ opId: 'op-1', userId: 'user-1', state: 'hidden' }); // no revision
|
||||||
expect(controller._revisions.get('user-1')).toBe(0);
|
expect(controller._revisions.get('user-1')).toBe(0);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -527,41 +527,41 @@ describe('FoundryAdapter surface delegation', () => {
|
|||||||
describe('user flag methods', () => {
|
describe('user flag methods', () => {
|
||||||
it('users.getFlag returns flag value for valid user, scope, and key', () => {
|
it('users.getFlag returns flag value for valid user, scope, and key', () => {
|
||||||
// First set a flag on the GM user
|
// First set a flag on the GM user
|
||||||
GM_USER.setFlag('video-view-manager', 'testFlag', 'testValue');
|
GM_USER.setFlag('scrying-pool', 'testFlag', 'testValue');
|
||||||
const result = adapter.users.getFlag(GM_USER.id, 'video-view-manager', 'testFlag');
|
const result = adapter.users.getFlag(GM_USER.id, 'scrying-pool', 'testFlag');
|
||||||
expect(result).toBe('testValue');
|
expect(result).toBe('testValue');
|
||||||
expect(USERS_STUB.get).toHaveBeenCalledWith(GM_USER.id);
|
expect(USERS_STUB.get).toHaveBeenCalledWith(GM_USER.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('users.getFlag returns null when flag does not exist', () => {
|
it('users.getFlag returns null when flag does not exist', () => {
|
||||||
const result = adapter.users.getFlag(GM_USER.id, 'video-view-manager', 'nonExistentFlag');
|
const result = adapter.users.getFlag(GM_USER.id, 'scrying-pool', 'nonExistentFlag');
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('users.getFlag returns null when user does not exist', () => {
|
it('users.getFlag returns null when user does not exist', () => {
|
||||||
const result = adapter.users.getFlag('unknown-user-id', 'video-view-manager', 'testFlag');
|
const result = adapter.users.getFlag('unknown-user-id', 'scrying-pool', 'testFlag');
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
expect(USERS_STUB.get).toHaveBeenCalledWith('unknown-user-id');
|
expect(USERS_STUB.get).toHaveBeenCalledWith('unknown-user-id');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('users.setFlag sets flag value for valid user', async () => {
|
it('users.setFlag sets flag value for valid user', async () => {
|
||||||
const promise = adapter.users.setFlag(PLAYER_USER.id, 'video-view-manager', 'reactionCamEnabled', true);
|
const promise = adapter.users.setFlag(PLAYER_USER.id, 'scrying-pool', 'reactionCamEnabled', true);
|
||||||
expect(promise).not.toBeNull();
|
expect(promise).not.toBeNull();
|
||||||
await promise;
|
await promise;
|
||||||
expect(USERS_STUB.get).toHaveBeenCalledWith(PLAYER_USER.id);
|
expect(USERS_STUB.get).toHaveBeenCalledWith(PLAYER_USER.id);
|
||||||
// Verify the flag was set
|
// Verify the flag was set
|
||||||
expect(PLAYER_USER.getFlag('video-view-manager', 'reactionCamEnabled')).toBe(true);
|
expect(PLAYER_USER.getFlag('scrying-pool', 'reactionCamEnabled')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('users.setFlag returns null when user does not exist', () => {
|
it('users.setFlag returns null when user does not exist', () => {
|
||||||
const promise = adapter.users.setFlag('unknown-user-id', 'video-view-manager', 'testFlag', true);
|
const promise = adapter.users.setFlag('unknown-user-id', 'scrying-pool', 'testFlag', true);
|
||||||
expect(promise).toBeNull();
|
expect(promise).toBeNull();
|
||||||
expect(USERS_STUB.get).toHaveBeenCalledWith('unknown-user-id');
|
expect(USERS_STUB.get).toHaveBeenCalledWith('unknown-user-id');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('users.getFlagModule returns module-scoped flag', () => {
|
it('users.getFlagModule returns module-scoped flag', () => {
|
||||||
GM_USER.setFlag('video-view-manager', 'hpReactiveCamStylingEnabled', false);
|
GM_USER.setFlag('scrying-pool', 'reactionCamEnabled', false);
|
||||||
const result = adapter.users.getFlagModule(GM_USER.id, 'hpReactiveCamStylingEnabled');
|
const result = adapter.users.getFlagModule(GM_USER.id, 'reactionCamEnabled');
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -574,7 +574,7 @@ describe('FoundryAdapter surface delegation', () => {
|
|||||||
const promise = adapter.users.setFlagModule(PLAYER_USER.id, 'reactionCamEnabled', true);
|
const promise = adapter.users.setFlagModule(PLAYER_USER.id, 'reactionCamEnabled', true);
|
||||||
expect(promise).not.toBeNull();
|
expect(promise).not.toBeNull();
|
||||||
await promise;
|
await promise;
|
||||||
expect(PLAYER_USER.getFlag('video-view-manager', 'reactionCamEnabled')).toBe(true);
|
expect(PLAYER_USER.getFlag('scrying-pool', 'reactionCamEnabled')).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ function makeAdapter({
|
|||||||
localize: vi.fn((key, data) => {
|
localize: vi.fn((key, data) => {
|
||||||
// Simple mock that returns the key with data substituted
|
// Simple mock that returns the key with data substituted
|
||||||
const messages = {
|
const messages = {
|
||||||
'video-view-manager.notifications.personalHidden': 'GM has hidden your camera. Your portrait is shown to other Participants.',
|
'scrying-pool.notifications.personalHidden': 'GM has hidden your camera. Your portrait is shown to other Participants.',
|
||||||
'video-view-manager.notifications.personalShowed': 'Your camera is now visible to the table.',
|
'scrying-pool.notifications.personalShowed': 'Your camera is now visible to the table.',
|
||||||
'video-view-manager.notifications.gmHid': 'GM hid {name}\'s camera',
|
'scrying-pool.notifications.gmHid': 'GM hid {name}\'s camera',
|
||||||
'video-view-manager.notifications.gmShowed': 'GM showed {name}\'s camera',
|
'scrying-pool.notifications.gmShowed': 'GM showed {name}\'s camera',
|
||||||
};
|
};
|
||||||
let msg = messages[key] ?? key;
|
let msg = messages[key] ?? key;
|
||||||
if (data?.name) {
|
if (data?.name) {
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ function createMockAdapter(overrides = {}) {
|
|||||||
localize: vi.fn((key) => {
|
localize: vi.fn((key) => {
|
||||||
// For testing, return strings with placeholders that match ConfirmationBar's .replace() calls
|
// For testing, return strings with placeholders that match ConfirmationBar's .replace() calls
|
||||||
const translations = {
|
const translations = {
|
||||||
'video-view-manager.presets.confirmation.applied': 'Preset applied — {name}',
|
'scrying-pool.presets.confirmation.applied': 'Preset applied — {name}',
|
||||||
'video-view-manager.presets.confirmation.counts': '{hidden} hidden, {visible} visible',
|
'scrying-pool.presets.confirmation.counts': '{hidden} hidden, {visible} visible',
|
||||||
'video-view-manager.presets.confirmation.partial-fail': '(some updates pending)',
|
'scrying-pool.presets.confirmation.partial-fail': '(some updates pending)',
|
||||||
'video-view-manager.presets.confirmation.undo': 'Undo preset apply',
|
'scrying-pool.presets.confirmation.undo': 'Undo preset apply',
|
||||||
};
|
};
|
||||||
return translations[key] ?? key;
|
return translations[key] ?? key;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ describe('DirectorsBoard', () => {
|
|||||||
|
|
||||||
it('has DEFAULT_OPTIONS with position', () => {
|
it('has DEFAULT_OPTIONS with position', () => {
|
||||||
expect(DirectorsBoard.DEFAULT_OPTIONS.position).toEqual({
|
expect(DirectorsBoard.DEFAULT_OPTIONS.position).toEqual({
|
||||||
width: 400,
|
width: 420,
|
||||||
height: 300,
|
height: 480,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -110,7 +110,7 @@ describe('DirectorsBoard', () => {
|
|||||||
boardWithOptions._loadPosition();
|
boardWithOptions._loadPosition();
|
||||||
|
|
||||||
expect(game.user.getFlag).toHaveBeenCalledWith(
|
expect(game.user.getFlag).toHaveBeenCalledWith(
|
||||||
'video-view-manager',
|
'scrying-pool',
|
||||||
'directorsBoardState'
|
'directorsBoardState'
|
||||||
);
|
);
|
||||||
// Position should be merged into options.position (not replaced)
|
// Position should be merged into options.position (not replaced)
|
||||||
@@ -136,8 +136,8 @@ describe('DirectorsBoard', () => {
|
|||||||
expect(boardWithOptions.options.position).toEqual({
|
expect(boardWithOptions.options.position).toEqual({
|
||||||
left: 100,
|
left: 100,
|
||||||
top: 200,
|
top: 200,
|
||||||
width: 400,
|
width: 420,
|
||||||
height: 300,
|
height: 480,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ describe('PresetLoadDialog', () => {
|
|||||||
it('should return hasPresets false when no presets exist', async () => {
|
it('should return hasPresets false when no presets exist', async () => {
|
||||||
scenePresetManager.list.mockReturnValue([]);
|
scenePresetManager.list.mockReturnValue([]);
|
||||||
adapter.i18n.localize = vi.fn((key) => {
|
adapter.i18n.localize = vi.fn((key) => {
|
||||||
if (key === 'video-view-manager.presets.load.emptyMessage') return 'No presets available';
|
if (key === 'scrying-pool.presets.load.emptyMessage') return 'No presets available';
|
||||||
return key;
|
return key;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -163,10 +163,10 @@ describe('PresetLoadDialog', () => {
|
|||||||
it('should use i18n for labels', async () => {
|
it('should use i18n for labels', async () => {
|
||||||
adapter.i18n.localize = vi.fn((key) => {
|
adapter.i18n.localize = vi.fn((key) => {
|
||||||
const translations = {
|
const translations = {
|
||||||
'video-view-manager.presets.load.loadButton': 'Load',
|
'scrying-pool.presets.load.loadButton': 'Load',
|
||||||
'video-view-manager.presets.load.cancelButton': 'Cancel',
|
'scrying-pool.presets.load.cancelButton': 'Cancel',
|
||||||
'video-view-manager.presets.load.title': 'Load Preset',
|
'scrying-pool.presets.load.title': 'Load Preset',
|
||||||
'video-view-manager.presets.load.emptyMessage': 'No presets',
|
'scrying-pool.presets.load.emptyMessage': 'No presets',
|
||||||
};
|
};
|
||||||
return translations[key] || key;
|
return translations[key] || key;
|
||||||
});
|
});
|
||||||
@@ -276,7 +276,7 @@ describe('PresetLoadDialog', () => {
|
|||||||
|
|
||||||
it('should show notification on successful load via adapter.notifications', async () => {
|
it('should show notification on successful load via adapter.notifications', async () => {
|
||||||
adapter.i18n.localize = vi.fn((key) => {
|
adapter.i18n.localize = vi.fn((key) => {
|
||||||
if (key === 'video-view-manager.presets.notifications.applied') return 'Applied preset: {name}';
|
if (key === 'scrying-pool.presets.notifications.applied') return 'Applied preset: {name}';
|
||||||
return key;
|
return key;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -398,7 +398,7 @@ describe('PresetLoadDialog', () => {
|
|||||||
|
|
||||||
it('should use the correct template path', () => {
|
it('should use the correct template path', () => {
|
||||||
expect(PresetLoadDialog.PARTS.dialog.template).toBe(
|
expect(PresetLoadDialog.PARTS.dialog.template).toBe(
|
||||||
'modules/video-view-manager/templates/preset-load-dialog.hbs'
|
'modules/scrying-pool/templates/preset-load-dialog.hbs'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ describe('PresetSaveDialog', () => {
|
|||||||
|
|
||||||
it('should return empty string as defaultName when no presets exist', async () => {
|
it('should return empty string as defaultName when no presets exist', async () => {
|
||||||
adapter.i18n.localize = vi.fn((key) => {
|
adapter.i18n.localize = vi.fn((key) => {
|
||||||
if (key === 'video-view-manager.presets.save.namePlaceholder') return 'Enter preset name';
|
if (key === 'scrying-pool.presets.save.namePlaceholder') return 'Enter preset name';
|
||||||
return key;
|
return key;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -146,11 +146,11 @@ describe('PresetSaveDialog', () => {
|
|||||||
it('should return all i18n labels', async () => {
|
it('should return all i18n labels', async () => {
|
||||||
adapter.i18n.localize = vi.fn((key) => {
|
adapter.i18n.localize = vi.fn((key) => {
|
||||||
const translations = {
|
const translations = {
|
||||||
'video-view-manager.presets.save.saveButton': 'Save',
|
'scrying-pool.presets.save.saveButton': 'Save',
|
||||||
'video-view-manager.presets.save.cancelButton': 'Cancel',
|
'scrying-pool.presets.save.cancelButton': 'Cancel',
|
||||||
'video-view-manager.presets.save.title': 'Save Preset',
|
'scrying-pool.presets.save.title': 'Save Preset',
|
||||||
'video-view-manager.presets.save.nameLabel': 'Preset Name',
|
'scrying-pool.presets.save.nameLabel': 'Preset Name',
|
||||||
'video-view-manager.presets.save.namePlaceholder': 'Enter preset name',
|
'scrying-pool.presets.save.namePlaceholder': 'Enter preset name',
|
||||||
};
|
};
|
||||||
return translations[key] || key;
|
return translations[key] || key;
|
||||||
});
|
});
|
||||||
@@ -320,7 +320,7 @@ describe('PresetSaveDialog', () => {
|
|||||||
scenePresetManager.save = vi.fn().mockResolvedValue({ name: 'My Preset' });
|
scenePresetManager.save = vi.fn().mockResolvedValue({ name: 'My Preset' });
|
||||||
dialog.close = vi.fn().mockResolvedValue({});
|
dialog.close = vi.fn().mockResolvedValue({});
|
||||||
adapter.i18n.localize = vi.fn((key) => {
|
adapter.i18n.localize = vi.fn((key) => {
|
||||||
if (key === 'video-view-manager.presets.notifications.saved') return 'Preset {name} saved!';
|
if (key === 'scrying-pool.presets.notifications.saved') return 'Preset {name} saved!';
|
||||||
return key;
|
return key;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -447,7 +447,7 @@ describe('PresetSaveDialog', () => {
|
|||||||
|
|
||||||
it('should use the correct template path', () => {
|
it('should use the correct template path', () => {
|
||||||
expect(PresetSaveDialog.PARTS.dialog.template).toBe(
|
expect(PresetSaveDialog.PARTS.dialog.template).toBe(
|
||||||
'modules/video-view-manager/templates/preset-save-dialog.hbs'
|
'modules/scrying-pool/templates/preset-save-dialog.hbs'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -459,7 +459,7 @@ describe('PresetSaveDialog', () => {
|
|||||||
expect(options.classes).toContain('preset-save-dialog');
|
expect(options.classes).toContain('preset-save-dialog');
|
||||||
expect(options.window.title).toBe('Save Scene Preset');
|
expect(options.window.title).toBe('Save Scene Preset');
|
||||||
expect(options.window.resizable).toBe(false);
|
expect(options.window.resizable).toBe(false);
|
||||||
expect(options.position.width).toBe(320);
|
expect(options.position.width).toBe(360);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should store references to dependencies', () => {
|
it('should store references to dependencies', () => {
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ describe('ScenePresetPanel', () => {
|
|||||||
|
|
||||||
it('sets aria-label using i18n', () => {
|
it('sets aria-label using i18n', () => {
|
||||||
panel.init();
|
panel.init();
|
||||||
expect(adapter.i18n.localize).toHaveBeenCalledWith('video-view-manager.scenePresetPanel.title');
|
expect(adapter.i18n.localize).toHaveBeenCalledWith('scrying-pool.scenePresetPanel.title');
|
||||||
expect(panel._element.getAttribute('aria-label')).toBe('video-view-manager.scenePresetPanel.title');
|
expect(panel._element.getAttribute('aria-label')).toBe('scrying-pool.scenePresetPanel.title');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets aria-expanded to false initially', () => {
|
it('sets aria-expanded to false initially', () => {
|
||||||
@@ -267,7 +267,7 @@ describe('ScenePresetPanel', () => {
|
|||||||
|
|
||||||
it('uses i18n for message', () => {
|
it('uses i18n for message', () => {
|
||||||
panel._buildEmptyHtml();
|
panel._buildEmptyHtml();
|
||||||
expect(adapter.i18n.localize).toHaveBeenCalledWith('video-view-manager.scenePresetPanel.noScene');
|
expect(adapter.i18n.localize).toHaveBeenCalledWith('scrying-pool.scenePresetPanel.noScene');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('escapes HTML in message', () => {
|
it('escapes HTML in message', () => {
|
||||||
@@ -450,7 +450,7 @@ describe('ScenePresetPanel', () => {
|
|||||||
mockTarget.checked = true;
|
mockTarget.checked = true;
|
||||||
await panel._onToggleAutoApply(mockTarget);
|
await panel._onToggleAutoApply(mockTarget);
|
||||||
expect(adapter.notifications.info).toHaveBeenCalledWith(
|
expect(adapter.notifications.info).toHaveBeenCalledWith(
|
||||||
'video-view-manager.scenePresetPanel.notifications.enabled'
|
'scrying-pool.scenePresetPanel.notifications.enabled'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -460,7 +460,7 @@ describe('ScenePresetPanel', () => {
|
|||||||
mockTarget.checked = false;
|
mockTarget.checked = false;
|
||||||
await panel._onToggleAutoApply(mockTarget);
|
await panel._onToggleAutoApply(mockTarget);
|
||||||
expect(adapter.notifications.info).toHaveBeenCalledWith(
|
expect(adapter.notifications.info).toHaveBeenCalledWith(
|
||||||
'video-view-manager.scenePresetPanel.notifications.disabled'
|
'scrying-pool.scenePresetPanel.notifications.disabled'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -241,6 +241,30 @@ describe('ScryingPoolStrip', () => {
|
|||||||
const data = strip.getData();
|
const data = strip.getData();
|
||||||
expect(data.hasStreamAccess).toBe(false);
|
expect(data.hasStreamAccess).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('includes current user when showGMSelfFeed is true', () => {
|
||||||
|
adapter.settings = { get: vi.fn(() => true) };
|
||||||
|
adapter.users.all.mockReturnValue([{ id: 'u1' }, { id: 'u2' }]);
|
||||||
|
const data = strip.getData();
|
||||||
|
expect(data.participants.map(p => p.userId)).toContain('u1');
|
||||||
|
expect(data.participants.map(p => p.userId)).toContain('u2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('excludes current user when showGMSelfFeed is false', () => {
|
||||||
|
adapter.settings = { get: vi.fn(() => false) };
|
||||||
|
adapter.users.all.mockReturnValue([{ id: 'u1' }, { id: 'u2' }]);
|
||||||
|
const data = strip.getData();
|
||||||
|
// u1 is the current user (mocked in beforeEach), should be excluded
|
||||||
|
expect(data.participants.map(p => p.userId)).not.toContain('u1');
|
||||||
|
expect(data.participants.map(p => p.userId)).toContain('u2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes all users when settings is unavailable (defaults to true)', () => {
|
||||||
|
// no adapter.settings — fallback to true
|
||||||
|
adapter.users.all.mockReturnValue([{ id: 'u1' }, { id: 'u2' }]);
|
||||||
|
const data = strip.getData();
|
||||||
|
expect(data.participants.length).toBe(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('_attachVideoStream() (Story 5.1)', () => {
|
describe('_attachVideoStream() (Story 5.1)', () => {
|
||||||
|
|||||||
@@ -31,8 +31,6 @@ function createMockAdapter(overrides = {}) {
|
|||||||
'SCRYING_POOL.PrivacyPanel.sectionDescription': 'Control which automation effects can affect your camera.',
|
'SCRYING_POOL.PrivacyPanel.sectionDescription': 'Control which automation effects can affect your camera.',
|
||||||
'SCRYING_POOL.PrivacyPanel.reactionCamLabel': 'Reaction Cam',
|
'SCRYING_POOL.PrivacyPanel.reactionCamLabel': 'Reaction Cam',
|
||||||
'SCRYING_POOL.PrivacyPanel.reactionCamDescription': 'Automatically show your camera during key moments (combat, rolls, etc.)',
|
'SCRYING_POOL.PrivacyPanel.reactionCamDescription': 'Automatically show your camera during key moments (combat, rolls, etc.)',
|
||||||
'SCRYING_POOL.PrivacyPanel.hpReactiveCamStylingLabel': 'HP-Reactive Cam Styling',
|
|
||||||
'SCRYING_POOL.PrivacyPanel.hpReactiveCamStylingDescription': 'Apply visual styling to your camera based on your character\'s HP',
|
|
||||||
'SCRYING_POOL.PrivacyPanel.toggleOn': 'Enabled',
|
'SCRYING_POOL.PrivacyPanel.toggleOn': 'Enabled',
|
||||||
'SCRYING_POOL.PrivacyPanel.toggleOff': 'Disabled',
|
'SCRYING_POOL.PrivacyPanel.toggleOff': 'Disabled',
|
||||||
'SCRYING_POOL.PrivacyPanel.readOnlyNotice': 'This player\'s privacy settings are read-only',
|
'SCRYING_POOL.PrivacyPanel.readOnlyNotice': 'This player\'s privacy settings are read-only',
|
||||||
@@ -130,7 +128,6 @@ describe('PlayerPrivacyPanel', () => {
|
|||||||
it('should return context with settings', async () => {
|
it('should return context with settings', async () => {
|
||||||
const settings = createPrivacySettings({
|
const settings = createPrivacySettings({
|
||||||
reactionCamEnabled: true,
|
reactionCamEnabled: true,
|
||||||
hpReactiveCamStylingEnabled: false,
|
|
||||||
});
|
});
|
||||||
playerPrivacyManager.getSettings.mockReturnValue(settings);
|
playerPrivacyManager.getSettings.mockReturnValue(settings);
|
||||||
adapter.users.current.mockReturnValue({ id: targetUserId });
|
adapter.users.current.mockReturnValue({ id: targetUserId });
|
||||||
@@ -140,7 +137,7 @@ describe('PlayerPrivacyPanel', () => {
|
|||||||
expect(context.title).toBe('Player Privacy Panel');
|
expect(context.title).toBe('Player Privacy Panel');
|
||||||
expect(context.sectionHeader).toBe('Automation Opt-ins');
|
expect(context.sectionHeader).toBe('Automation Opt-ins');
|
||||||
expect(context.automationEffects).toBeDefined();
|
expect(context.automationEffects).toBeDefined();
|
||||||
expect(context.automationEffects).toHaveLength(2);
|
expect(context.automationEffects).toHaveLength(1);
|
||||||
expect(context.isReadOnly).toBe(false);
|
expect(context.isReadOnly).toBe(false);
|
||||||
expect(context.isOwnUser).toBe(true);
|
expect(context.isOwnUser).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -156,25 +153,22 @@ describe('PlayerPrivacyPanel', () => {
|
|||||||
expect(context.isOwnUser).toBe(false);
|
expect(context.isOwnUser).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include both automation effects', async () => {
|
it('should include the available automation effect', async () => {
|
||||||
const context = await panel._prepareContext();
|
const context = await panel._prepareContext();
|
||||||
|
|
||||||
expect(context.automationEffects).toHaveLength(2);
|
expect(context.automationEffects).toHaveLength(1);
|
||||||
expect(context.automationEffects[0].key).toBe('reactionCam');
|
expect(context.automationEffects[0].key).toBe('reactionCam');
|
||||||
expect(context.automationEffects[1].key).toBe('hpReactiveCamStyling');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reflect current settings in context', async () => {
|
it('should reflect current settings in context', async () => {
|
||||||
const settings = createPrivacySettings({
|
const settings = createPrivacySettings({
|
||||||
reactionCamEnabled: true,
|
reactionCamEnabled: true,
|
||||||
hpReactiveCamStylingEnabled: true,
|
|
||||||
});
|
});
|
||||||
playerPrivacyManager.getSettings.mockReturnValue(settings);
|
playerPrivacyManager.getSettings.mockReturnValue(settings);
|
||||||
|
|
||||||
const context = await panel._prepareContext();
|
const context = await panel._prepareContext();
|
||||||
|
|
||||||
expect(context.automationEffects[0].enabled).toBe(true);
|
expect(context.automationEffects[0].enabled).toBe(true);
|
||||||
expect(context.automationEffects[1].enabled).toBe(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -354,13 +348,11 @@ describe('PlayerPrivacyPanel', () => {
|
|||||||
it('should clear cached elements', () => {
|
it('should clear cached elements', () => {
|
||||||
// Set up some cached values
|
// Set up some cached values
|
||||||
panel._reactionCamToggle = document.createElement('div');
|
panel._reactionCamToggle = document.createElement('div');
|
||||||
panel._hpReactiveCamToggle = document.createElement('div');
|
|
||||||
panel._currentSettings = createPrivacySettings();
|
panel._currentSettings = createPrivacySettings();
|
||||||
|
|
||||||
panel._onClose();
|
panel._onClose();
|
||||||
|
|
||||||
expect(panel._reactionCamToggle).toBe(null);
|
expect(panel._reactionCamToggle).toBe(null);
|
||||||
expect(panel._hpReactiveCamToggle).toBe(null);
|
|
||||||
expect(panel._currentSettings).toBe(null);
|
expect(panel._currentSettings).toBe(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ describe('VisibilityBadge', () => {
|
|||||||
badge.init();
|
badge.init();
|
||||||
await badge._setFirstBadgeEncountered();
|
await badge._setFirstBadgeEncountered();
|
||||||
const mockUser = adapter.users.current();
|
const mockUser = adapter.users.current();
|
||||||
expect(mockUser.setFlag).toHaveBeenCalledWith('video-view-manager', 'firstBadgeEncounter', true);
|
expect(mockUser.setFlag).toHaveBeenCalledWith('scrying-pool', 'firstBadgeEncounter', true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -390,7 +390,7 @@ describe('FirstEncounterPanel (via VisibilityBadge)', () => {
|
|||||||
// Directly call _onGotIt to avoid async click handler timing issues
|
// Directly call _onGotIt to avoid async click handler timing issues
|
||||||
await panel._onGotIt();
|
await panel._onGotIt();
|
||||||
const mockUser = adapter.users.current();
|
const mockUser = adapter.users.current();
|
||||||
expect(mockUser.setFlag).toHaveBeenCalledWith('video-view-manager', 'firstBadgeEncounter', true);
|
expect(mockUser.setFlag).toHaveBeenCalledWith('scrying-pool', 'firstBadgeEncounter', true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clears timer (no ghost timer after dismissal)', async () => {
|
it('clears timer (no ghost timer after dismissal)', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user