@@ -4,7 +4,7 @@ import queryString from 'query-string';
44import { connect } from 'react-redux' ;
55
66import fetchProjectFile from './fetch-project-file' ;
7- import { loadProjectFileAssets } from './project-file-assets' ;
7+ import { loadProjectFileAssets , getShowBuiltin } from './project-file-assets' ;
88import { setProjectFile , setProjectFileLoading , setProjectFileError } from '../reducers/project-file' ;
99import { setDynamicAssets } from '../reducers/dynamic-assets' ;
1010import { setExternalDeck } from '../reducers/cards' ;
@@ -14,14 +14,15 @@ import {
1414 requestProjectUpload ,
1515 onLoadedProject
1616} from '../reducers/project-state' ;
17+ import pako from 'pako' ;
1718import log from './log' ;
1819
1920const EXTERNAL_DECK_ID = '__external__' ;
2021
2122/**
2223 * Higher Order Component to load external project files via ?project=<value>.
2324 * The value can be:
24- * - A base64-encoded JSON project file
25+ * - A pako-compressed JSON project file (prefixed with "pako:")
2526 * - A URL to a .sb3 file
2627 * - A URL to a JSON project file
2728 *
@@ -68,10 +69,16 @@ const ProjectFileHOC = function (WrappedComponent) {
6869 } else {
6970 this . loadProjectFile ( resolvedValue ) ;
7071 }
71- } else {
72- // Treat as base64-encoded JSON
72+ } else if ( value . startsWith ( 'pako:' ) ) {
73+ // Treat as pako-compressed JSON
74+ ( async ( ) => {
7375 try {
74- const json = decodeURIComponent ( atob ( value ) ) ;
76+ const compressed = atob ( value . slice ( 5 ) ) ;
77+ const bytes = new Uint8Array ( compressed . length ) ;
78+ for ( let i = 0 ; i < compressed . length ; i ++ ) {
79+ bytes [ i ] = compressed . charCodeAt ( i ) ;
80+ }
81+ const json = pako . inflate ( bytes , { to : 'string' } ) ;
7582 const projectFile = JSON . parse ( json ) ;
7683 if ( ! projectFile . title || typeof projectFile . title !== 'string' ) {
7784 throw new Error ( 'Project file must have a "title" string field' ) ;
@@ -93,32 +100,59 @@ const ProjectFileHOC = function (WrappedComponent) {
93100 } ) ;
94101 }
95102 // Resolve asset URLs (sounds, costumes, backdrops)
96- const resolveAssetUrls = assets => {
103+ const resolveAssetUrls = ( assets , resolveUrlFn ) => {
104+ const resolve = resolveUrlFn || resolveUrl ;
97105 if ( ! Array . isArray ( assets ) ) return assets ;
98106 return assets . map ( item => {
99107 const resolved = Object . assign ( { } , item ) ;
100108 if ( resolved . url ) {
101- resolved . url = resolveUrl ( resolved . url ) ;
109+ resolved . url = resolve ( resolved . url ) ;
102110 }
103111 return resolved ;
104112 } ) ;
105113 } ;
114+ const fetchLibrary = async libraryUrl => {
115+ const absLibUrl = resolveUrl ( libraryUrl ) ;
116+ const res = await fetch ( absLibUrl ) ;
117+ if ( ! res . ok ) {
118+ throw new Error ( `Failed to fetch library: ${ res . status } ` ) ;
119+ }
120+ const items = await res . json ( ) ;
121+ const libBase = absLibUrl . substring ( 0 , absLibUrl . lastIndexOf ( '/' ) + 1 ) ;
122+ const resolveLibUrl = relative => {
123+ if ( ! relative ) return null ;
124+ if ( / ^ h t t p s ? : \/ \/ / . test ( relative ) ) return relative ;
125+ return new URL ( relative , libBase ) . href ;
126+ } ;
127+ return resolveAssetUrls ( items , resolveLibUrl ) ;
128+ } ;
129+ const resolveLibrary = async library => {
130+ if ( typeof library === 'string' ) return fetchLibrary ( library ) ;
131+ if ( Array . isArray ( library ) ) {
132+ const parts = await Promise . all ( library . map ( item => {
133+ if ( typeof item === 'string' ) return fetchLibrary ( item ) ;
134+ const resolved = Object . assign ( { } , item ) ;
135+ if ( resolved . url ) resolved . url = resolveUrl ( resolved . url ) ;
136+ return [ resolved ] ;
137+ } ) ) ;
138+ return parts . flat ( ) ;
139+ }
140+ return library ;
141+ } ;
106142 // Normalize asset-type field (flat array or {tags, library, showBuiltin})
107- const normalizeAssetField = field => {
143+ const normalizeAssetField = async field => {
108144 if ( ! field ) return field ;
109145 if ( Array . isArray ( field ) ) return resolveAssetUrls ( field ) ;
110146 if ( typeof field === 'object' ) {
111147 const normalized = Object . assign ( { } , field ) ;
112- if ( Array . isArray ( normalized . library ) ) {
113- normalized . library = resolveAssetUrls ( normalized . library ) ;
114- }
148+ normalized . library = await resolveLibrary ( normalized . library ) ;
115149 return normalized ;
116150 }
117151 return field ;
118152 } ;
119- projectFile . sounds = normalizeAssetField ( projectFile . sounds ) ;
120- projectFile . costumes = normalizeAssetField ( projectFile . costumes ) ;
121- projectFile . backdrops = normalizeAssetField ( projectFile . backdrops ) ;
153+ projectFile . sounds = await normalizeAssetField ( projectFile . sounds ) ;
154+ projectFile . costumes = await normalizeAssetField ( projectFile . costumes ) ;
155+ projectFile . backdrops = await normalizeAssetField ( projectFile . backdrops ) ;
122156 if ( Array . isArray ( projectFile . sprites ) ) {
123157 projectFile . sprites = projectFile . sprites . map ( sprite => {
124158 const resolved = Object . assign ( { } , sprite ) ;
@@ -127,15 +161,42 @@ const ProjectFileHOC = function (WrappedComponent) {
127161 return resolved ;
128162 } ) ;
129163 } else if ( projectFile . sprites && typeof projectFile . sprites === 'object' ) {
130- const normalized = Object . assign ( { } , projectFile . sprites ) ;
131- if ( Array . isArray ( normalized . library ) ) {
132- normalized . library = normalized . library . map ( sprite => {
133- const resolved = Object . assign ( { } , sprite ) ;
134- resolved . costumes = resolveAssetUrls ( resolved . costumes ) ;
135- resolved . sounds = resolveAssetUrls ( resolved . sounds ) ;
136- return resolved ;
164+ const fetchSpriteLibrary = async libraryUrl => {
165+ const absLibUrl = resolveUrl ( libraryUrl ) ;
166+ const res = await fetch ( absLibUrl ) ;
167+ if ( ! res . ok ) {
168+ throw new Error ( `Failed to fetch sprite library: ${ res . status } ` ) ;
169+ }
170+ const items = await res . json ( ) ;
171+ const libBase = absLibUrl . substring ( 0 , absLibUrl . lastIndexOf ( '/' ) + 1 ) ;
172+ const resolveLibUrl = relative => {
173+ if ( ! relative ) return null ;
174+ if ( / ^ h t t p s ? : \/ \/ / . test ( relative ) ) return relative ;
175+ return new URL ( relative , libBase ) . href ;
176+ } ;
177+ return items . map ( sprite => {
178+ const s = Object . assign ( { } , sprite ) ;
179+ s . costumes = resolveAssetUrls ( s . costumes , resolveLibUrl ) ;
180+ s . sounds = resolveAssetUrls ( s . sounds , resolveLibUrl ) ;
181+ return s ;
137182 } ) ;
138- }
183+ } ;
184+ const resolveSpriteLibrary = async library => {
185+ if ( typeof library === 'string' ) return fetchSpriteLibrary ( library ) ;
186+ if ( Array . isArray ( library ) ) {
187+ const parts = await Promise . all ( library . map ( item => {
188+ if ( typeof item === 'string' ) return fetchSpriteLibrary ( item ) ;
189+ const s = Object . assign ( { } , item ) ;
190+ s . costumes = resolveAssetUrls ( s . costumes ) ;
191+ s . sounds = resolveAssetUrls ( s . sounds ) ;
192+ return [ s ] ;
193+ } ) ) ;
194+ return parts . flat ( ) ;
195+ }
196+ return library ;
197+ } ;
198+ const normalized = Object . assign ( { } , projectFile . sprites ) ;
199+ normalized . library = await resolveSpriteLibrary ( normalized . library ) ;
139200 projectFile . sprites = normalized ;
140201 }
141202 this . props . onSetProjectFile ( projectFile ) ;
@@ -151,6 +212,7 @@ const ProjectFileHOC = function (WrappedComponent) {
151212 this . props . onSetProjectFileError ( error . message ) ;
152213 this . setState ( { isLoadingProjectFromUrl : false } ) ;
153214 }
215+ } ) ( ) ;
154216 }
155217 }
156218 }
@@ -169,6 +231,7 @@ const ProjectFileHOC = function (WrappedComponent) {
169231 if ( ! field ) return false ;
170232 if ( Array . isArray ( field ) ) return field . length > 0 ;
171233 if ( typeof field === 'object' ) {
234+ if ( field . showBuiltin === false ) return true ;
172235 return ( field . library && field . library . length > 0 ) ||
173236 ( field . tags && field . tags . length > 0 ) ;
174237 }
@@ -192,6 +255,21 @@ const ProjectFileHOC = function (WrappedComponent) {
192255 } )
193256 . catch ( error => {
194257 log . error ( 'Failed to load project file assets:' , error ) ;
258+ // Still propagate showBuiltin flags even if asset fetches failed
259+ this . props . onSetDynamicAssets ( {
260+ costumes : [ ] ,
261+ sounds : [ ] ,
262+ backdrops : [ ] ,
263+ sprites : [ ] ,
264+ costumeTags : null ,
265+ soundTags : null ,
266+ backdropTags : null ,
267+ spriteTags : null ,
268+ showBuiltinCostumes : getShowBuiltin ( projectFile . costumes ) ,
269+ showBuiltinSounds : getShowBuiltin ( projectFile . sounds ) ,
270+ showBuiltinBackdrops : getShowBuiltin ( projectFile . backdrops ) ,
271+ showBuiltinSprites : getShowBuiltin ( projectFile . sprites )
272+ } ) ;
195273 } ) ;
196274 }
197275 setupDeck ( projectFile ) {
@@ -248,6 +326,10 @@ const ProjectFileHOC = function (WrappedComponent) {
248326 } )
249327 . catch ( error => {
250328 log . error ( 'Failed to load sb3:' , error ) ;
329+ // Still load assets (costumes, sprites, etc.) even if sb3 failed
330+ if ( projectFile ) {
331+ this . loadAssets ( projectFile ) ;
332+ }
251333 this . setState ( { isLoadingProjectFromUrl : false } ) ;
252334 } ) ;
253335 }
0 commit comments