@@ -432,15 +432,47 @@ export async function syncCurrentCostumeData(isFinalSync = false) {
432432}
433433
434434/**
435- * Generates a SHA-256 hash of a given ArrayBuffer.
435+ * Generates a SHA-256 hash of a given ArrayBuffer or ArrayBufferView .
436436 * This is used to detect changes in binary asset data (costumes, sounds).
437- * @param {ArrayBuffer } messageBuffer - The binary data to hash.
437+ * @param {ArrayBuffer|ArrayBufferView|Uint8Array } messageBuffer - The binary data to hash.
438438 * @returns {Promise<string|null> } A promise that resolves to the SHA-256 hash as a hex string, or null on error.
439439 */
440440async function digestMessage ( messageBuffer ) {
441- if ( ! messageBuffer || messageBuffer . byteLength === 0 ) return 'empty' ; // Handle empty data case.
441+ if ( ! messageBuffer ) return 'empty' ; // Handle null/undefined case.
442+
443+ // Convert to ArrayBuffer if it's an ArrayBufferView (like Uint8Array)
444+ let arrayBufferToHash ;
445+ if ( messageBuffer instanceof ArrayBuffer ) {
446+ arrayBufferToHash = messageBuffer ;
447+ } else if ( messageBuffer . buffer && messageBuffer . buffer instanceof ArrayBuffer ) {
448+ // Handle ArrayBufferView types (Uint8Array, etc.)
449+ // If it's a view of the entire buffer, use the buffer directly
450+ // Otherwise, create a new view with just the relevant portion
451+ if ( messageBuffer . byteOffset === 0 && messageBuffer . byteLength === messageBuffer . buffer . byteLength ) {
452+ arrayBufferToHash = messageBuffer . buffer ;
453+ } else {
454+ // Create a new ArrayBuffer with just the view's data
455+ arrayBufferToHash = messageBuffer . buffer . slice ( messageBuffer . byteOffset , messageBuffer . byteOffset + messageBuffer . byteLength ) ;
456+ }
457+ } else if ( typeof messageBuffer === 'string' ) {
458+ // Handle string data (base64 encoded or data URL)
459+ console . error ( 'Collab: digestMessage received string data instead of binary. This should not happen.' ) ;
460+ return null ;
461+ } else {
462+ // Unknown type - try to check if it has byteLength
463+ if ( typeof messageBuffer . byteLength === 'number' ) {
464+ console . error ( 'Collab: digestMessage received unexpected type with byteLength:' , typeof messageBuffer , messageBuffer ) ;
465+ return null ;
466+ } else {
467+ console . error ( 'Collab: digestMessage received invalid data type:' , typeof messageBuffer , messageBuffer ) ;
468+ return null ;
469+ }
470+ }
471+
472+ if ( arrayBufferToHash . byteLength === 0 ) return 'empty' ; // Handle empty data case.
473+
442474 try {
443- const hashBuffer = await crypto . subtle . digest ( 'SHA-256' , messageBuffer ) ; // Use Web Cryptography API.
475+ const hashBuffer = await crypto . subtle . digest ( 'SHA-256' , arrayBufferToHash ) ; // Use Web Cryptography API.
444476 const hashArray = Array . from ( new Uint8Array ( hashBuffer ) ) ; // Convert ArrayBuffer to Array of bytes.
445477 // Convert bytes to hex string.
446478 return hashArray . map ( b => b . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ;
@@ -598,7 +630,16 @@ export async function updateSoundProgrammatically(target, soundIndex, soundAsset
598630 if ( constants . debugging ) console . log ( `Collab RX [soundEdited]: AudioBuffer decoded. Calling vm.updateSoundBuffer for sound ${ soundIndex } on target "${ target . getName ( ) } ".` ) ;
599631
600632 // Call the VM method to update the sound's asset, data format, sample rate, and other metadata.
601- constants . mutableRefs . vm . updateSoundBuffer ( soundIndex , audioBuffer , dataFormat , target ) ;
633+ // IMPORTANT: Pass the Uint8Array data as the third parameter to preserve the raw asset data
634+ // for project save/load. The signature matches sound-editor.jsx usage.
635+ // This ensures the sound asset data is properly saved and can be reloaded correctly.
636+ constants . mutableRefs . vm . updateSoundBuffer ( soundIndex , audioBuffer , uint8ArrayData ) ;
637+
638+ // Update the sound's dataFormat if it differs (this ensures metadata is correct)
639+ if ( soundToUpdate . asset && soundToUpdate . asset . dataFormat !== dataFormat ) {
640+ soundToUpdate . asset . dataFormat = dataFormat ;
641+ soundToUpdate . dataFormat = dataFormat ;
642+ }
602643
603644 // If the updated sound belongs to the currently editing target, trigger UI refreshes.
604645 if ( target . id === constants . mutableRefs . vm . runtime . getEditingTarget ( ) ?. id ) {
@@ -611,7 +652,7 @@ export async function updateSoundProgrammatically(target, soundIndex, soundAsset
611652 if ( soundEditor && constants . mutableRefs . vm . editingTarget && constants . mutableRefs . vm . editingTarget . id === target . id ) {
612653 const redux = window . ReduxStore ; // Assuming Redux store is globally accessible.
613654 // Check if the sound editor is open and the currently edited sound matches the one being updated.
614- if ( redux && redux . getState ( ) . scratchGui . soundEditor . soundIndex === soundIndex ) {
655+ if ( redux && redux . getState ( ) . scratchGui . soundEditor && redux . getState ( ) . scratchGui . soundEditor . soundIndex === soundIndex ) {
615656 if ( constants . debugging ) console . log ( `Collab RX [soundEdited]: Attempting to refresh sound editor UI for target ${ target . getName ( ) } , sound ${ soundIndex } ` ) ;
616657 // Currently, a direct Redux dispatch to force re-render might be needed,
617658 // or rely on a more granular VM event if Scratch GUI supports it.
0 commit comments