var onUninstall, onInstall; const Plugins = { Vue: [], //Vue Object installed: [], //Simple List of Names json: undefined, //Json from website download_stats: {}, all: [], //Vue Object Data registered: {}, devReload() { var reloads = 0; for (var i = Plugins.all.length-1; i >= 0; i--) { if (Plugins.all[i].source == 'file') { Plugins.all[i].reload() reloads++; } } Blockbench.showQuickMessage(tl('message.plugin_reload', [reloads])) console.log('Reloaded '+reloads+ ' plugin'+pluralS(reloads)) }, sort() { Plugins.all.sort((a, b) => { if (a.tags.find(tag => tag.match(/deprecated/i))) return 1; if (b.tags.find(tag => tag.match(/deprecated/i))) return -1; let download_difference = (Plugins.download_stats[b.id] || 0) - (Plugins.download_stats[a.id] || 0); if (download_difference) { return download_difference } else { return sort_collator.compare(a.title, b.title); } }); } } StateMemory.init('installed_plugins', 'array') Plugins.installed = StateMemory.installed_plugins = StateMemory.installed_plugins.filter(p => p && typeof p == 'object'); class Plugin { constructor(id, data) { this.id = id||'unknown'; this.installed = false; this.title = ''; this.author = ''; this.description = ''; this.about = ''; this.icon = ''; this.tags = []; this.dependencies = []; this.version = '0.0.1'; this.variant = 'both'; this.min_version = ''; this.max_version = ''; this.source = 'store'; this.creation_date = 0; this.await_loading = false; this.about_fetched = false; this.disabled = false; this.extend(data) Plugins.all.safePush(this); } extend(data) { if (!(data instanceof Object)) return this; Merge.boolean(this, data, 'installed') Merge.string(this, data, 'title') Merge.string(this, data, 'author') Merge.string(this, data, 'description') Merge.string(this, data, 'about') Merge.string(this, data, 'icon') Merge.string(this, data, 'version') Merge.string(this, data, 'variant') Merge.string(this, data, 'min_version') Merge.boolean(this, data, 'await_loading'); Merge.boolean(this, data, 'disabled'); if (data.creation_date) this.creation_date = Date.parse(data.creation_date); if (data.tags instanceof Array) this.tags.safePush(...data.tags.slice(0, 3)); if (data.dependencies instanceof Array) this.dependencies.safePush(...data.dependencies); this.new_repo_format = this.min_version != '' && !compareVersions('4.8.0', this.min_version); Merge.function(this, data, 'onload') Merge.function(this, data, 'onunload') Merge.function(this, data, 'oninstall') Merge.function(this, data, 'onuninstall') return this; } get name() { return this.title; } async install() { let required_dependencies = this.dependencies .map(id => (Plugins.all.find(p => p.id == id) || id)) .filter(p => (p instanceof Plugin == false || p.installed == false)); if (required_dependencies.length) { let failed_dependency = required_dependencies.find(p => (!p.isInstallable || p.isInstallable() != true)); if (failed_dependency) { let error_message = failed_dependency; if (failed_dependency instanceof Plugin) { error_message = `**${failed_dependency.title}**: ${failed_dependency.isInstallable()}`; } Blockbench.showMessageBox({ title: 'message.plugin_dependencies.title', message: `${tl('message.plugin_dependencies.invalid')}\n\n${error_message}`, }); return; } let list = required_dependencies.map(p => `**${p.title}** ${tl('dialog.plugins.author', [p.author])}`); let response = await new Promise(resolve => { Blockbench.showMessageBox({ title: 'message.plugin_dependencies.title', message: `${tl('message.plugin_dependencies.message1')} \n\n* ${ list.join('\n* ') }\n\n${tl('message.plugin_dependencies.message2')}`, buttons: ['dialog.continue', 'dialog.cancel'], width: 512, }, button => { resolve(button == 0); }) }) if (!response) return; for (let dependency of required_dependencies) { await dependency.install(); } } return await this.download(true); } async load(first, cb) { var scope = this; Plugins.registered[this.id] = this; return await new Promise((resolve, reject) => { $.getScript(Plugins.path + scope.id + '.js', () => { if (cb) cb.bind(scope)() scope.bindGlobalData(first) if (first && scope.oninstall) { scope.oninstall() } if (first) Blockbench.showQuickMessage(tl('message.installed_plugin', [this.title])); resolve() }).fail(() => { if (isApp) { console.log('Could not find file of plugin "'+scope.id+'". Uninstalling it instead.') scope.uninstall() } if (first) Blockbench.showQuickMessage(tl('message.installed_plugin_fail', [this.title])); reject() }) this.remember() scope.installed = true; }) } bindGlobalData() { var scope = this; if (onUninstall) { scope.onuninstall = onUninstall } if (onUninstall) { scope.onuninstall = onUninstall } if (window.plugin_data) { console.warn(`plugin_data is deprecated. Please use Plugin.register instead. (${plugin_data.id || 'unknown plugin'})`) } window.onInstall = window.onUninstall = window.plugin_data = undefined return this; } async download(first) { var scope = this; function register() { jQuery.ajax({ url: 'https://blckbn.ch/api/event/install_plugin', type: 'POST', data: { plugin: scope.id } }) } if (!isApp) { if (first) register(); return await scope.load(first) } // Download files async function copyFileToDrive(origin_filename, target_filename, callback) { var file = originalFs.createWriteStream(PathModule.join(Plugins.path, target_filename)); https.get('https://cdn.jsdelivr.net/gh/JannisX11/blockbench-plugins/plugins/'+origin_filename, function(response) { response.pipe(file); if (callback) response.on('end', callback); }); } return await new Promise(async (resolve, reject) => { // New system if (this.new_repo_format) { copyFileToDrive(`${this.id}/${this.id}.js`, `${this.id}.js`, () => { if (first) register(); setTimeout(async function() { await scope.load(first); resolve() }, 20) }); if (this.hasImageIcon()) { copyFileToDrive(`${this.id}/${this.icon}`, this.id + '.' + this.icon); } await this.fetchAbout(); if (this.about) { fs.writeFileSync(PathModule.join(Plugins.path, this.id + '.about.md'), this.about, 'utf-8'); } } else { // Legacy system copyFileToDrive(`${this.id}.js`, `${this.id}.js`, () => { if (first) register(); setTimeout(async function() { await scope.load(first); resolve() }, 20) }); } }); } async loadFromFile(file, first) { var scope = this; if (!isApp && !first) return this; if (first) { if (isApp) { if (!confirm(tl('message.load_plugin_app'))) return; } else { if (!confirm(tl('message.load_plugin_web'))) return; } } this.id = pathToName(file.path); Plugins.registered[this.id] = this; Plugins.all.safePush(this); this.source = 'file'; this.tags.safePush('Local'); return await new Promise((resolve, reject) => { if (isApp) { $.getScript(file.path, () => { if (window.plugin_data) { scope.id = (plugin_data && plugin_data.id)||pathToName(file.path) scope.extend(plugin_data) scope.bindGlobalData() } if (first && scope.oninstall) { scope.oninstall() } scope.installed = true; scope.path = file.path; this.remember(); Plugins.sort(); resolve() }).fail(reject) } else { try { new Function(file.content)(); } catch (err) { reject(err) } if (!Plugins.registered && window.plugin_data) { scope.id = (plugin_data && plugin_data.id)||scope.id scope.extend(plugin_data) scope.bindGlobalData() } if (first && scope.oninstall) { scope.oninstall() } scope.installed = true this.remember() Plugins.sort() resolve() } }) } async loadFromURL(url, first) { if (first) { if (isApp) { if (!confirm(tl('message.load_plugin_app'))) return; } else { if (!confirm(tl('message.load_plugin_web'))) return; } } this.id = pathToName(url) Plugins.registered[this.id] = this; Plugins.all.safePush(this) this.tags.safePush('Remote'); this.source = 'url'; await new Promise((resolve, reject) => { $.getScript(url, () => { if (window.plugin_data) { this.id = (plugin_data && plugin_data.id)||pathToName(url) this.extend(plugin_data) this.bindGlobalData() } if (first && this.oninstall) { this.oninstall() } this.installed = true this.path = url this.remember() Plugins.sort() // Save if (isApp) { var file = originalFs.createWriteStream(Plugins.path+this.id+'.js') https.get(url, (response) => { response.pipe(file); response.on('end', resolve) }).on('error', reject); } else { resolve() } }).fail(() => { if (isApp) { this.load().then(resolve).catch(resolve) } }) }) return this; } remember(id = this.id, path = this.path) { let entry = Plugins.installed.find(plugin => plugin.id == this.id); let already_exists = !!entry; if (!entry) entry = {}; entry.id = id; entry.version = this.version; entry.path = path; entry.source = this.source; entry.disabled = this.disabled ? true : undefined; if (!already_exists) Plugins.installed.push(entry); StateMemory.save('installed_plugins') return this; } uninstall() { try { this.unload(); if (this.onuninstall) { this.onuninstall(); } } catch (err) { console.log('Error in unload or uninstall method: ', err); } delete Plugins.registered[this.id]; let in_installed = Plugins.installed.find(plugin => plugin.id == this.id); Plugins.installed.remove(in_installed); StateMemory.save('installed_plugins') this.installed = false; if (isApp && this.source !== 'store') { Plugins.all.remove(this) } if (isApp && this.source != 'file') { function removeCachedFile(filepath) { if (fs.existsSync(filepath)) { fs.unlink(filepath, (err) => { if (err) console.log(err); }); } } removeCachedFile(Plugins.path + this.id + '.js'); removeCachedFile(Plugins.path + this.id + '.' + this.icon); removeCachedFile(Plugins.path + this.id + '.about.md'); } StateMemory.save('installed_plugins') return this; } unload() { if (this.onunload) { this.onunload() } return this; } reload() { if (!isApp && this.source == 'file') return this; this.unload() this.tags.empty(); this.dependencies.empty(); Plugins.all.remove(this) if (this.source == 'file') { this.loadFromFile({path: this.path}, false) } else if (this.source == 'url') { this.loadFromURL(this.path, false) } return this; } toggleDisabled() { if (!this.disabled) { this.disabled = true; this.unload() } else { if (this.onload) { this.onload() } this.disabled = false; } this.remember(); } isReloadable() { return this.installed && !this.disabled && ((this.source == 'file' && isApp) || (this.source == 'url')); } isInstallable() { var scope = this; var result = scope.variant === 'both' || ( isApp === (scope.variant === 'desktop') && isApp !== (scope.variant === 'web') ); if (result && scope.min_version) { result = Blockbench.isOlderThan(scope.min_version) ? 'outdated_client' : true; } if (result && scope.max_version) { result = Blockbench.isNewerThan(scope.max_version) ? 'outdated_plugin' : true } if (result === false) { result = (scope.variant === 'web') ? 'web_only' : 'app_only' } return (result === true) ? true : tl('dialog.plugins.'+result); } hasImageIcon() { return this.icon.endsWith('.png') || this.icon.endsWith('.svg'); } getIcon() { if (this.hasImageIcon()) { if (isApp) { if (this.installed && this.source == 'store') { return Plugins.path + this.id + '.' + this.icon; } if (this.source != 'store') return this.path.replace(/\w+\.js$/, this.icon); } return `https://cdn.jsdelivr.net/gh/JannisX11/blockbench-plugins/plugins/${this.id}/${this.icon}`; } return this.icon; } async fetchAbout() { if (!this.about_fetched && !this.about && this.new_repo_format) { if (isApp && this.installed) { try { let content = fs.readFileSync(PathModule.join(Plugins.path, this.id + '.about.md'), {encoding: 'utf-8'}); this.about = content; this.about_fetched = true; return; } catch (err) { console.error('failed to get about for plugin ' + this.id); } } let url = `https://cdn.jsdelivr.net/gh/JannisX11/blockbench-plugins/plugins/${this.id}/about.md`; let result = await fetch(url).catch(() => { console.error('about.md missing for plugin ' + this.id); }); if (result.ok) { this.about = await result.text(); } this.about_fetched = true; } } } // Alias for typescript const BBPlugin = Plugin; Plugin.register = function(id, data) { if (typeof id !== 'string' || typeof data !== 'object') { console.warn('Plugin.register: not enough arguments, string and object required.') return; } var plugin = Plugins.registered[id]; if (!plugin) { plugin = Plugins.registered.unknown; if (plugin) { delete Plugins.registered.unknown; plugin.id = id; Plugins.registered[id] = plugin; } } if (!plugin) { Blockbench.showMessageBox({ translateKey: 'load_plugin_failed', message: tl('message.load_plugin_failed.message', [id]) }) }; plugin.extend(data) if (plugin.isInstallable() == true && plugin.disabled == false) { if (plugin.onload instanceof Function) { plugin.onload() } } return plugin; } if (isApp) { Plugins.path = app.getPath('userData')+osfs+'plugins'+osfs fs.readdir(Plugins.path, function(err) { if (err) { fs.mkdir(Plugins.path, function(a) {}) } }) } else { Plugins.path = 'https://cdn.jsdelivr.net/gh/JannisX11/blockbench-plugins/plugins/'; } Plugins.loading_promise = new Promise((resolve, reject) => { $.ajax({ cache: false, url: 'https://cdn.jsdelivr.net/gh/JannisX11/blockbench-plugins/plugins.json', dataType: 'json', success(data) { Plugins.json = data; resolve(); Plugins.loading_promise.resolved = true; }, error() { console.log('Could not connect to plugin server') $('#plugin_available_empty').text('Could not connect to plugin server') resolve(); Plugins.loading_promise.resolved = true; } }); }) $.getJSON('https://blckbn.ch/api/stats/plugins?weeks=2', data => { Plugins.download_stats = data; if (Plugins.json) { Plugins.sort(); } }) async function loadInstalledPlugins() { if (!Plugins.loading_promise.resolved) { await Plugins.loading_promise; } const install_promises = []; if (Plugins.json instanceof Object && navigator.onLine) { //From Store for (var id in Plugins.json) { var plugin = new Plugin(id, Plugins.json[id]); let installed_match = Plugins.installed.find(p => { return p && p.id == id && p.source == 'store' }); if (installed_match) { if (isApp && ( (installed_match.version && plugin.version && !compareVersions(plugin.version, installed_match.version)) || Blockbench.isOlderThan(plugin.min_version) )) { // Get from file let promise = plugin.load(false); install_promises.push(promise); } else { // Update let promise = plugin.download(); if (plugin.await_loading) { install_promises.push(promise); } } } } Plugins.sort(); } else if (Plugins.installed.length > 0 && isApp) { //Offline Plugins.installed.forEach(function(plugin_data) { if (plugin_data.source == 'store') { let instance = new Plugin(plugin_data.id); let promise = instance.load(false, function() { Plugins.sort(); }) install_promises.push(promise); } }) } if (Plugins.installed.length > 0) { var load_counter = 0; Plugins.installed.forEachReverse(function(plugin) { if (plugin.source == 'file') { //Dev Plugins if (isApp && fs.existsSync(plugin.path)) { var instance = new Plugin(plugin.id, {disabled: plugin.disabled}); install_promises.push(instance.loadFromFile({path: plugin.path}, false)); load_counter++; console.log(`🧩📁 Loaded plugin "${plugin.id || plugin.path}" from file`); } else { Plugins.installed.remove(plugin); } } else if (plugin.source == 'url') { var instance = new Plugin(plugin.id, {disabled: plugin.disabled}); install_promises.push(instance.loadFromURL(plugin.path, false)); load_counter++; console.log(`🧩🌐 Loaded plugin "${plugin.id || plugin.path}" from URL`); } else { if (Plugins.all.find(p => p.id == plugin.id)) { load_counter++; console.log(`🧩🛒 Loaded plugin "${plugin.id}" from store`); } else { Plugins.installed.remove(plugin); } } }) console.log(`Loaded ${load_counter} plugin${pluralS(load_counter)}`) } StateMemory.save('installed_plugins') install_promises.forEach(promise => { promise.catch(console.error); }) return await Promise.allSettled(install_promises); } BARS.defineActions(function() { Plugins.dialog = new Dialog({ id: 'plugins', title: 'dialog.plugins.title', buttons: [], width: 1200, component: { data: { tab: 'installed', search_term: '', items: Plugins.all, selected_plugin: null, page: 0, per_page: 25, isMobile: Blockbench.isMobile, }, computed: { plugin_search() { var name = this.search_term.toUpperCase() return this.items.filter(item => { if ((this.tab == 'installed') == item.installed) { if (name.length > 0) { return ( item.id.toUpperCase().includes(name) || item.title.toUpperCase().includes(name) || item.description.toUpperCase().includes(name) || item.author.toUpperCase().includes(name) || item.tags.find(tag => tag.toUpperCase().includes(name)) ) } return true; } return false; }) }, suggested_rows() { let tags = ["Animation"]; this.items.forEach(plugin => { if (!plugin.installed) return; tags.safePush(...plugin.tags) }) let rows = tags.map(tag => { let plugins = this.items.filter(plugin => !plugin.installed && plugin.tags.includes(tag) && !plugin.tags.includes('Deprecated')).slice(0, 12); return { title: tag, plugins, } }).filter(row => row.plugins.length > 2); //rows.sort((a, b) => a.plugins.length - b.plugins.length); rows.sort(() => Math.random() - 0.5); let cutoff = Date.now() - (3_600_000 * 24 * 28); let new_plugins = this.items.filter(plugin => !plugin.installed && plugin.creation_date > cutoff && !plugin.tags.includes('Deprecated')) new_plugins.sort((a, b) => a.creation_date - b.creation_date); let new_row = { title: 'New', plugins: new_plugins.slice(0, 12) } rows.splice(0, 0, new_row); return rows.slice(0, 3); }, viewed_plugins() { return this.plugin_search.slice(this.page * this.per_page, (this.page+1) * this.per_page); }, pages() { let pages = []; let length = this.plugin_search.length; for (let i = 0; i * this.per_page < length; i++) { pages.push(i); } return pages; } }, methods: { setTab(tab) { this.tab = tab; this.setPage(0); }, setPage(number) { this.page = number; }, selectPlugin(plugin) { plugin.fetchAbout(); this.selected_plugin = plugin; }, showDependency(dependency) { let plugin = Plugins.all.find(p => p.id == dependency); if (plugin) { this.selectPlugin(plugin); } }, getDependencyName(dependency) { let plugin = Plugins.all.find(p => p.id == dependency); return plugin ? (plugin.title + (plugin.installed ? ' ✓' : '')) : (dependency + ' ⚠'); }, isDependencyInstalled(dependency) { let plugin = Plugins.all.find(p => p.id == dependency); return plugin && plugin.installed; }, getTagClass(tag) { if (tag.match(/^(local|remote)$/i)) { return 'plugin_tag_source' } else if (tag.match(/^minecraft/i)) { return 'plugin_tag_mc' } else if (tag.match(/^deprecated/i)) { return 'plugin_tag_deprecated' } }, formatAbout(about) { return pureMarked(about); }, getIconNode: Blockbench.getIconNode, pureMarked, tl }, mount_directly: true, template: `
home
${tl('dialog.plugins.installed')}
${tl('dialog.plugins.available')}
  1. {{ number+1 }}
Back to Overview todo

{{ selected_plugin.title || selected_plugin.id }}
v{{ selected_plugin.version }}

{{ tl('dialog.plugins.author', [selected_plugin.author]) }}
🌙 ${tl('dialog.plugins.is_disabled')}
✓ ${tl('dialog.plugins.is_installed')}
{{ selected_plugin.description }}
${tl('dialog.plugins.dependencies')} {{ getDependencyName(dep) }}
error {{ selected_plugin.isInstallable() }}

About

Blockbench Plugins

Plugins allow you to configure Blockbench beyond the default capabilities. Select from a list of 100 community created plugins.

Want to write your own plugin? Check out the Plugin Documentation.

{{row.title}}

  • {{ plugin.title || plugin.id }}
    {{ plugin.author }}
` } }) let actions_setup = false; new Action('plugins_window', { icon: 'extension', category: 'blockbench', side_menu: new Menu('plugins_window', [ 'load_plugin', 'load_plugin_from_url' ]), click(e) { Plugins.dialog.show(); let none_installed = !Plugins.all.find(plugin => plugin.installed); if (none_installed) Plugins.dialog.content_vue.tab = 'available'; if (!actions_setup) { BarItems.load_plugin.toElement('#plugins_list_main_bar'); BarItems.load_plugin_from_url.toElement('#plugins_list_main_bar'); actions_setup = true; } $('dialog#plugins #plugin_search_bar input').trigger('focus') } }) new Action('reload_plugins', { icon: 'sync', category: 'blockbench', click() { Plugins.devReload() } }) new Action('load_plugin', { icon: 'fa-file-code', category: 'blockbench', click() { Blockbench.import({ resource_id: 'dev_plugin', extensions: ['js'], type: 'Blockbench Plugin', }, function(files) { new Plugin().loadFromFile(files[0], true) }) } }) new Action('load_plugin_from_url', { icon: 'cloud_download', category: 'blockbench', click() { Blockbench.textPrompt('URL', '', url => { new Plugin().loadFromURL(url, true) }) } }) new Action('add_plugin', { icon: 'add', category: 'blockbench', click() { setTimeout(_ => ActionControl.select('+plugin: '), 1); } }) new Action('remove_plugin', { icon: 'remove', category: 'blockbench', click() { setTimeout(_ => ActionControl.select('-plugin: '), 1); } }) })