diff --git a/apps/remix-ide/src/app/components/preload.tsx b/apps/remix-ide/src/app/components/preload.tsx new file mode 100644 index 0000000000..0e17efa214 --- /dev/null +++ b/apps/remix-ide/src/app/components/preload.tsx @@ -0,0 +1,115 @@ +import { RemixApp } from '@remix-ui/app' +import React, { useEffect, useRef, useState } from 'react' +import { render } from 'react-dom' +import * as packageJson from '../../../../../package.json' +import { fileSystems } from '../files/fileSystem' +import { indexedDBFileSystem } from '../files/filesystems/indexedDB' +import { localStorageFS } from '../files/filesystems/localStorage' +import { fileSystemUtility } from '../files/filesystems/migrateFileSystem' + +export const Preload = () => { + + const [supported, setSupported] = useState(true) + const [error, setError] = useState(false) + const [showDownloader, setShowDownloader] = useState(false) + const remixFileSystems = useRef(new fileSystems()) + + const logo = + + + + + + function loadAppComponent() { + import('../../app').then((AppComponent) => { + const appComponent = new AppComponent.default() + appComponent.run().then(() => { + render( + <> + + , + document.getElementById('root') + ) + }) + }).catch(err => { + console.log('Error loading Remix:', err) + setError(true) + }) + } + + + + const downloadBackup = async () =>{ + const migrator = new fileSystemUtility() + migrator.downloadBackup(remixFileSystems.current.fileSystems['localstorage']) + loadAppComponent() + } + + const skipBackup = async() => { + loadAppComponent() + } + + useEffect(() => { + + + async function loadStorage() { + const remixIndexedDB = new indexedDBFileSystem() + const localStorageFileSystem = new localStorageFS() + + + await remixFileSystems.current.addFileSystem(remixIndexedDB) + await remixFileSystems.current.addFileSystem(localStorageFileSystem) + + setShowDownloader(true) + + const migrator = new fileSystemUtility() + await migrator.migrate(localStorageFileSystem, remixIndexedDB) + const fsLoaded = await remixFileSystems.current.setFileSystem([remixIndexedDB, localStorageFileSystem]) + + if (fsLoaded) { + console.log(fsLoaded.name + ' activated') + console.log(localStorageFileSystem) + // migrator.downloadBackup(localStorageFileSystem) + //loadAppComponent() + } else { + console.log('No filesystem could be loaded') + setSupported(false) + // displayBrowserNotSupported() + } + } + + loadStorage() + }, []) + + return <> +
+ {logo} +
+ REMIX IDE +
+ v{packageJson.version} +
+ {!supported ? +
+ Your browser does not support any of the filesytems required by Remix. + Either change the settings in your browser or use a supported browser. +
: null} + {error ? +
+ An unknown error has occured loading the application. +
: null} + {showDownloader ? +
+ This app will be updated now. Please download a backup of your files now to make sure you don't lose your work. +

+ You don't need to do anything else, your files will be available when the app loads. +
{await downloadBackup()}} className='btn btn-primary mt-1'>download backup
+
{await skipBackup()}}className='btn btn-primary mt-1'>skip backup
+
: null} + {(supported && !error && !showDownloader) ? +
+ +
: null} +
+ +} \ No newline at end of file diff --git a/apps/remix-ide/src/app/files/fileSystem.ts b/apps/remix-ide/src/app/files/fileSystem.ts new file mode 100644 index 0000000000..8e37260de1 --- /dev/null +++ b/apps/remix-ide/src/app/files/fileSystem.ts @@ -0,0 +1,85 @@ +export class fileSystem { + name: string + enabled: boolean + available: boolean + fs: any + fsCallBack: any; + hasWorkSpaces: boolean + loaded: boolean + load: () => Promise + test: () => Promise + + constructor() { + this.available = false + this.enabled = false + this.hasWorkSpaces = false + this.loaded = false + } + ReadWriteTest = async () => { + try { + const str = 'Hello World' + await this.fs.writeFile('/test.txt', str, 'utf8') + if (await this.fs.readFile('/test.txt', 'utf8') === str) { + console.log('Read/Write Test Passed') + return true + } + await this.fs.remove('/test.txt', 'utf8') + } catch (e) { + console.log(e) + } + return false + } + + checkWorkspaces = async () => { + try { + await this.fs.stat('.workspaces') + this.hasWorkSpaces = true + } catch (e) { + + } + } + + set = async () => { + const w = (window as any) + if (!this.loaded) return false + w.remixFileSystem = this.fs + w.remixFileSystemCallback = this.fsCallBack + return true + } +} + +export class fileSystems { + fileSystems: Record + constructor() { + this.fileSystems = {} + } + + addFileSystem = async (fs: fileSystem) => { + try { + this.fileSystems[fs.name] = fs + if (await fs.test()) await fs.load() + console.log(fs.name + ' is loaded...') + return true + } catch (e) { + console.log(fs.name + ' not available...') + return false + } + } + /** + * sets filesystem using list as fallback + * @param {string[]} names + * @returns {Promise} + */ + setFileSystem = async (filesystems?: fileSystem[]): Promise => { + for (const fs of filesystems) { + if (this.fileSystems[fs.name]) { + const result = await this.fileSystems[fs.name].set() + if (result) return this.fileSystems[fs.name] + } + } + return null + } + + +} + diff --git a/apps/remix-ide/src/app/files/filesystems/indexedDB.ts b/apps/remix-ide/src/app/files/filesystems/indexedDB.ts new file mode 100644 index 0000000000..b62032cf25 --- /dev/null +++ b/apps/remix-ide/src/app/files/filesystems/indexedDB.ts @@ -0,0 +1,91 @@ +import LightningFS from "@isomorphic-git/lightning-fs" +import { fileSystem } from "../fileSystem" + +export class IndexedDBStorage extends LightningFS { + base: LightningFS.PromisifedFS + addSlash: (file: string) => string + extended: { exists: (path: string) => Promise; rmdir: (path: any) => Promise; readdir: (path: any) => Promise; unlink: (path: any) => Promise; mkdir: (path: any) => Promise; readFile: (path: any, options: any) => Promise; rename: (from: any, to: any) => Promise; writeFile: (path: any, content: any, options: any) => Promise; stat: (path: any) => Promise; init(name: string, opt?: LightningFS.FSConstructorOptions): void; activate(): Promise; deactivate(): Promise; lstat(filePath: string): Promise; readlink(filePath: string): Promise; symlink(target: string, filePath: string): Promise } + constructor(name: string) { + super(name) + this.addSlash = (file) => { + if (!file.startsWith('/')) file = '/' + file + return file + } + this.base = this.promises + this.extended = { + ...this.promises, + exists: async (path: string) => { + return new Promise((resolve) => { + this.base.stat(this.addSlash(path)).then(() => resolve(true)).catch(() => resolve(false)) + }) + }, + rmdir: async (path) => { + return this.base.rmdir(this.addSlash(path)) + }, + readdir: async (path) => { + return this.base.readdir(this.addSlash(path)) + }, + unlink: async (path) => { + return this.base.unlink(this.addSlash(path)) + }, + mkdir: async (path) => { + return this.base.mkdir(this.addSlash(path)) + }, + readFile: async (path, options) => { + return this.base.readFile(this.addSlash(path), options) + }, + rename: async (from, to) => { + return this.base.rename(this.addSlash(from), this.addSlash(to)) + }, + writeFile: async (path, content, options) => { + return this.base.writeFile(this.addSlash(path), content, options) + }, + stat: async (path) => { + return this.base.stat(this.addSlash(path)) + } + } + } +} + + +export class indexedDBFileSystem extends fileSystem { + constructor() { + super() + this.name = 'indexedDB' + } + + load = async () => { + return new Promise((resolve, reject) => { + try { + const fs = new IndexedDBStorage('RemixFileSystem') + fs.init('RemixFileSystem') + this.fs = fs.extended + this.fsCallBack = fs + this.loaded = true + resolve(true) + } catch (e) { + reject(e) + } + }) + } + + test = async () => { + return new Promise((resolve, reject) => { + if (!window.indexedDB) { + this.available = false + reject('No indexedDB on window') + } + const request = window.indexedDB.open("RemixTestDataBase", 3); + request.onerror = () => { + this.available = false + reject('Error creating test database') + }; + request.onsuccess = () => { + window.indexedDB.deleteDatabase("RemixTestDataBase"); + this.available = true + resolve(true) + }; + }) + } + +} \ No newline at end of file diff --git a/apps/remix-ide/src/app/files/filesystems/localStorage.ts b/apps/remix-ide/src/app/files/filesystems/localStorage.ts new file mode 100644 index 0000000000..dc757bd7f2 --- /dev/null +++ b/apps/remix-ide/src/app/files/filesystems/localStorage.ts @@ -0,0 +1,58 @@ +import { fileSystem } from "../fileSystem"; + +export class localStorageFS extends fileSystem { + + constructor() { + super() + this.name = 'localstorage' + } + load = async () => { + const me = this + return new Promise((resolve, reject) => { + try { + // eslint-disable-next-line no-undef + BrowserFS.install(window) + // eslint-disable-next-line no-undef + BrowserFS.configure({ + fs: 'LocalStorage' + }, async function (e) { + if (e) { + console.log('BROWSEFS Error: ' + e) + reject(e) + } else { + me.fs = { ...window.require('fs') } + me.fsCallBack = window.require('fs') + me.fs.readdir = me.fs.readdirSync + me.fs.readFile = me.fs.readFileSync + me.fs.writeFile = me.fs.writeFileSync + me.fs.stat = me.fs.statSync + me.fs.unlink = me.fs.unlinkSync + me.fs.rmdir = me.fs.rmdirSync + me.fs.mkdir = me.fs.mkdirSync + me.fs.rename = me.fs.renameSync + me.fs.exists = me.fs.existsSync + me.loaded = true + resolve(true) + } + }) + } catch (e) { + console.log('BrowserFS is not ready!') + reject(e) + } + }) + } + + test = async () => { + return new Promise((resolve, reject) => { + const test = 'test'; + try { + localStorage.setItem(test, test); + localStorage.removeItem(test); + resolve(true) + } catch(e) { + reject(e) + } + }) + } + +} \ No newline at end of file diff --git a/apps/remix-ide/src/app/files/filesystems/migrateFileSystem.ts b/apps/remix-ide/src/app/files/filesystems/migrateFileSystem.ts new file mode 100644 index 0000000000..9e5e43b80a --- /dev/null +++ b/apps/remix-ide/src/app/files/filesystems/migrateFileSystem.ts @@ -0,0 +1,189 @@ +import { hashMessage } from "ethers/lib/utils" +import JSZip from "jszip" +import { fileSystem } from "../fileSystem" + +export class fileSystemUtility { + migrate = async (fsFrom: fileSystem, fsTo: fileSystem) => { + try { + await fsFrom.checkWorkspaces() + await fsTo.checkWorkspaces() + + if (fsTo.hasWorkSpaces) { + console.log(`${fsTo.name} already has files`) + return true + } + + if (!fsFrom.hasWorkSpaces) { + console.log('no files to migrate') + return true + } + + await this.populateWorkspace(testData, fsFrom.fs) + const fromFiles = await this.copyFolderToJson('/', null, null, fsFrom.fs) + console.log(fsFrom.name, hashMessage(JSON.stringify(fromFiles)), fromFiles) + await this.populateWorkspace(fromFiles, fsTo.fs) + const toFiles = await this.copyFolderToJson('/', null, null, fsTo.fs) + + if (hashMessage(JSON.stringify(toFiles)) === hashMessage(JSON.stringify(fromFiles))) { + console.log('file migration successful') + return true + } else { + console.log('file migration failed falling back to ' + fsFrom.name) + fsTo.loaded = false + return false + } + } catch (e) { + console.log(e) + console.log('file migration failed falling back to ' + fsFrom.name) + fsTo.loaded = false + return false + } + } + + downloadBackup = async (fs: fileSystem) => { + try { + const zip = new JSZip() + await fs.checkWorkspaces() + await this.copyFolderToJson('/', null, null, fs.fs, ({ path, content }) => { + zip.file(path, content) + }) + const blob = await zip.generateAsync({ type: 'blob' }) + const today = new Date() + const date = today.getFullYear() + '-' + (today.getMonth() + 1) + '-' + today.getDate() + const time = today.getHours() + 'h' + today.getMinutes() + 'min' + this.saveAs(blob, `remix-backup-at-${time}-${date}.zip`) + + } catch (e) { + console.log(e) + } + } + + populateWorkspace = async (json, fs) => { + for (const item in json) { + const isFolder = json[item].content === undefined + if (isFolder) { + await this.createDir(item, fs) + await this.populateWorkspace(json[item].children, fs) + } else { + await fs.writeFile(item, json[item].content, 'utf8') + } + } + } + + + /** + * copy the folder recursively + * @param {string} path is the folder to be copied over + * @param {Function} visitFile is a function called for each visited files + * @param {Function} visitFolder is a function called for each visited folders + */ + copyFolderToJson = async (path, visitFile, visitFolder, fs, cb = null) => { + visitFile = visitFile || (() => { }) + visitFolder = visitFolder || (() => { }) + return await this._copyFolderToJsonInternal(path, visitFile, visitFolder, fs, cb) + } + + /** + * copy the folder recursively (internal use) + * @param {string} path is the folder to be copied over + * @param {Function} visitFile is a function called for each visited files + * @param {Function} visitFolder is a function called for each visited folders + */ + async _copyFolderToJsonInternal(path, visitFile, visitFolder, fs, cb) { + visitFile = visitFile || function () { /* do nothing. */ } + visitFolder = visitFolder || function () { /* do nothing. */ } + + const json = {} + // path = this.removePrefix(path) + if (await fs.exists(path)) { + const items = await fs.readdir(path) + visitFolder({ path }) + if (items.length !== 0) { + for (const item of items) { + const file: any = {} + const curPath = `${path}${path.endsWith('/') ? '' : '/'}${item}` + if ((await fs.stat(curPath)).isDirectory()) { + file.children = await this._copyFolderToJsonInternal(curPath, visitFile, visitFolder, fs, cb) + } else { + file.content = await fs.readFile(curPath, 'utf8') + if (cb) cb({ path: curPath, content: file.content }) + visitFile({ path: curPath, content: file.content }) + + } + json[curPath] = file + } + } + } + return json + } + + createDir = async (path, fs) => { + const paths = path.split('/') + if (paths.length && paths[0] === '') paths.shift() + let currentCheck = '' + for (const value of paths) { + currentCheck = currentCheck + (currentCheck ? '/' : '') + value + if (!await fs.exists(currentCheck)) { + await fs.mkdir(currentCheck) + } + } + } + + saveAs = (blob, name) => { + const node = document.createElement('a') + node.download = name + node.rel = 'noopener' + node.href = URL.createObjectURL(blob) + setTimeout(function () { URL.revokeObjectURL(node.href) }, 4E4) // 40s + setTimeout(function () { + try { + node.dispatchEvent(new MouseEvent('click')) + } catch (e) { + const evt = document.createEvent('MouseEvents') + evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, + 20, false, false, false, false, 0, null) + node.dispatchEvent(evt) + } + }, 0) // 40s + } +} + + +/* eslint-disable no-template-curly-in-string */ +const testData = { + '.workspaces': { + children: { + '.workspaces/default_workspace': { + children: { + '.workspaces/default_workspace/README.txt': { + content: 'TEST README' + } + } + }, + '.workspaces/emptyspace': { + + }, + '.workspaces/workspace_test': { + children: { + '.workspaces/workspace_test/TEST_README.txt': { + content: 'TEST README' + }, + '.workspaces/workspace_test/test_contracts': { + children: { + '.workspaces/workspace_test/test_contracts/1_Storage.sol': { + content: 'testing' + }, + '.workspaces/workspace_test/test_contracts/artifacts': { + children: { + '.workspaces/workspace_test/test_contracts/artifacts/Storage_metadata.json': { + content: '{ "test": "data" }' + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/apps/remix-ide/src/app/tabs/theme-module.js b/apps/remix-ide/src/app/tabs/theme-module.js index 78c8f1788d..c17eaa95c3 100644 --- a/apps/remix-ide/src/app/tabs/theme-module.js +++ b/apps/remix-ide/src/app/tabs/theme-module.js @@ -33,10 +33,13 @@ export class ThemeModule extends Plugin { this._deps = { config: Registry.getInstance().get('config').api } - this.themes = themes.reduce((acc, theme) => { - theme.url = window.location.origin + window.location.pathname + theme.url - return { ...acc, [theme.name.toLocaleLowerCase()]: theme } - }, {}) + this.themes = {} + themes.map((theme) => { + this.themes[theme.name.toLocaleLowerCase()] = { + ...theme, + url: window.location.origin + window.location.pathname + theme.url + } + }) this._paq = _paq let queryTheme = (new QueryParams()).get().theme queryTheme = queryTheme && queryTheme.toLocaleLowerCase() @@ -65,6 +68,7 @@ export class ThemeModule extends Plugin { initTheme (callback) { // callback is setTimeOut in app.js which is always passed if (callback) this.initCallback = callback if (this.active) { + document.getElementById('theme-link') ? document.getElementById('theme-link').remove():null const nextTheme = this.themes[this.active] // Theme document.documentElement.style.setProperty('--theme', nextTheme.quality) @@ -93,9 +97,9 @@ export class ThemeModule extends Plugin { _paq.push(['trackEvent', 'themeModule', 'switchTo', next]) const nextTheme = this.themes[next] // Theme if (!this.forced) this._deps.config.set('settings/theme', next) - document.getElementById('theme-link').remove() - const theme = document.createElement('link') + document.getElementById('theme-link') ? document.getElementById('theme-link').remove():null + const theme = document.createElement('link') theme.setAttribute('rel', 'stylesheet') theme.setAttribute('href', nextTheme.url) theme.setAttribute('id', 'theme-link') diff --git a/apps/remix-ide/src/assets/js/init.js b/apps/remix-ide/src/assets/js/init.js index 460d9b565e..8265067b82 100644 --- a/apps/remix-ide/src/assets/js/init.js +++ b/apps/remix-ide/src/assets/js/init.js @@ -41,135 +41,12 @@ for (const k in assets[versionToLoad]) { } window.onload = () => { - // eslint-disable-next-line no-undef - class IndexedDBFS extends LightningFS { - constructor(...t) { - super(...t) - this.addSlash = (file) => { - if (!file.startsWith('/')) file = '/' + file - return file - } - this.base = this.promises - this.promises = { - ...this.promises, - - exists: async (path) => { - return new Promise((resolve, reject) => { - this.base.stat(this.addSlash(path)).then(() => resolve(true)).catch(() => resolve(false)) - }) - }, - rmdir: async (path) => { - return this.base.rmdir(this.addSlash(path)) - }, - readdir: async (path) => { - return this.base.readdir(this.addSlash(path)) - }, - unlink: async (path) => { - return this.base.unlink(this.addSlash(path)) - }, - mkdir: async (path) => { - return this.base.mkdir(this.addSlash(path)) - }, - readFile: async (path, options) => { - return this.base.readFile(this.addSlash(path), options) - }, - rename: async (from, to) => { - return this.base.rename(this.addSlash(from), this.addSlash(to)) - }, - writeFile: async (path, content, options) => { - return this.base.writeFile(this.addSlash(path), content, options) - }, - stat: async (path) => { - return this.base.stat(this.addSlash(path)) - } - } - } - } - function loadApp() { const app = document.createElement('script') app.setAttribute('src', versions[versionToLoad]) document.body.appendChild(app) } - async function ReadWriteTest(fs) { - try { - console.log(await fs.readdir('/')) - const str = 'Hello World' - await fs.writeFile('/test.txt', str , 'utf8') - if(await fs.readFile('/test.txt', 'utf8') === str){ - console.log('Read/Write Test Passed') - } - } catch (e) { - console.log(e) - } - } - - try { - // localStorage - // eslint-disable-next-line no-undef - BrowserFS.install(window) - // eslint-disable-next-line no-undef - BrowserFS.configure({ - fs: 'LocalStorage' - }, async function (e) { - if (e) { - console.log('BROWSEFS Error: ' + e) - } else { - window.remixLocalStorage = { ...window.require('fs') } - window.remixLocalStorageCallBack = window.require('fs') - window.remixLocalStorage.readdir = window.remixLocalStorage.readdirSync - window.remixLocalStorage.readFile = window.remixLocalStorage.readFileSync - window.remixLocalStorage.writeFile = window.remixLocalStorage.writeFileSync - window.remixLocalStorage.stat = window.remixLocalStorage.statSync - window.remixLocalStorage.unlink = window.remixLocalStorage.unlinkSync - window.remixLocalStorage.rmdir = window.remixLocalStorage.rmdirSync - window.remixLocalStorage.mkdir = window.remixLocalStorage.mkdirSync - window.remixLocalStorage.rename = window.remixLocalStorage.renameSync - window.remixLocalStorage.exists = window.remixLocalStorage.existsSync - //loadApp() - console.log('BrowserFS is ready!') - await ReadWriteTest(window.remixLocalStorage) - } - }) - } catch (e) { - console.log('BrowserFS is not ready!') - } - if (!window.indexedDB) { - console.log("Your browser doesn't support a stable version of IndexedDB. Such and such feature will not be available."); - } - var request = window.indexedDB.open("RemixTestDataBase", 3); - console.log(request) - request.onerror = event => { - // Do something with request.errorCode! - console.log('INDEEDDB ERROR') - }; - request.onsuccess = event => { - // Do something with request.result! - console.log("INDEEDDB SUCCESS") - window.indexedDB.deleteDatabase("RemixTestDataBase"); - activateIndexedDB() - }; - - function activateIndexedDB() { - // indexedDB - window.remixIndexedDBCallBack = new IndexedDBFS() - window.remixIndexedDBCallBack.init('RemixFileSystem').then(async () => { - window.remixIndexedDB = window.remixIndexedDBCallBack.promises - // check if .workspaces is present in indexeddb - console.log('indexeddb ready') - await ReadWriteTest(window.remixIndexedDB) - window.remixIndexedDB.stat('.workspaces').then((dir) => { - console.log(dir) - // if (dir.isDirectory()) loadApp() - }).catch((e) => { - console.log('error creating .workspaces', e) - // no indexeddb .workspaces -> run migration - // eslint-disable-next-line no-undef - //migrateFilesFromLocalStorage(loadApp) - }) - }).catch((e) => { - console.log('INDEEDDB ERROR: ' + e) - }) - } + loadApp() + return } diff --git a/apps/remix-ide/src/index.html b/apps/remix-ide/src/index.html index 3587ad1233..f2573bc29b 100644 --- a/apps/remix-ide/src/index.html +++ b/apps/remix-ide/src/index.html @@ -28,8 +28,6 @@ - -