mirror of
https://github.com/JannisX11/blockbench.git
synced 2025-04-06 17:31:09 +08:00
Merge branch 'plugin-browser' into next
This commit is contained in:
commit
ada4cd6d69
BIN
assets/plugins.png
Normal file
BIN
assets/plugins.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
267
css/dialogs.css
267
css/dialogs.css
@ -222,7 +222,7 @@
|
||||
margin: 4px 0;
|
||||
}
|
||||
.dialog h3 {
|
||||
margin-left: 16px;
|
||||
margin-left: 0;
|
||||
}
|
||||
.dialog_bar label.in_toolbar {
|
||||
padding-left: 0;
|
||||
@ -1116,6 +1116,37 @@ dialog#edit_bedrock_binding > .dialog_wrapper > .dialog_content {
|
||||
}
|
||||
|
||||
/*Plugin Menu*/
|
||||
dialog#plugins {
|
||||
max-width: min(1400px, 100%);
|
||||
height: calc(96% - 108px);
|
||||
}
|
||||
dialog#plugins .dialog_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
dialog#plugins content.dialog_content {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
max-height: initial;
|
||||
}
|
||||
#plugin_browser_sidebar {
|
||||
width: 38.2%;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 10px;
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
#plugin_browser_page,
|
||||
#plugin_browser_start_page {
|
||||
width: 61.8%;
|
||||
flex-grow: 1;
|
||||
padding: 16px 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.bar.next_to_title {
|
||||
display: inline-block;
|
||||
vertical-align: text-bottom;
|
||||
@ -1127,37 +1158,67 @@ dialog#edit_bedrock_binding > .dialog_wrapper > .dialog_content {
|
||||
float: left;
|
||||
z-index: inherit;
|
||||
}
|
||||
#plugin_search_bar {
|
||||
flex-grow: 1;
|
||||
}
|
||||
#plugins .tab_bar {
|
||||
width: calc(100% - 300px);
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
#plugin_browser_sidebar > .pagination_numbers {
|
||||
padding: 8px;
|
||||
}
|
||||
#plugin_list {
|
||||
max-height: 600px;
|
||||
overflow-y: scroll;
|
||||
min-height: 80px;
|
||||
}
|
||||
#plugin_list > li {
|
||||
min-height: 128px;
|
||||
overflow-y: hidden;
|
||||
margin: 8px;
|
||||
background-color: var(--color-ui);
|
||||
margin: 12px;
|
||||
padding: 8px 12px;
|
||||
padding-bottom: 12px;
|
||||
margin-right: 2px;
|
||||
cursor: pointer;
|
||||
background-color: var(--color-ui);
|
||||
}
|
||||
#plugin_list > li.selected {
|
||||
background-color: var(--color-button);
|
||||
}
|
||||
#plugin_list > li.incompatible {
|
||||
color: var(--color-subtle_text);
|
||||
}
|
||||
body.theme_borders #plugin_list > li {
|
||||
border: 1px solid var(--color-border);
|
||||
margin: 0;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
#plugin_list > li.expanded {
|
||||
min-height: 128px;
|
||||
height: auto;
|
||||
#plugin_list > li > div:first-child {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
#plugin_list > li .icon_wrapper {
|
||||
display: inline;
|
||||
#plugin_list > li:hover:not(.incompatible) .title {
|
||||
color: var(--color-light);
|
||||
}
|
||||
#plugin_list > li > * {
|
||||
margin: 0;
|
||||
margin-left: 12px;
|
||||
cursor: default;
|
||||
.plugin_icon_area {
|
||||
flex: 0 0 48px;
|
||||
padding-top: 2px;
|
||||
text-align: center;
|
||||
margin-left: -2px;
|
||||
height: 52px;
|
||||
}
|
||||
.plugin_icon_area .icon {
|
||||
font-size: 32px;
|
||||
width: 48px;
|
||||
max-width: unset;
|
||||
margin-top: 8px;
|
||||
display: inline-block;
|
||||
}
|
||||
.plugin_icon_area img {
|
||||
border-radius: 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
#plugin_list > li * {
|
||||
cursor: inherit;
|
||||
}
|
||||
#plugin_list > li .button_bar {
|
||||
height: auto;
|
||||
@ -1166,10 +1227,11 @@ dialog#edit_bedrock_binding > .dialog_wrapper > .dialog_content {
|
||||
margin-top: 0;
|
||||
text-align: right;
|
||||
}
|
||||
#plugin_list > li .button_bar.tiny {
|
||||
color: var(--color-subtle_text);
|
||||
font-size: 0.86em;
|
||||
padding-right: 2px;
|
||||
.plugin_compatibility_issue {
|
||||
color: var(--color-error);
|
||||
}
|
||||
.plugin_compatibility_issue > .icon {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
#plugin_list > li button {
|
||||
min-width: 100px;
|
||||
@ -1187,29 +1249,7 @@ dialog#edit_bedrock_binding > .dialog_wrapper > .dialog_content {
|
||||
margin-top: 2px;
|
||||
}
|
||||
#plugin_list > li .title {
|
||||
width: auto;
|
||||
float: left;
|
||||
font-size: 1.34em;
|
||||
padding-top: 8px;
|
||||
margin-bottom: -5px;
|
||||
height: 48px;
|
||||
}
|
||||
#plugin_list > li .title i {
|
||||
width: 22px;
|
||||
font-size: 0.9em;
|
||||
padding: 3px;
|
||||
float: left;
|
||||
margin-top: 5px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
#plugin_list > li .title i.plugin_expand_icon {
|
||||
display: none;
|
||||
}
|
||||
#plugin_list > li.has_about_text .title:hover i.plugin_expand_icon {
|
||||
display: inline-block;
|
||||
}
|
||||
#plugin_list > li.has_about_text .title:hover .plugin_icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#plugin_list .plugin_version {
|
||||
@ -1229,6 +1269,26 @@ dialog#edit_bedrock_binding > .dialog_wrapper > .dialog_content {
|
||||
max-height: 148px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.plugin_installed_tag,
|
||||
.plugin_disabled_tag {
|
||||
display: inline-block;
|
||||
background-color: var(--color-back);
|
||||
height: 25px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 5px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.plugin_installed_tag {
|
||||
color: var(--color-confirm);
|
||||
}
|
||||
dialog#plugins .version {
|
||||
display: inline-block;
|
||||
color: var(--color-subtle_text);
|
||||
background-color: var(--color-back);
|
||||
font-size: 15px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
#plugin_list .about {
|
||||
overflow-y: auto;
|
||||
max-height: 480px;
|
||||
@ -1236,25 +1296,32 @@ dialog#edit_bedrock_binding > .dialog_wrapper > .dialog_content {
|
||||
padding: 6px 12px;
|
||||
background: var(--color-button);
|
||||
}
|
||||
dialog#plugins .author {
|
||||
color: var(--color-subtle_text);
|
||||
margin-top: -6px;
|
||||
}
|
||||
#plugin_list > li ul.plugin_tag_list {
|
||||
margin: 5px 8px;
|
||||
margin-top: 4px;
|
||||
line-height: 0;
|
||||
}
|
||||
.plugin_tag_list li {
|
||||
display: inline-block;
|
||||
background-color: var(--color-accent);
|
||||
color: var(--color-accent_text);
|
||||
max-width: 180px;
|
||||
height: 22px;
|
||||
padding: 1px 4px;
|
||||
height: 25px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 5px;
|
||||
font-size: 0.9em;
|
||||
margin: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
overflow: hidden;
|
||||
line-height: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
#plugin_list .plugin_tag_list li {
|
||||
height: 22px;
|
||||
font-size: 0.9em;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
.plugin_tag_list li.plugin_tag_source {
|
||||
background-color: #ff7a52;
|
||||
color: #111625;
|
||||
@ -1272,6 +1339,109 @@ dialog#edit_bedrock_binding > .dialog_wrapper > .dialog_content {
|
||||
margin-top: 30px;
|
||||
color: var(--color-subtle_text);
|
||||
}
|
||||
#plugin_browser_page .button_bar {
|
||||
margin: 8px 0px;
|
||||
float: right;
|
||||
}
|
||||
#plugin_browser_page .button_bar button {
|
||||
height: 70px;
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
min-width: 72px;
|
||||
width: auto;
|
||||
margin-right: 0;
|
||||
padding: 0 2px;
|
||||
text-decoration: none;
|
||||
}
|
||||
#plugin_browser_page .button_bar button:hover {
|
||||
background-color: var(--color-accent);
|
||||
}
|
||||
#plugin_browser_page .button_bar button i {
|
||||
display: block;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
max-width: unset;
|
||||
font-size: 35px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.disabled_plugin {
|
||||
color: var(--color-subtle_text);
|
||||
}
|
||||
.plugin_browser_page_header {
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.plugin_browser_page_header .plugin_icon_area {
|
||||
flex-basis: 64px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.plugin_browser_page_header h1 {
|
||||
margin: 0;
|
||||
}
|
||||
#plugin_page_background_decoration {
|
||||
pointer-events: none;
|
||||
color: black;
|
||||
opacity: 0.1;
|
||||
font-size: 700px;
|
||||
height: 614px;
|
||||
width: 584px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
overflow: hidden;
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
|
||||
#plugin_browser_start_page > img {
|
||||
float: right;
|
||||
width: 320px;
|
||||
margin-bottom: -26px;
|
||||
margin-top: -20px;
|
||||
}
|
||||
.plugins_suggested_row {
|
||||
width: 100%;
|
||||
clear: both;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.plugins_suggested_row > ul {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
background-color: var(--color-back);
|
||||
overflow-x: scroll;
|
||||
padding: 12px 32px;
|
||||
width: calc(100% + 48px);
|
||||
margin-right: -24px;
|
||||
margin-left: -24px;
|
||||
}
|
||||
.plugins_suggested_row > ul > li {
|
||||
width: 211px;
|
||||
height: 130px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
padding-top: 5px;
|
||||
background-color: var(--color-ui);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
padding: 4px 4px;
|
||||
}
|
||||
.plugins_suggested_row > ul > li:hover {
|
||||
background-color: var(--color-button);
|
||||
}
|
||||
.plugins_suggested_row > ul > li * {
|
||||
cursor: inherit;
|
||||
}
|
||||
.plugins_suggested_row > ul > li .title {
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
line-height: normal;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Search Bar */
|
||||
.search_bar {
|
||||
float: right;
|
||||
position: relative;
|
||||
@ -1302,7 +1472,6 @@ dialog#edit_bedrock_binding > .dialog_wrapper > .dialog_content {
|
||||
color: var(--color-light);
|
||||
}
|
||||
|
||||
|
||||
/*Toolbar Dialog*/
|
||||
dialog#toolbar_edit .search_bar {
|
||||
margin-top: 10px;
|
||||
|
@ -725,7 +725,8 @@
|
||||
body.theme_borders .dialog_close_button,
|
||||
body.theme_borders #start_screen > content,
|
||||
body.theme_borders #quick_message_box,
|
||||
body.theme_borders action_selector > #action_selector_list
|
||||
body.theme_borders action_selector > #action_selector_list,
|
||||
body.theme_borders .plugins_suggested_row > ul > li
|
||||
{
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
@ -471,7 +471,8 @@
|
||||
}
|
||||
button > i {
|
||||
pointer-events: none;
|
||||
vertical-align: middle;
|
||||
vertical-align: sub;
|
||||
margin-right: 4px;
|
||||
}
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
|
@ -394,7 +394,7 @@ BARS.defineActions(function() {
|
||||
template: `
|
||||
<dialog id="action_selector" v-if="open">
|
||||
<div class="tool" ref="search_type_menu" @click="openTypeMenu($event)">
|
||||
<div class="icon_wrapper normal" v-html="getIconNode(search_types[search_type] ? search_types[search_type].icon : 'fullscreen').outerHTML"></div>
|
||||
<dynamic-icon :icon="search_types[search_type] ? search_types[search_type].icon : 'fullscreen'" />
|
||||
</div>
|
||||
<input type="text" v-model="search_input" inputmode="search" @input="e => search_input = e.target.value" autocomplete="off" autosave="off" autocorrect="off" spellcheck="false" autocapitalize="off">
|
||||
<i class="material-icons" id="action_search_bar_icon" @click="search_input = ''">{{ search_input ? 'clear' : 'search' }}</i>
|
||||
@ -407,7 +407,7 @@ BARS.defineActions(function() {
|
||||
@click="click(item, $event)"
|
||||
@mouseenter="index = i"
|
||||
>
|
||||
<div class="icon_wrapper normal" v-html="getIconNode(item.icon, item.color).outerHTML"></div>
|
||||
<dynamic-icon :icon="item.icon" :color="item.color" />
|
||||
<span>{{ item.name }}</span>
|
||||
<label class="keybinding_label">{{ item.keybind_label || (item.keybind ? item.keybind.label : '') }}</label>
|
||||
</li>
|
||||
|
@ -2510,7 +2510,7 @@ const BARS = {
|
||||
|
||||
<ul class="list" id="bar_item_list">
|
||||
<li v-for="item in searchedBarItems" v-on:click="addItem(item)" :class="{separator_item: item.type == 'separator'}">
|
||||
<div class="icon_wrapper normal" v-html="getIconNode(item.icon, item.color).outerHTML"></div>
|
||||
<dynamic-icon :icon="item.icon" :color="item.color" />
|
||||
<div class="icon_wrapper add"><i class="material-icons">add</i></div>
|
||||
{{ item.name }}
|
||||
</li>
|
||||
|
@ -399,10 +399,17 @@ function buildLines(dialog) {
|
||||
})
|
||||
}
|
||||
function buildComponent(dialog) {
|
||||
let dialog_content = $(dialog.object).find('.dialog_content')
|
||||
let mount = $(`<div />`).appendTo(dialog_content)
|
||||
let dialog_content = $(dialog.object).find('.dialog_content').get(0);
|
||||
let mount;
|
||||
// mount_directly, if enabled, skips one layer of wrapper. Class "dialog_content" must be added the the root element of the vue component.
|
||||
if (dialog.component.mount_directly) {
|
||||
mount = dialog_content;
|
||||
} else {
|
||||
mount = Interface.createElement('div');
|
||||
dialog_content.append(mount);
|
||||
}
|
||||
dialog.component.name = 'dialog-content'
|
||||
dialog.content_vue = new Vue(dialog.component).$mount(mount.get(0));
|
||||
dialog.content_vue = new Vue(dialog.component).$mount(mount);
|
||||
}
|
||||
function getStringWidth(string, size) {
|
||||
let node = Interface.createElement('label', {style: 'position: absolute; visibility: hidden;'}, string);
|
||||
|
@ -136,3 +136,13 @@ Vue.component('numeric-input', {
|
||||
</div>
|
||||
`
|
||||
})
|
||||
Vue.component('dynamic-icon', {
|
||||
props: {
|
||||
icon: String,
|
||||
color: String,
|
||||
},
|
||||
render(h) {
|
||||
let node = Blockbench.getIconNode(this.icon, this.color);
|
||||
return h(node.tagName, {class: node.className, src: node.attributes.src?.value}, node.textContent);
|
||||
}
|
||||
})
|
||||
|
@ -37,7 +37,6 @@ class Plugin {
|
||||
constructor(id, data) {
|
||||
this.id = id||'unknown';
|
||||
this.installed = false;
|
||||
this.expanded = false;
|
||||
this.title = '';
|
||||
this.author = '';
|
||||
this.description = '';
|
||||
@ -48,8 +47,11 @@ class Plugin {
|
||||
this.variant = 'both';
|
||||
this.min_version = '';
|
||||
this.max_version = '';
|
||||
this.source = 'store'
|
||||
this.source = 'store';
|
||||
this.creation_date = 0;
|
||||
this.await_loading = false;
|
||||
this.about_fetched = false;
|
||||
this.disabled = false;
|
||||
|
||||
this.extend(data)
|
||||
|
||||
@ -58,7 +60,6 @@ class Plugin {
|
||||
extend(data) {
|
||||
if (!(data instanceof Object)) return this;
|
||||
Merge.boolean(this, data, 'installed')
|
||||
Merge.boolean(this, data, 'expanded')
|
||||
Merge.string(this, data, 'title')
|
||||
Merge.string(this, data, 'author')
|
||||
Merge.string(this, data, 'description')
|
||||
@ -68,8 +69,12 @@ class Plugin {
|
||||
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));
|
||||
|
||||
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')
|
||||
@ -135,18 +140,43 @@ class Plugin {
|
||||
if (first) register();
|
||||
return await scope.load(first)
|
||||
}
|
||||
return await new Promise((resolve, reject) => {
|
||||
var file = originalFs.createWriteStream(Plugins.path+this.id+'.js')
|
||||
https.get('https://cdn.jsdelivr.net/gh/JannisX11/blockbench-plugins/plugins/'+this.id+'.js', function(response) {
|
||||
|
||||
// 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);
|
||||
response.on('end', function() {
|
||||
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) {
|
||||
@ -162,7 +192,6 @@ class Plugin {
|
||||
|
||||
this.id = pathToName(file.path);
|
||||
Plugins.registered[this.id] = this;
|
||||
localStorage.setItem('plugin_dev_path', file.path);
|
||||
Plugins.all.safePush(this);
|
||||
this.source = 'file';
|
||||
this.tags.safePush('Local');
|
||||
@ -217,7 +246,6 @@ class Plugin {
|
||||
|
||||
this.id = pathToName(url)
|
||||
Plugins.registered[this.id] = this;
|
||||
localStorage.setItem('plugin_dev_path', url)
|
||||
Plugins.all.safePush(this)
|
||||
this.tags.safePush('Remote');
|
||||
|
||||
@ -263,6 +291,7 @@ class Plugin {
|
||||
entry.version = this.version;
|
||||
entry.path = path;
|
||||
entry.source = this.source;
|
||||
entry.disabled = this.disabled ? true : undefined;
|
||||
|
||||
if (!already_exists) Plugins.installed.push(entry);
|
||||
|
||||
@ -288,14 +317,16 @@ class Plugin {
|
||||
Plugins.all.remove(this)
|
||||
}
|
||||
if (isApp && this.source != 'file') {
|
||||
var filepath = Plugins.path + this.id + '.js'
|
||||
if (fs.existsSync(filepath)) {
|
||||
fs.unlink(filepath, (err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
}
|
||||
});
|
||||
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;
|
||||
@ -321,8 +352,20 @@ class Plugin {
|
||||
}
|
||||
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.source == 'file' && isApp) || (this.source == 'url')
|
||||
return this.installed && !this.disabled && ((this.source == 'file' && isApp) || (this.source == 'url'));
|
||||
}
|
||||
isInstallable() {
|
||||
var scope = this;
|
||||
@ -343,20 +386,43 @@ class Plugin {
|
||||
}
|
||||
return (result === true) ? true : tl('dialog.plugins.'+result);
|
||||
}
|
||||
toggleInfo(force) {
|
||||
if (!this.about) return;
|
||||
var scope = this;
|
||||
Plugins.all.forEach(function(p) {
|
||||
if (p !== scope && p.expanded) p.expanded = false;
|
||||
})
|
||||
if (force !== undefined) {
|
||||
this.expanded = force === true
|
||||
} else {
|
||||
this.expanded = this.expanded !== true
|
||||
}
|
||||
hasImageIcon() {
|
||||
return this.icon.endsWith('.png') || this.icon.endsWith('.svg');
|
||||
}
|
||||
get expandicon() {
|
||||
return this.expanded ? 'expand_less' : 'expand_more'
|
||||
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
|
||||
@ -383,8 +449,7 @@ Plugin.register = function(id, data) {
|
||||
})
|
||||
};
|
||||
plugin.extend(data)
|
||||
if (data.icon) plugin.icon = Blockbench.getIconNode(data.icon)
|
||||
if (plugin.isInstallable() == true) {
|
||||
if (plugin.isInstallable() == true && plugin.disabled == false) {
|
||||
if (plugin.onload instanceof Function) {
|
||||
plugin.onload()
|
||||
}
|
||||
@ -404,13 +469,19 @@ if (isApp) {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// testing (todo: remove)
|
||||
data.animation_sliders.icon = 'icon.png';
|
||||
data.animation_sliders.about = '';
|
||||
data.animation_sliders.min_version = '4.8.0';
|
||||
data.animation_sliders.creation_date = '2023-06-23';
|
||||
|
||||
resolve();
|
||||
Plugins.loading_promise.resolved = true;
|
||||
},
|
||||
@ -444,7 +515,10 @@ async function loadInstalledPlugins() {
|
||||
return p && p.id == id && p.source == 'store'
|
||||
});
|
||||
if (installed_match) {
|
||||
if (isApp && installed_match.version && plugin.version && !compareVersions(plugin.version, installed_match.version)) {
|
||||
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);
|
||||
@ -460,11 +534,11 @@ async function loadInstalledPlugins() {
|
||||
Plugins.sort();
|
||||
} else if (Plugins.installed.length > 0 && isApp) {
|
||||
//Offline
|
||||
Plugins.installed.forEach(function(plugin) {
|
||||
Plugins.installed.forEach(function(plugin_data) {
|
||||
|
||||
if (plugin.source == 'store') {
|
||||
let plugin = new Plugin(plugin.id);
|
||||
let promise = plugin.load(false, function() {
|
||||
if (plugin_data.source == 'store') {
|
||||
let instance = new Plugin(plugin_data.id);
|
||||
let promise = instance.load(false, function() {
|
||||
Plugins.sort();
|
||||
})
|
||||
install_promises.push(promise);
|
||||
@ -478,7 +552,7 @@ async function loadInstalledPlugins() {
|
||||
if (plugin.source == 'file') {
|
||||
//Dev Plugins
|
||||
if (isApp && fs.existsSync(plugin.path)) {
|
||||
var instance = new Plugin(plugin.id);
|
||||
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`);
|
||||
@ -487,7 +561,7 @@ async function loadInstalledPlugins() {
|
||||
}
|
||||
|
||||
} else if (plugin.source == 'url') {
|
||||
var instance = new Plugin(plugin.id);
|
||||
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`);
|
||||
@ -517,13 +591,17 @@ BARS.defineActions(function() {
|
||||
Plugins.dialog = new Dialog({
|
||||
id: 'plugins',
|
||||
title: 'dialog.plugins.title',
|
||||
singleButton: true,
|
||||
width: 760,
|
||||
buttons: [],
|
||||
width: 1200,
|
||||
component: {
|
||||
data: {
|
||||
tab: 'installed',
|
||||
search_term: '',
|
||||
items: Plugins.all
|
||||
items: Plugins.all,
|
||||
selected_plugin: null,
|
||||
page: 0,
|
||||
per_page: 25,
|
||||
isMobile: Blockbench.isMobile,
|
||||
},
|
||||
computed: {
|
||||
plugin_search() {
|
||||
@ -543,9 +621,58 @@ BARS.defineActions(function() {
|
||||
}
|
||||
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;
|
||||
},
|
||||
getTagClass(tag) {
|
||||
if (tag.match(/^(local|remote)$/i)) {
|
||||
return 'plugin_tag_source'
|
||||
@ -555,52 +682,133 @@ BARS.defineActions(function() {
|
||||
return 'plugin_tag_deprecated'
|
||||
}
|
||||
},
|
||||
formatAbout(about) {
|
||||
about = about.replace(/\n/g, '\n\n').replace(/\n#/, '\n##');
|
||||
return pureMarked(about);
|
||||
},
|
||||
getIconNode: Blockbench.getIconNode,
|
||||
pureMarked,
|
||||
tl
|
||||
},
|
||||
mount_directly: true,
|
||||
template: `
|
||||
<div style="margin-top: 10px;">
|
||||
<div class="bar">
|
||||
<div class="tab_bar">
|
||||
<div :class="{open: tab == 'installed'}" @click="tab = 'installed'">${tl('dialog.plugins.installed')}</div>
|
||||
<div :class="{open: tab == 'available'}" @click="tab = 'available'">${tl('dialog.plugins.available')}</div>
|
||||
<content style="display: flex;" class="dialog_content">
|
||||
<div id="plugin_browser_sidebar" v-show="!isMobile || !selected_plugin">
|
||||
<div class="bar flex" id="plugins_list_main_bar">
|
||||
<div class="tool" v-if="!isMobile" @click="selected_plugin = null"><i class="material-icons icon">home</i></div>
|
||||
<search-bar id="plugin_search_bar" v-model="search_term" @input="setPage(0)"></search-bar>
|
||||
</div>
|
||||
<search-bar id="plugin_search_bar" v-model="search_term"></search-bar>
|
||||
<div class="tab_bar">
|
||||
<div :class="{open: tab == 'installed'}" @click="setTab('installed')">${tl('dialog.plugins.installed')}</div>
|
||||
<div :class="{open: tab == 'available'}" @click="setTab('available')">${tl('dialog.plugins.available')}</div>
|
||||
</div>
|
||||
<ul class="list" id="plugin_list">
|
||||
<li v-for="plugin in viewed_plugins" :plugin="plugin.id" :class="{plugin: true, testing: plugin.fromFile, selected: plugin == selected_plugin, disabled_plugin: plugin.disabled, incompatible: plugin.isInstallable() !== true}" @click="selectPlugin(plugin)">
|
||||
<div>
|
||||
<div class="plugin_icon_area">
|
||||
<img v-if="plugin.hasImageIcon()" :src="plugin.getIcon()" width="48" height="48px" />
|
||||
<dynamic-icon v-else :icon="plugin.icon" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="title">{{ plugin.title || plugin.id }}</div>
|
||||
<div class="author">{{ tl('dialog.plugins.author', [plugin.author]) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="description">{{ plugin.description }}</div>
|
||||
<ul class="plugin_tag_list">
|
||||
<li v-for="tag in plugin.tags" :class="getTagClass(tag)" :key="tag" @click="search_term = tag;">{{tag}}</li>
|
||||
</ul>
|
||||
</li>
|
||||
<div class="no_plugin_message tl" v-if="plugin_search.length < 1 && tab === 'installed'">${tl('dialog.plugins.none_installed')}</div>
|
||||
<div class="no_plugin_message tl" v-if="plugin_search.length < 1 && tab === 'available'" id="plugin_available_empty">{{ tl(navigator.onLine ? 'dialog.plugins.none_available' : 'dialog.plugins.offline') }}</div>
|
||||
</ul>
|
||||
<ol class="pagination_numbers" v-if="pages.length > 1">
|
||||
<li v-for="number in pages" :class="{selected: page == number}" @click="setPage(number)">{{ number+1 }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<ul class="list" id="plugin_list">
|
||||
<li v-for="plugin in plugin_search" v-bind:plugin="plugin.id" v-bind:class="{plugin: true, testing: plugin.fromFile, expanded: plugin.expanded, has_about_text: !!plugin.about}">
|
||||
<div class="title" v-on:click="plugin.toggleInfo()">
|
||||
<div class="icon_wrapper plugin_icon normal" v-html="getIconNode(plugin.icon || 'error_outline', plugin.icon ? plugin.color : 'var(--color-close)').outerHTML"></div>
|
||||
|
||||
<i v-if="plugin.expanded" class="material-icons plugin_expand_icon">expand_less</i>
|
||||
<i v-else class="material-icons plugin_expand_icon">expand_more</i>
|
||||
{{ plugin.title || plugin.id }}
|
||||
|
||||
<div id="plugin_browser_page" v-if="selected_plugin">
|
||||
<div v-if="isMobile" @click="selected_plugin = null;">Back to Overview todo</div>
|
||||
<div class="plugin_browser_page_header" :class="{disabled_plugin: selected_plugin.disabled}">
|
||||
<div class="plugin_icon_area">
|
||||
<img v-if="selected_plugin.hasImageIcon()" :src="selected_plugin.getIcon()" width="48" height="48px" />
|
||||
<dynamic-icon v-else :icon="selected_plugin.icon" />
|
||||
</div>
|
||||
<div class="plugin_version">{{ plugin.version }}</div>
|
||||
<div class="button_bar" v-if="plugin.installed || plugin.isInstallable() == true">
|
||||
<button type="button" class="" v-on:click="plugin.uninstall()" v-if="plugin.installed"><i class="material-icons">delete</i><span>${tl('dialog.plugins.uninstall')}</span></button>
|
||||
<button type="button" class="" v-on:click="plugin.install()" v-else><i class="material-icons">add</i><span>${tl('dialog.plugins.install')}</span></button>
|
||||
<button type="button" v-on:click="plugin.reload()" v-if="plugin.installed && plugin.isReloadable()"><i class="material-icons">refresh</i><span>${tl('dialog.plugins.reload')}</span></button>
|
||||
<div>
|
||||
<h1>
|
||||
{{ selected_plugin.title || selected_plugin.id }}
|
||||
<div class="version">v{{ selected_plugin.version }}</div>
|
||||
</h1>
|
||||
<div class="author">
|
||||
{{ tl('dialog.plugins.author', [selected_plugin.author]) }}
|
||||
<div v-if="selected_plugin.disabled" class="plugin_disabled_tag">🌙 ${tl('dialog.plugins.is_disabled')}</div>
|
||||
<div v-else-if="selected_plugin.installed" class="plugin_installed_tag">✓ ${tl('dialog.plugins.is_installed')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button_bar tiny" v-if="plugin.isInstallable() != true">{{ plugin.isInstallable() }}</div>
|
||||
</div>
|
||||
|
||||
<div class="author">{{ tl('dialog.plugins.author', [plugin.author]) }}</div>
|
||||
<div class="description">{{ plugin.description }}</div>
|
||||
<div v-if="plugin.expanded" class="about markdown" v-html="pureMarked(plugin.about.replace(/\\n/g, '\\n\\n'))"><button>a</button></div>
|
||||
<div v-if="plugin.expanded" v-on:click="plugin.toggleInfo()" style="text-decoration: underline;">${tl('dialog.plugins.show_less')}</div>
|
||||
<ul class="plugin_tag_list">
|
||||
<li v-for="tag in plugin.tags" :class="getTagClass(tag)" :key="tag" @click="search_term = tag;">{{tag}}</li>
|
||||
<div class="button_bar" v-if="selected_plugin.installed || selected_plugin.isInstallable() == true">
|
||||
<button type="button" v-if="selected_plugin.installed && selected_plugin.source != 'store'" @click="selected_plugin.toggleDisabled()">
|
||||
<i class="material-icons icon">bedtime</i>
|
||||
<span>{{ selected_plugin.disabled ? '${tl('dialog.plugins.enable')}' : '${tl('dialog.plugins.disable')}' }}</span>
|
||||
</button>
|
||||
<button type="button" @click="selected_plugin.reload()" v-if="selected_plugin.installed && selected_plugin.isReloadable()">
|
||||
<i class="material-icons icon">refresh</i>
|
||||
<span>${tl('dialog.plugins.reload')}</span>
|
||||
</button>
|
||||
<button type="button" class="" @click="selected_plugin.uninstall()" v-if="selected_plugin.installed">
|
||||
<i class="material-icons icon">delete</i>
|
||||
<span>${tl('dialog.plugins.uninstall')}</span>
|
||||
</button>
|
||||
<button type="button" class="" @click="selected_plugin.install()" v-else>
|
||||
<i class="material-icons icon">add</i>
|
||||
<span>${tl('dialog.plugins.install')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul class="plugin_tag_list">
|
||||
<li v-for="tag in selected_plugin.tags" :class="getTagClass(tag)" :key="tag" @click="search_term = tag;">{{tag}}</li>
|
||||
</ul>
|
||||
|
||||
<div class="description" :class="{disabled_plugin: selected_plugin.disabled}">{{ selected_plugin.description }}</div>
|
||||
|
||||
<div class="button_bar tiny plugin_compatibility_issue" v-if="selected_plugin.isInstallable() != true">
|
||||
<i class="material-icons icon">error</i>
|
||||
{{ selected_plugin.isInstallable() }}
|
||||
</div>
|
||||
|
||||
<h2 v-if="selected_plugin.about" style="margin-top: 36px;">About</h2>
|
||||
<dynamic-icon v-else-if="!selected_plugin.hasImageIcon()" :icon="selected_plugin.icon" id="plugin_page_background_decoration" />
|
||||
<div class="about markdown" v-if="selected_plugin.about" v-html="formatAbout(selected_plugin.about)"></div>
|
||||
</div>
|
||||
|
||||
<div id="plugin_browser_start_page" v-if="!selected_plugin && !isMobile">
|
||||
<h1>Blockbench Plugins</h1>
|
||||
<img src="./assets/plugins.png" />
|
||||
<p>Plugins allow you to configure Blockbench beyond the default capabilities. Select from a list of 100 community created plugins.</p>
|
||||
<p>Want to write your own plugin? Check out the <a href="https://www.blockbench.net/wiki/docs/plugin" target="_blank">Plugin Documentation</a>.</p>
|
||||
|
||||
<div v-for="row in suggested_rows" class="plugins_suggested_row">
|
||||
<h3>{{row.title}}</h3>
|
||||
<ul>
|
||||
<li v-for="plugin in row.plugins" @click="selectPlugin(plugin)">
|
||||
<div class="plugin_icon_area">
|
||||
<img v-if="plugin.hasImageIcon()" :src="plugin.getIcon()" width="48" height="48px" />
|
||||
<dynamic-icon v-else :icon="plugin.icon" />
|
||||
</div>
|
||||
<div class="title"><span>{{ plugin.title || plugin.id }}</span></div>
|
||||
<div class="author">{{ plugin.author }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<div class="no_plugin_message tl" v-if="plugin_search.length < 1 && tab === 'installed'">${tl('dialog.plugins.none_installed')}</div>
|
||||
<div class="no_plugin_message tl" v-if="plugin_search.length < 1 && tab === 'available'" id="plugin_available_empty">{{ tl(navigator.onLine ? 'dialog.plugins.none_available' : 'dialog.plugins.offline') }}</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</content>
|
||||
`
|
||||
}
|
||||
})
|
||||
|
||||
let actions_setup = false;
|
||||
new Action('plugins_window', {
|
||||
icon: 'extension',
|
||||
category: 'blockbench',
|
||||
@ -612,13 +820,11 @@ BARS.defineActions(function() {
|
||||
Plugins.dialog.show();
|
||||
let none_installed = !Plugins.all.find(plugin => plugin.installed);
|
||||
if (none_installed) Plugins.dialog.content_vue.tab = 'available';
|
||||
if (!Plugins.dialog.button_bar) {
|
||||
Plugins.dialog.button_bar = Interface.createElement('div', {class: 'bar next_to_title', id: 'plugins_header_bar'});
|
||||
Plugins.dialog.object.firstElementChild.after(Plugins.dialog.button_bar);
|
||||
BarItems.load_plugin.toElement('#plugins_header_bar');
|
||||
BarItems.load_plugin_from_url.toElement('#plugins_header_bar');
|
||||
if (!actions_setup) {
|
||||
BarItems.load_plugin.toElement('#plugins_list_main_bar');
|
||||
BarItems.load_plugin_from_url.toElement('#plugins_list_main_bar');
|
||||
actions_setup = true;
|
||||
}
|
||||
$('#plugin_list').css('max-height', limitNumber(window.innerHeight-226, 80, 800)+'px');
|
||||
$('dialog#plugins #plugin_search_bar input').trigger('focus')
|
||||
}
|
||||
})
|
||||
|
File diff suppressed because one or more lines are too long
@ -525,7 +525,10 @@
|
||||
"dialog.plugins.web_only": "Only for the web app",
|
||||
"dialog.plugins.app_only": "Only for the desktop app",
|
||||
"dialog.plugins.author": "by %0",
|
||||
"dialog.plugins.show_less": "Show Less",
|
||||
"dialog.plugins.is_installed": "Installed",
|
||||
"dialog.plugins.is_disabled": "Disabled",
|
||||
"dialog.plugins.disable": "Disable",
|
||||
"dialog.plugins.enable": "Enable",
|
||||
|
||||
"dialog.load_plugins_from_query.title": "Load Plugins",
|
||||
"dialog.load_plugins_from_query.text": "You used a link that requires plugins to be installed. Would you like to install them?",
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Blockbench",
|
||||
"description": "Low-poly modeling and animation software",
|
||||
"version": "4.7.4",
|
||||
"version": "4.8.0",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"author": {
|
||||
"name": "JannisX11",
|
||||
|
Loading…
x
Reference in New Issue
Block a user