@@ -684,10 +684,13 @@ class CustomDrawingSheet extends DrawingConfig {
684684 */
685685class NotePreviewer extends HandlebarsApplicationMixin ( ApplicationV2 ) {
686686 constructor ( document , options = { } ) {
687+ // Ensure singleton instance per document
688+ options . id = `note-preview-${ document . id } ` ;
687689 super ( options ) ;
688690 this . document = document ;
689691 this . localSound = null ;
690692 this . globalSoundActive = false ;
693+ this . audioPollInterval = null ;
691694 }
692695
693696 static DEFAULT_OPTIONS = {
@@ -771,6 +774,26 @@ class NotePreviewer extends HandlebarsApplicationMixin(ApplicationV2) {
771774 localAudio . addEventListener ( "ended" , ( ) => {
772775 if ( ! this . globalSoundActive ) cassette . classList . remove ( "playing" ) ;
773776 } ) ;
777+
778+ // Poll for external playback (e.g. from canvas context menu) to sync animation
779+ const audioPath = this . document . flags [ MODULE_ID ] ?. audioPath ;
780+ if ( audioPath ) {
781+ this . audioPollInterval = setInterval ( ( ) => {
782+ // Check if any sound with this source is currently playing
783+ const isPlaying = Array . from ( game . audio . playing . values ( ) ) . some ( s => s . src === audioPath && s . playing ) ;
784+
785+ if ( isPlaying ) {
786+ if ( ! cassette . classList . contains ( "playing" ) ) {
787+ cassette . classList . add ( "playing" ) ;
788+ }
789+ } else {
790+ // Only remove if not locally playing (checked via audio element) and not globally active state
791+ if ( localAudio . paused && ! this . globalSoundActive && cassette . classList . contains ( "playing" ) ) {
792+ cassette . classList . remove ( "playing" ) ;
793+ }
794+ }
795+ } , 500 ) ;
796+ }
774797 }
775798
776799 // Global Play Button (GM only)
@@ -852,9 +875,32 @@ class NotePreviewer extends HandlebarsApplicationMixin(ApplicationV2) {
852875 }
853876 } ) ;
854877 } ) ;
878+
879+ // Handle auto-play options passed from Context Menu
880+ if ( options . autoplay ) {
881+ if ( localAudio && localAudio . paused ) {
882+ // Small delay to ensure DOM is ready and constraints are met
883+ setTimeout ( ( ) => {
884+ localAudio . play ( ) . catch ( e => console . warn ( "Investigation Board: Auto-play blocked by browser policy" , e ) ) ;
885+ } , 100 ) ;
886+ }
887+ }
888+
889+ if ( options . autobroadcast && playGlobalBtn ) {
890+ // Small delay to ensure logic is ready
891+ setTimeout ( ( ) => {
892+ if ( ! playGlobalBtn . classList . contains ( "active" ) ) {
893+ playGlobalBtn . click ( ) ;
894+ }
895+ } , 100 ) ;
896+ }
855897 }
856898
857899 async _onClose ( options ) {
900+ if ( this . audioPollInterval ) {
901+ clearInterval ( this . audioPollInterval ) ;
902+ this . audioPollInterval = null ;
903+ }
858904 if ( this . localSound ) {
859905 this . localSound . stop ( ) ;
860906 this . localSound = null ;
@@ -1045,14 +1091,22 @@ class CustomDrawing extends Drawing {
10451091
10461092 // Media-specific options
10471093 if ( noteData . type === "media" && noteData . audioPath ) {
1048- // Local Play/Stop
1049- const isLocalPlaying = Array . from ( game . audio . playing . values ( ) ) . some ( s => s . src === noteData . audioPath ) ;
1094+ const appId = `note-preview-${ this . document . id } ` ;
1095+ const app = foundry . applications . instances . get ( appId ) ;
1096+ const audioEl = app ?. element ?. querySelector ( '.local-audio-player' ) ;
1097+ const isAppPlaying = audioEl && ! audioEl . paused ;
1098+ const isLegacyPlaying = Array . from ( game . audio . playing . values ( ) ) . some ( s => s . src === noteData . audioPath ) ;
1099+ const isPlaying = isAppPlaying || isLegacyPlaying ;
1100+
10501101 const playMeOption = document . createElement ( 'div' ) ;
10511102
1052- if ( isLocalPlaying ) {
1103+ if ( isPlaying ) {
10531104 playMeOption . innerHTML = '<i class="fas fa-stop"></i> Stop for Me' ;
10541105 playMeOption . onclick = ( e ) => {
10551106 e . stopPropagation ( ) ;
1107+ // Stop App Audio
1108+ if ( audioEl ) audioEl . pause ( ) ;
1109+ // Stop Legacy Audio
10561110 const sounds = Array . from ( game . audio . playing . values ( ) ) . filter ( s => s . src === noteData . audioPath ) ;
10571111 sounds . forEach ( s => s . stop ( ) ) ;
10581112 menu . remove ( ) ;
@@ -1061,11 +1115,8 @@ class CustomDrawing extends Drawing {
10611115 playMeOption . innerHTML = '<i class="fas fa-volume-up"></i> Play for Me' ;
10621116 playMeOption . onclick = ( e ) => {
10631117 e . stopPropagation ( ) ;
1064- // Check if already playing locally
1065- if ( Array . from ( game . audio . playing . values ( ) ) . some ( s => s . src === noteData . audioPath && s . playing ) ) {
1066- return ;
1067- }
1068- game . audio . play ( noteData . audioPath , { volume : 0.8 } ) ;
1118+ // Open Preview with autoplay
1119+ new NotePreviewer ( this . document ) . render ( true , { autoplay : true } ) ;
10691120 menu . remove ( ) ;
10701121 } ;
10711122 }
@@ -1080,12 +1131,22 @@ class CustomDrawing extends Drawing {
10801131 playAllOption . innerHTML = '<i class="fas fa-stop"></i> Stop for All' ;
10811132 playAllOption . onclick = ( e ) => {
10821133 e . stopPropagation ( ) ;
1134+ // Try to use App control first
1135+ if ( app ) {
1136+ const globalBtn = app . element . querySelector ( '.global-toggle' ) ;
1137+ if ( globalBtn && globalBtn . classList . contains ( 'active' ) ) {
1138+ globalBtn . click ( ) ;
1139+ menu . remove ( ) ;
1140+ return ;
1141+ }
1142+ }
1143+
1144+ // Fallback to manual socket stop
10831145 if ( socket ) {
10841146 socket . emit ( SOCKET_NAME , {
10851147 action : "stopAudio" ,
10861148 audioPath : noteData . audioPath
10871149 } ) ;
1088- // Stop locally too (GM is a client)
10891150 const sound = activeGlobalSounds . get ( noteData . audioPath ) ;
10901151 if ( sound ) {
10911152 sound . stop ( ) ;
@@ -1098,22 +1159,8 @@ class CustomDrawing extends Drawing {
10981159 playAllOption . innerHTML = '<i class="fas fa-broadcast-tower"></i> Play for All' ;
10991160 playAllOption . onclick = ( e ) => {
11001161 e . stopPropagation ( ) ;
1101- // Check if already playing globally
1102- if ( activeGlobalSounds . has ( noteData . audioPath ) && activeGlobalSounds . get ( noteData . audioPath ) . playing ) {
1103- return ;
1104- }
1105-
1106- if ( socket ) {
1107- socket . emit ( SOCKET_NAME , {
1108- action : "playAudio" ,
1109- audioPath : noteData . audioPath
1110- } ) ;
1111- ( async ( ) => {
1112- const sound = await game . audio . play ( noteData . audioPath , { volume : 0.8 } ) ;
1113- if ( sound ) activeGlobalSounds . set ( noteData . audioPath , sound ) ;
1114- } ) ( ) ;
1115- ui . notifications . info ( `Playing for everyone: ${ noteData . audioPath } ` ) ;
1116- }
1162+ // Unified: Open Preview and broadcast
1163+ new NotePreviewer ( this . document ) . render ( true , { autobroadcast : true } ) ;
11171164 menu . remove ( ) ;
11181165 } ;
11191166 }
@@ -1138,12 +1185,11 @@ class CustomDrawing extends Drawing {
11381185 e . stopPropagation ( ) ;
11391186 menu . remove ( ) ;
11401187
1141- const confirm = await Dialog . confirm ( {
1142- title : "Remove All Connections" ,
1188+ const confirm = await foundry . applications . api . DialogV2 . confirm ( {
1189+ window : { title : "Remove All Connections" } ,
11431190 content : `<p>Are you sure you want to remove ALL yarn connections connected to this note (incoming and outgoing)?</p>` ,
1144- yes : ( ) => true ,
1145- no : ( ) => false ,
1146- defaultYes : false
1191+ rejectClose : false ,
1192+ modal : true
11471193 } ) ;
11481194
11491195 if ( confirm ) {
@@ -1254,12 +1300,11 @@ class CustomDrawing extends Drawing {
12541300 e . stopPropagation ( ) ;
12551301 menu . remove ( ) ;
12561302
1257- const confirm = await Dialog . confirm ( {
1258- title : "Delete Note" ,
1303+ const confirm = await foundry . applications . api . DialogV2 . confirm ( {
1304+ window : { title : "Delete Note" } ,
12591305 content : `<p>Are you sure you want to delete this note?</p>` ,
1260- yes : ( ) => true ,
1261- no : ( ) => false ,
1262- defaultYes : false
1306+ rejectClose : false ,
1307+ modal : true
12631308 } ) ;
12641309
12651310 if ( confirm ) {
0 commit comments