PlayCanvas Github Integration

Import files from Github repository to PlayCanvas Editor

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

You will need to install an extension such as Tampermonkey to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         PlayCanvas Github Integration
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Import files from Github repository to PlayCanvas Editor
// @author       BrandLab360
// @match        https://playcanvas.com/editor/*
// @grant        GM_xmlhttpRequest
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

    function initPanel() {
    // Ensure pcui and the PlayCanvas Editor API are available
    if (typeof pcui === 'undefined' || typeof editor === 'undefined') {
      console.error('PlayCanvas Editor API or pcui is not available');
      return;
    }

    // Create the UI panel
    const panel = new pcui.Panel({
      headerText: 'Github Integration',
      collapsible: true,
      collapsed: true
    });
    panel.style.position = 'absolute';
    panel.style.bottom = '10px';
    panel.style.right = '10px';
    panel.style.width = '300px';
    editor.call('layout.viewport').append(panel);

    // Create the input field for the GitHub personal access token
    const tokenField = new pcui.TextInput({
      placeholder: 'Enter GitHub Access Token',
      width: '95%'
    });
    panel.append(tokenField);

    // Create the dropdown input
    const dropdown = new pcui.SelectInput({
      options: [
        { v: 'https://github.com/lukesmith1024/PlaycanvasTest', t: 'PlaycanvasTest' }
      ]
    });

    // Add the dropdown input to the panel
    panel.append(dropdown);

    // Create the input field for the GitHub repository URL
    const repoField = new pcui.TextInput({
      placeholder: 'GitHub Repository URL',
    });
    panel.append(repoField);

    // Add an event listener for when the dropdown value changes
    dropdown.on('change', value => {
      repoField.value = value;
    });

    // Create the button for importing files
    const importButton = new pcui.Button({
      text: 'Import from Github',
    });
    panel.append(importButton);

    // Function to recursively import folders and files from the GitHub repository
    async function importFromGithub(token, repoUrl) {
      const apiUrl = 'https://api.github.com';

      // Extract the owner and repository name from the URL
      const [, , , owner, repo] = repoUrl.split('/');

          // Check if a folder with the name of the GitHub repository already exists
      const existingFolder = editor.call('assets:findOne', (asset) => {
        return asset.get('name') === repo && asset.get('type') === 'folder';
      });

      if (existingFolder) {
        var errorMsg = `A folder with the name '${repo}' already exists. Import aborted.`;
        console.error(errorMsg);
        alert(errorMsg);
        return; // Stop importing if the folder already exists
      }

      // Create a root folder with the name of the GitHub repository
      const rootFolderAsset = await new Promise((resolve) => {
        editor.on('assets:add', (asset) => {
          if (asset.get('name') === repo && asset.get('type') === 'folder') {
            resolve(asset);
          }
        });

        editor.call('assets:create', { name: repo, type: 'folder' }, (err) => {
          if (err) {
            console.error(`Error creating root folder asset: ${err}`);
          }
        });
      });

      const rootFolderId = rootFolderAsset.get('id');
      console.log(`Created root folder '${repo}' with ID '${rootFolderId}'`);


      async function getContents(path = '') {
         console.log(`getContents called for path: '${path}'`);
        const response = await fetch(`${apiUrl}/repos/${owner}/${repo}/contents/${path}`, {
          headers: { Authorization: `token ${token}` },
        });

        if (!response.ok) {
          throw new Error(`Error fetching contents from repository: ${response.statusText}`);
        }

        const contents = await response.json();
        console.log(`Fetched contents of '${path}':`, contents);
        return contents;
      }


      function isPlayCanvasMaterial(obj) {
        if (typeof obj !== 'object' || obj === null) {
          return false;
        }

        // Check if the object has specific properties that are unique to PlayCanvas materials
        const materialProperties = [
          'ambient',
          'diffuse',
          'specular',
          'shininess',
          'opacity',
        ];

        return materialProperties.every((property) => obj.hasOwnProperty(property));
      }


      function applyMaterialProperties(material, materialData) {
        for (const key in materialData) {
          if (materialData.hasOwnProperty(key) && material.hasOwnProperty(key)) {
            material[key] = materialData[key];
          }
        }
        material.update();
      }

      async function uploadFile(file) {
        console.log(`Uploading file '${file.name}' with type '${file.type}'`);

        console.log('File object:', file);
        console.log('Download URL:', file.download_url);

        const downloadUrl = file.download_url;

        return new Promise((resolve, reject) => {
          GM_xmlhttpRequest({
            method: 'GET',
            url: downloadUrl,
            responseType: 'arraybuffer',
            headers: {
              'Authorization': `token ${token}`
            },
            onload: async (response) => {
              if (response.status !== 200) {
                reject(new Error(`Error fetching file content: ${response.statusText}`));
                return;
              }

              let fileContent;
               if (file.type === 'application/json') {
                const decoder = new TextDecoder('utf-8');
                const arrayBuffer = new Uint8Array(response.response).buffer;
                const responseText = decoder.decode(arrayBuffer);

                let parsedJson;
                try {
                  parsedJson = JSON.parse(responseText);
                } catch (error) {
                  console.error(`Error parsing JSON for '${file.name}', importing as plain text:`, error);
                  fileContent = new Blob([responseText], { type: 'text/plain' });
                }

               console.log("Parsed JSON:", parsedJson);

               if (isPlayCanvasMaterial(parsedJson)) {
                  file.isMaterial = true;
                  file.materialData = parsedJson;
                  console.log(file.name + " is a material");
                  fileContent = new Blob([responseText], { type: 'text/plain' });
                } else {
                  fileContent = new Blob([JSON.stringify(parsedJson)], { type: 'application/json' });
                }


              } else if (file.isMaterial) {
                const decoder = new TextDecoder('utf-8');
                const arrayBuffer = new Uint8Array(response.response).buffer;
                const responseText = decoder.decode(arrayBuffer);
                fileContent = new Blob([responseText], { type: 'text/plain' });
              } else {
                fileContent = new Blob([response.response], { type: file.type });
              }


              console.log(`File content for '${file.name}':`, fileContent);

              // Create a File instance
              const fileInstance = new File([fileContent], file.name, { type: file.type });

              // Call the createAssetInPlayCanvas function
              const parentFolder = file.parent ? editor.call('assets:get', file.parent) : editor.call('assets:panel:getHierarchy');
              const fileType = getFileTypeFromExtension(file.name);
              await createAssetInPlayCanvas(file.name, fileInstance, file.type, fileType, parentFolder, file.isMaterial, file.materialData);


              resolve();
            },

            onerror: (error) => {
              reject(new Error(`Error fetching file content: ${error}`));
            }
          });
        });
      }

      async function createAssetInPlayCanvas(fileName, file, mimeType, assetType, parentFolder, isMaterial, materialData) {
        let asset;

        isMaterial = isMaterial || false;
        console.log("Is material:", isMaterial);

        if (isMaterial === true) {
          assetType = 'material';
          const materialName = fileName.replace('.json', '');

          console.log("Material name:", materialName);
          console.log("Parent folder ID:", parentFolder.get('id'));
          console.log("Material data:", materialData);

          // Create material asset
          asset = await new Promise((resolve, reject) => {
            const assetAddedHandler = function (addedAsset) {
              if (addedAsset.get('type') === 'material') {
                console.log('Material asset created:', addedAsset);
                editor.off('assets:add', assetAddedHandler);
                resolve(addedAsset);
              }
            };

            editor.on('assets:add', assetAddedHandler);

            console.log("Creating material asset with data:", { name: materialName, type: assetType, parent: parentFolder.get('id'), data: materialData });

            editor.call('assets:create', {
              name: materialName,
              type: assetType,
              parent: parentFolder.get('id'),
              data: materialData,
            }, (materialAsset) => {
              if (!materialAsset) {
                editor.off('assets:add', assetAddedHandler);
                reject('Failed to create the material asset.');
              }
            });
          });

          if (asset) {
            console.log("Material asset created:", asset);

            // Save changes to the material asset
            asset.save();
            console.log("Created material asset:", asset);
          } else {
            console.error('Failed to create the material asset.');
          }

        } else {
          const assetData = {
            name: fileName,
            type: assetType,
            file: file,
            parent: parentFolder.get('id')
          };

          console.log('Asset data:', assetData);

          // Create the asset using 'assets:create' event
          asset = await new Promise((resolve, reject) => {
            editor.call('assets:create', assetData, (err, createdAsset) => {
              if (err) {
                console.error(`Error creating asset: ${err}`);
                reject(err);
              } else {
                resolve(createdAsset);
              }
            });
          });

          console.log('Asset:', asset);

          // Add the asset to the assets registry and the asset panel
          if (asset) {
            console.log('Uploading file for asset:', asset);
            editor.call('assets:panel:files:upload', asset, file);
          } else {
            console.error('Failed to create the asset.');
          }
        }
      }

      function getMimeTypeFromExtension(filename) {
        const ext = filename.split('.').pop().toLowerCase();
        const mimeTypes = {
          'png': 'image/png',
          'jpg': 'image/jpeg',
          'jpeg': 'image/jpeg',
          'gif': 'image/gif',
          'json': 'application/json',
          'txt': 'text/plain',
          'csv': 'text/csv',
          'html': 'text/html',
          'css': 'text/css',
          'js': 'application/javascript',
          'fbx': 'model/vnd.fbx',
          'obj': 'model/obj',
          'mp3': 'audio/mpeg',
          'wav': 'audio/wav',
          'ogg': 'audio/ogg',
          'mp4': 'video/mp4',
          'webm': 'video/webm',
          'hdr': 'image/vnd.radiance',
        };

        return mimeTypes[ext] || 'application/octet-stream';
      }

      function getFileTypeFromExtension(filename) {
        const ext = filename.split('.').pop().toLowerCase();
        const fileTypes = {
          'png': 'texture',
          'jpg': 'texture',
          'jpeg': 'texture',
          'gif': 'texture',
          'json': 'json',
          'txt': 'text',
          'csv': 'text',
          'html': 'text',
          'css': 'text',
          'js': 'script',
          'fbx': 'model',
          'obj': 'model',
          'mp3': 'audio',
          'wav': 'audio',
          'ogg': 'audio',
          'mp4': 'video',
          'webm': 'video',
          'hdr': 'texture',
        };

        return fileTypes[ext] || 'unknown';
      }

      async function processFolder(contents, parentFolderId = null) {
        console.log(`Contents of '${parentFolderId || ''}':`, contents);

        // Wait for all folder creations and file uploads to complete
        await Promise.all(contents.map(async (item) => {
          if (item.type === 'dir') {
            console.log(`Processing folder: '${item.name}'`);
            const folderData = {
              name: item.name,
              type: 'folder',
              parent: parentFolderId
            };

            const folderAsset = await new Promise((resolve) => {
              editor.on('assets:add', (asset) => {
                if (asset.get('name') === folderData.name && asset.get('type') === folderData.type) {
                  resolve(asset);
                }
              });

              editor.call('assets:create', folderData, (err) => {
                if (err) {
                  console.error(`Error creating folder asset: ${err}`);
                }
              });
            });

            const folderId = folderAsset.get('id');
            console.log(`Created folder '${item.name}' with ID '${folderId}'`);

            const subContents = await getContents(item.path);
            await processFolder(subContents, folderId);
          } else {
            console.log(`Processing file: '${item.name}'`);
            const file = {
              name: item.name,
              path: item.path,
              type: getMimeTypeFromExtension(item.path),
              download_url: item.download_url,
              parent: parentFolderId,
            };
            await uploadFile(file);
          }
        }));
      }

      try {
        const rootContents = await getContents();
        await processFolder(rootContents, rootFolderId, token);

      } catch (err) {
        console.error(`Error importing from Github: ${err.message}`);
      }
    }

     // Handle the click event on the import button
    importButton.on('click', () => {
      const token = tokenField.value;
      const repoUrl = repoField.value;

        if (!token || !repoUrl) {
          console.error('Please provide a GitHub personal access token and a repository URL');
          return;
        }

      importFromGithub(token, repoUrl);
    });
  }

  // Initialize the panel after a delay to make sure the editor is fully loaded
  setTimeout(() => {
  initPanel();
  }, 5000);

    // Load scripts in Editor folder as extentions
  const importExtensionsFromAssets = () => {
    const extentionsFolder = editor.assets.list().find((data) => {
      const { type, name, path } = data.json();
      return type === "folder" && name === "Editor";
    });

    if (!extentionsFolder) {
      console.warn("Create an Editor folder in the assets.");
      return;
    }

    const extentions = editor.assets.list().filter((data) => {
      const { path, type } = data.json();
      return type === "script";
    });

    extentions.forEach((data) => {
      const { url } = data.json().file;
      var script = document.createElement("script");
      script.src = url;
      console.log(url)
      document.head.appendChild(script);
    });
  };

  editor.on('assets:load', () => importExtensionsFromAssets());

})();