in webapp/src/app.tsx [1680:2079]
header: lf("Extensions cannot be used together"),
body: lf("Extensions '{0}' and '{1}' cannot be used together, because they use incompatible settings ({2}).",
confl.pkg1.id, confl.pkg0.id, confl.settingName)
})
}
});
// load side docs
const editorForFile = this.pickEditorFor(file);
const documentation = pkg?.mainPkg?.config?.documentation;
if (documentation)
this.setSideDoc(documentation, editorForFile == this.blocksEditor);
else {
const readme = main.lookupFile("this/README.md");
const readmeContent = readme?.content?.trim();
// no auto-popup when editing packages locally
// ### @autoOpen false
if (!h.githubId && readmeContent && !/#{2,}\s+@autoOpen\s+false\s*/i.test(readmeContent))
this.setSideMarkdown(readme.content);
}
// update recentUse on the header
return workspace.saveAsync(h)
}).then(() => this.loadTutorialFiltersAsync())
.finally(() => {
// Editor is loaded
pxt.BrowserUtils.changeHash("#editor", true);
document.getElementById("root").focus(); // Clear the focus.
cmds.maybeReconnectAsync(false, true);
this.editorLoaded();
})
}
private loadTutorialFiltersAsync(): Promise<void> {
const header = pkg.mainEditorPkg().header;
if (!header || !header.tutorial || !header.tutorial.tutorialMd)
return Promise.resolve();
pxt.perf.measureStart("loadTutorial loadBlockly")
const t = header.tutorial;
if (typeof t.tutorialCode === "string") {
t.tutorialCode = [t.tutorialCode];
}
return this.loadBlocklyAsync()
.then(() => tutorial.getUsedBlocksAsync(t.tutorialCode, t.tutorial, t.language))
.then((tutorialBlocks) => {
let editorState: pxt.editor.EditorState = {
searchBar: false
}
if (tutorialBlocks?.usedBlocks && Object.keys(tutorialBlocks.usedBlocks).length > 0) {
editorState.filters = {
blocks: tutorialBlocks.usedBlocks,
defaultState: pxt.editor.FilterState.Hidden
}
editorState.hasCategories = !(header.tutorial.metadata && header.tutorial.metadata.flyoutOnly);
}
this.setState({ editorState: editorState });
this.editor.filterToolbox(true);
const stepInfo = t.tutorialStepInfo;
const showHint = stepInfo[header.tutorial.tutorialStep].showHint;
if (showHint) this.showTutorialHint();
//else this.showLightbox();
})
.catch(e => {
// Failed to decompile
pxt.tickEvent('tutorial.faileddecompile', { tutorial: t.tutorial });
this.setState({ editorState: { searchBar: false, filters: undefined } });
core.warningNotification(lf("Could not filter tutorial blocks, displaying full toolbox."))
})
.finally(() => {
pxt.perf.measureEnd("loadTutorial loadBlockly")
});
}
private async loadTutorialTemplateCodeAsync(): Promise<void> {
const header = pkg.mainEditorPkg().header;
if (!header || !header.tutorial || !header.tutorial.templateCode)
return;
const template = header.tutorial.templateCode;
// Delete from the header so that we don't overwrite the user code if the tutorial is
// re-opened
delete header.tutorial.templateCode;
let currentText = await workspace.getTextAsync(header.id);
// If we're starting in the asset editor, always load into TS
const preferredEditor = header.tutorial.metadata?.preferredEditor;
if (preferredEditor && filenameForEditor(preferredEditor) === pxt.ASSETS_FILE) {
currentText[pxt.MAIN_TS] = template;
}
const projectname = projectNameForEditor(preferredEditor || header.editor);
if (projectname === pxt.JAVASCRIPT_PROJECT_NAME) {
currentText[pxt.MAIN_TS] = template;
}
else if (projectname === pxt.PYTHON_PROJECT_NAME) {
const pyCode = await compiler.decompilePythonSnippetAsync(template)
if (pyCode) {
currentText[pxt.MAIN_PY] = pyCode;
}
}
else {
const resp = await compiler.decompileBlocksSnippetAsync(template)
const blockXML = resp.outfiles[pxt.MAIN_BLOCKS];
if (blockXML) {
currentText[pxt.MAIN_BLOCKS] = blockXML
}
}
let newText = currentText;
if (header.tutorial.mergeHeaderId) {
const previousHeader = workspace.getHeader(header.tutorial.mergeHeaderId);
if (previousHeader) {
const previousText = await workspace.getTextAsync(previousHeader.id);
newText = mergeProjectCode(previousText, currentText, !!header.tutorial.mergeCarryoverCode);
}
}
for (const file of Object.keys(newText)) {
if (newText[file] !== undefined) {
pkg.mainEditorPkg().setFile(file, newText[file]);
}
}
await workspace.saveAsync(header);
let parsedTilemap: any;
let parsedImage: any;
try {
parsedTilemap = newText[pxt.TILEMAP_JRES] ? JSON.parse(newText[pxt.TILEMAP_JRES]) : null;
parsedImage = newText[pxt.IMAGES_JRES] ? JSON.parse(newText[pxt.IMAGES_JRES]) : null;
}
catch (e) {
return Promise.reject(e);
}
const project = pxt.react.getTilemapProject();
if (parsedTilemap) {
project.loadTilemapJRes(pxt.inflateJRes(parsedTilemap), true);
}
if (parsedImage) {
project.loadAssetsJRes(pxt.inflateJRes(parsedImage));
}
await pkg.mainEditorPkg().buildAssetsAsync();
await pkg.mainEditorPkg().saveFilesAsync();
}
private loadTutorialJresCodeAsync(): Promise<void> {
const header = pkg.mainEditorPkg().header;
if (!header || !header.tutorial || !(header.tutorial.jres || header.tutorial.assetFiles))
return Promise.resolve();
const tilemapJRes = header.tutorial.jres || header.tutorial.assetFiles?.[pxt.TILEMAP_JRES];
const imageJRes = header.tutorial.assetFiles?.[pxt.IMAGES_JRES];
// Delete from the header so that we don't double-load the tiles into
// the project if the project gets reloaded
delete header.tutorial.jres;
delete header.tutorial.assetFiles;
let parsedTilemap: any;
let parsedImage: any;
try {
parsedTilemap = tilemapJRes ? JSON.parse(tilemapJRes) : null;
parsedImage = imageJRes ? JSON.parse(imageJRes) : null;
}
catch (e) {
return Promise.reject(e);
}
const project = pxt.react.getTilemapProject();
if (parsedTilemap) {
project.loadTilemapJRes(pxt.inflateJRes(parsedTilemap), true);
}
if (parsedImage) {
project.loadAssetsJRes(pxt.inflateJRes(parsedImage));
}
return pkg.mainEditorPkg().buildAssetsAsync();
}
private async loadTutorialCustomTsAsync(): Promise<void> {
const mainPkg = pkg.mainEditorPkg();
const header = mainPkg.header;
if (!header || !header.tutorial || !header.tutorial.customTs)
return Promise.resolve();
const customTs = header.tutorial.customTs;
await mainPkg.setContentAsync(pxt.TUTORIAL_CUSTOM_TS, customTs);
await mainPkg.saveFilesAsync();
return Promise.resolve();
}
removeProject() {
if (!pkg.mainEditorPkg().header) return;
core.confirmDelete(pkg.mainEditorPkg().header.name, () => {
let curr = pkg.mainEditorPkg().header
curr.isDeleted = true
return workspace.forceSaveAsync(curr, {})
.then(() => this.openHome());
})
}
///////////////////////////////////////////////////////////
//////////// Import /////////////
///////////////////////////////////////////////////////////
hexFileImporters: pxt.editor.IHexFileImporter[] = [{
id: "default",
canImport: data => data.meta.cloudId == "ks/" + pxt.appTarget.id || data.meta.cloudId == pxt.CLOUD_ID + pxt.appTarget.id // match on targetid
|| (Util.startsWith(data.meta.cloudId, pxt.CLOUD_ID + pxt.appTarget.id)) // trying to load white-label file into main target
,
importAsync: (project, data) => {
let h: pxt.workspace.InstallHeader = {
target: pxt.appTarget.id,
targetVersion: data.meta.targetVersions ? data.meta.targetVersions.target : undefined,
editor: data.meta.editor,
name: data.meta.name,
meta: {},
pubId: "",
pubCurrent: false
};
const files = JSON.parse(data.source) as pxt.Map<string>;
// we cannot load the workspace until we've loaded the project
return workspace.installAsync(h, files)
.then(hd => this.loadHeaderAsync(hd, null));
}
}];
resourceImporters: pxt.editor.IResourceImporter[] = [
new serial.ResourceImporter()
];
isHexFile(filename: string): boolean {
return /\.(hex|uf2)$/i.test(filename)
}
isBlocksFile(filename: string): boolean {
return /\.blocks$/i.test(filename)
}
isTypescriptFile(filename: string): boolean {
return /\.ts$/i.test(filename);
}
isPythonFile(filename: string): boolean {
return /\.py$/i.test(filename);
}
isProjectFile(filename: string): boolean {
return /\.(pxt|mkcd|mkcd-\w+)$/i.test(filename)
}
isPNGFile(filename: string): boolean {
return pxt.appTarget.compile.saveAsPNG && /\.png$/i.test(filename);
}
isAssetFile(filename: string): boolean {
let exts = pxt.appTarget.runtime ? pxt.appTarget.runtime.assetExtensions : null
if (exts) {
let ext = filename.replace(/.*\./, "").toLowerCase()
return exts.indexOf(ext) >= 0
}
return false
}
importProjectCoreAsync(buf: Uint8Array, options?: pxt.editor.ImportFileOptions) {
return (buf[0] == '{'.charCodeAt(0) ?
Promise.resolve(pxt.U.uint8ArrayToString(buf)) :
pxt.lzmaDecompressAsync(buf))
.then(contents => {
let parsedContents = JSON.parse(contents);
if (parsedContents.target && parsedContents.target == pxt.appTarget.id) {
let blockSnippet = parsedContents as pxt.blocks.BlockSnippet;
blockSnippet.xml.forEach(xml => {
let text = pxt.Util.htmlUnescape(xml.replace(/^"|"$/g, ""));
pxt.blocks.loadBlocksXml(this.blocksEditor.editor, text)
})
} else {
let data = parsedContents as pxt.cpp.HexFile;
this.importHex(data, options);
}
}).catch(e => {
core.warningNotification(lf("Sorry, we could not import this project or block snippet."))
this.openHome();
});
}
importHexFile(file: File, options?: pxt.editor.ImportFileOptions) {
if (!file) return;
pxt.cpp.unpackSourceFromHexFileAsync(file)
.then(data => this.importHex(data, options))
.catch(e => {
pxt.reportException(e);
core.warningNotification(lf("Sorry, we could not recognize this file."))
});
}
importBlocksFiles(file: File, options?: pxt.editor.ImportFileOptions) {
if (!file) return;
ts.pxtc.Util.fileReadAsTextAsync(file)
.then(contents => {
this.newProject({
filesOverride: { [pxt.MAIN_BLOCKS]: contents, [pxt.MAIN_TS]: " " },
name: file.name.replace(/\.blocks$/i, '') || lf("Untitled")
})
})
}
importTypescriptFile(file: File, options?: pxt.editor.ImportFileOptions) {
if (!file) return;
ts.pxtc.Util.fileReadAsTextAsync(file)
.then(contents => {
this.newProject({
filesOverride: { [pxt.MAIN_BLOCKS]: '', [pxt.MAIN_TS]: contents || " " },
name: file.name.replace(/\.ts$/i, '') || lf("Untitled")
})
})
}
importProjectFile(file: File, options?: pxt.editor.ImportFileOptions) {
if (!file) return;
ts.pxtc.Util.fileReadAsBufferAsync(file)
.then(buf => this.importProjectCoreAsync(buf, options))
}
importPNGFile(file: File, options?: pxt.editor.ImportFileOptions) {
if (!file) return;
ts.pxtc.Util.fileReadAsBufferAsync(file)
.then(buf => pxt.Util.decodeBlobAsync("data:image/png;base64," +
btoa(pxt.Util.uint8ArrayToString(buf))))
.then(buf => this.importProjectCoreAsync(buf, options))
}
importPNGBuffer(buf: ArrayBuffer) {
pxt.Util.decodeBlobAsync("data:image/png;base64," +
btoa(pxt.Util.uint8ArrayToString(new Uint8Array(buf))))
.then(buf => this.importProjectCoreAsync(buf));
}
importAssetFile(file: File) {
ts.pxtc.Util.fileReadAsBufferAsync(file)
.then(buf => {
let basename = file.name.replace(/.*[\/\\]/, "")
return pkg.mainEditorPkg().saveAssetAsync(basename, buf)
});
}
importHex(data: pxt.cpp.HexFile, options?: pxt.editor.ImportFileOptions) {
if (!data || !data.meta) {
if (data && (data as any)[pxt.CONFIG_NAME]) {
data = cloudsync.reconstructMeta(data as any)
} else {
core.warningNotification(lf("Sorry, we could not recognize this file."))
if (options && options.openHomeIfFailed) this.openHome();
return;
}
}
if (typeof data.source == "object") {
(data as any).source = JSON.stringify(data.source)
}
// intercept newer files early
if (this.hexFileImporters.some(fi => fi.id == "default" && fi.canImport(data))) {
const checkAsync = this.tryCheckTargetVersionAsync(data.meta.targetVersions && data.meta.targetVersions.target);
if (checkAsync) {
checkAsync.then(() => {
if (options && options.openHomeIfFailed) this.newProject();
});
return;
}
}
if (options && options.extension) {
pxt.tickEvent("import.extension");
const files = ts.pxtc.Util.jsonTryParse(data.source);
if (files) {
const n = data.meta.name;
const fn = `${data.meta.name}.json`;
// insert file into package
const mpkg = pkg.mainEditorPkg();
if (mpkg) {
pxt.debug(`adding ${fn} to package`);
// save file
mpkg.setContentAsync(fn, data.source)
.then(() => mpkg.updateConfigAsync(cfg => {