diff --git a/package-lock.json b/package-lock.json index 749193635..3ef1b57b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bridge", - "version": "2.3.3", + "version": "2.3.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "bridge", - "version": "2.3.3", + "version": "2.3.5", "dependencies": { "@mdi/font": "^6.9.96", "@types/lz-string": "^1.3.34", @@ -19,7 +19,7 @@ "comlink": "^4.3.0", "compare-versions": "^3.6.0", "core-js": "^3.6.5", - "dash-compiler": "^0.10.5", + "dash-compiler": "^0.10.6", "escape-string-regexp": "^5.0.0", "fflate": "^0.6.7", "idb-keyval": "^5.1.3", @@ -28,7 +28,7 @@ "jsonc-parser": "^3.0.0", "lodash-es": "^4.17.20", "lz-string": "^1.4.4", - "mc-project-core": "^0.3.21", + "mc-project-core": "^0.3.22", "molang": "^1.13.11", "monaco-editor": "^0.33.0", "path-browserify": "^1.0.1", @@ -1843,6 +1843,30 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.13", "dev": true, @@ -2345,6 +2369,18 @@ "node": ">=0.10.0" } }, + "node_modules/acorn": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "8.11.0", "dev": true, @@ -2588,9 +2624,10 @@ } }, "node_modules/buffer-from": { - "version": "1.1.1", - "dev": true, - "license": "MIT" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true }, "node_modules/builtin-modules": { "version": "3.3.0", @@ -2873,16 +2910,16 @@ "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==" }, "node_modules/dash-compiler": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/dash-compiler/-/dash-compiler-0.10.5.tgz", - "integrity": "sha512-WXL/Nng2CQ9b2kzvJoydoK4BhjyR2Fj1x8xYAxDtDCLk0JZ2IlR+j+Sm1vMFm1ADQWhhHErj4ENSST+iyw2l8w==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/dash-compiler/-/dash-compiler-0.10.6.tgz", + "integrity": "sha512-rRYKk7x9OytFmqbUA4Jl34ov7BrWfCQwnRBJLJ4oLLqYKunpyXNzpCoDiGP8tqEQQkMtDhv410TSMwzYGx5ZrQ==", "dependencies": { "@swc/wasm-web": "^1.2.218", "bridge-common-utils": "^0.3.0", "bridge-js-runtime": "^0.3.7", "is-glob": "^4.0.3", "json5": "^2.2.0", - "mc-project-core": "^0.3.21", + "mc-project-core": "^0.3.22", "micromatch": "^4.0.4", "molang": "^1.13.11", "path-browserify": "^1.0.1" @@ -4527,7 +4564,9 @@ } }, "node_modules/mc-project-core": { - "version": "0.3.21", + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/mc-project-core/-/mc-project-core-0.3.22.tgz", + "integrity": "sha512-WTjSut6x+f3xTmVr0P75uldgezjTIRO2h/5SQoFBuKZT8XzphEtLBmdiBfNobAz5vsOaCLEuiBkL++DXe3x6Og==", "dependencies": { "bridge-common-utils": "^0.3.3", "json5": "^2.2.0", @@ -5103,9 +5142,10 @@ } }, "node_modules/source-map-support": { - "version": "0.5.19", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, - "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -5113,8 +5153,9 @@ }, "node_modules/source-map-support/node_modules/source-map": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -5287,13 +5328,15 @@ } }, "node_modules/terser": { - "version": "5.7.1", + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", + "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.7.2", - "source-map-support": "~0.5.19" + "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" @@ -5302,14 +5345,6 @@ "node": ">=10" } }, - "node_modules/terser/node_modules/source-map": { - "version": "0.7.3", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 8" - } - }, "node_modules/tga-js": { "version": "1.1.1", "license": "MIT" @@ -7192,6 +7227,29 @@ "version": "1.1.1", "dev": true }, + "@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + } + } + }, "@jridgewell/sourcemap-codec": { "version": "1.4.13", "dev": true @@ -7549,6 +7607,12 @@ } } }, + "acorn": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "dev": true + }, "ajv": { "version": "8.11.0", "dev": true, @@ -7712,7 +7776,9 @@ } }, "buffer-from": { - "version": "1.1.1", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, "builtin-modules": { @@ -7875,16 +7941,16 @@ "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==" }, "dash-compiler": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/dash-compiler/-/dash-compiler-0.10.5.tgz", - "integrity": "sha512-WXL/Nng2CQ9b2kzvJoydoK4BhjyR2Fj1x8xYAxDtDCLk0JZ2IlR+j+Sm1vMFm1ADQWhhHErj4ENSST+iyw2l8w==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/dash-compiler/-/dash-compiler-0.10.6.tgz", + "integrity": "sha512-rRYKk7x9OytFmqbUA4Jl34ov7BrWfCQwnRBJLJ4oLLqYKunpyXNzpCoDiGP8tqEQQkMtDhv410TSMwzYGx5ZrQ==", "requires": { "@swc/wasm-web": "^1.2.218", "bridge-common-utils": "^0.3.0", "bridge-js-runtime": "^0.3.7", "is-glob": "^4.0.3", "json5": "^2.2.0", - "mc-project-core": "^0.3.21", + "mc-project-core": "^0.3.22", "micromatch": "^4.0.4", "molang": "^1.13.11", "path-browserify": "^1.0.1" @@ -8768,7 +8834,9 @@ } }, "mc-project-core": { - "version": "0.3.21", + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/mc-project-core/-/mc-project-core-0.3.22.tgz", + "integrity": "sha512-WTjSut6x+f3xTmVr0P75uldgezjTIRO2h/5SQoFBuKZT8XzphEtLBmdiBfNobAz5vsOaCLEuiBkL++DXe3x6Og==", "requires": { "bridge-common-utils": "^0.3.3", "json5": "^2.2.0", @@ -9128,7 +9196,9 @@ "version": "1.0.2" }, "source-map-support": { - "version": "0.5.19", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "requires": { "buffer-from": "^1.0.0", @@ -9137,6 +9207,8 @@ "dependencies": { "source-map": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true } } @@ -9229,18 +9301,15 @@ } }, "terser": { - "version": "5.7.1", + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", + "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", "dev": true, "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.7.2", - "source-map-support": "~0.5.19" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "dev": true - } + "source-map-support": "~0.5.20" } }, "tga-js": { diff --git a/package.json b/package.json index 966c0f681..a571b6490 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bridge", - "version": "2.3.5", + "version": "2.3.6", "private": true, "scripts": { "dev": "vite", @@ -23,7 +23,7 @@ "comlink": "^4.3.0", "compare-versions": "^3.6.0", "core-js": "^3.6.5", - "dash-compiler": "^0.10.5", + "dash-compiler": "^0.10.6", "escape-string-regexp": "^5.0.0", "fflate": "^0.6.7", "idb-keyval": "^5.1.3", @@ -32,7 +32,7 @@ "jsonc-parser": "^3.0.0", "lodash-es": "^4.17.20", "lz-string": "^1.4.4", - "mc-project-core": "^0.3.21", + "mc-project-core": "^0.3.22", "molang": "^1.13.11", "monaco-editor": "^0.33.0", "path-browserify": "^1.0.1", diff --git a/public/packages.zip b/public/packages.zip index 75825bdba..aeea8efc2 100644 Binary files a/public/packages.zip and b/public/packages.zip differ diff --git a/src/components/Actions/KeyBindingManager.ts b/src/components/Actions/KeyBindingManager.ts index 46e1018fa..b4b6a8578 100644 --- a/src/components/Actions/KeyBindingManager.ts +++ b/src/components/Actions/KeyBindingManager.ts @@ -5,12 +5,22 @@ import { del, set, shallowReactive } from 'vue' const IGNORE_KEYS = ['Control', 'Alt', 'Meta'] +interface IKeyEvent { + key: string + altKey: boolean + ctrlKey: boolean + shiftKey: boolean + metaKey: boolean + target: EventTarget | null + preventDefault: () => void + stopImmediatePropagation: () => void +} export class KeyBindingManager { protected state: Record = shallowReactive({}) protected lastTimeStamp = 0 - protected onKeydown = (event: KeyboardEvent) => { - const { key, ctrlKey, altKey, metaKey, shiftKey, code } = event + protected onKeydown = (event: IKeyEvent) => { + const { key, ctrlKey, altKey, metaKey, shiftKey } = event if (IGNORE_KEYS.includes(key)) return const keyCode = toStrKeyCode({ @@ -32,10 +42,47 @@ export class KeyBindingManager { keyBinding.trigger() } } + protected onMouseDown = (event: MouseEvent) => { + let buttonName = null + switch (event.button) { + case 0: + buttonName = 'Left' + break + case 1: + buttonName = 'Middle' + break + case 2: + buttonName = 'Right' + break + case 3: + buttonName = 'Back' + break + case 4: + buttonName = 'Forward' + break + default: + console.error(`Unknown mouse button: ${event.button}`) + } + if (!buttonName) return + + this.onKeydown({ + key: `mouse${buttonName}`, + ctrlKey: event.ctrlKey, + altKey: event.altKey, + metaKey: event.metaKey, + shiftKey: event.shiftKey, + target: event.target, + preventDefault: () => event.preventDefault(), + stopImmediatePropagation: () => event.stopImmediatePropagation(), + }) + } constructor(protected element: HTMLDivElement | Document = document) { // @ts-ignore TypeScript isn't smart enough to understand that the type "KeyboardEvent" is correct element.addEventListener('keydown', this.onKeydown) + + // @ts-ignore TypeScript isn't smart enough to understand that the type "MouseEvent" is correct + element.addEventListener('mousedown', this.onMouseDown) } create(keyBindingConfig: IKeyBindingConfig) { diff --git a/src/components/Compiler/Sidebar/create.ts b/src/components/Compiler/Sidebar/create.ts index 3196ba889..42f9d647f 100644 --- a/src/components/Compiler/Sidebar/create.ts +++ b/src/components/Compiler/Sidebar/create.ts @@ -58,6 +58,13 @@ export function createCompilerSidebar() { displayName: 'sidebar.compiler.name', icon: 'mdi-cogs', disabled: () => App.instance.isNoProjectSelected, + /** + * The compiler window is doing more harm than good on mobile (confusion with app settings) so + * we are now disabling it by default. + * Additionally, manual production builds are also pretty much useless as they are internal to bridge. and can only be + * accessed over the "Open Project Folder" button within the project explorer context menu + */ + defaultVisibility: !App.instance.mobile.isCurrentDevice(), onClick: async () => { const app = await App.getApp() const compilerWindow = app.windows.compilerWindow diff --git a/src/components/Editors/EntityModel/Tab.ts b/src/components/Editors/EntityModel/Tab.ts index 338d71cdb..4e824f402 100644 --- a/src/components/Editors/EntityModel/Tab.ts +++ b/src/components/Editors/EntityModel/Tab.ts @@ -7,42 +7,57 @@ import json5 from 'json5' import { FileWatcher } from '/@/components/FileSystem/FileWatcher' import { InformationWindow } from '../../Windows/Common/Information/InformationWindow' import { markRaw } from 'vue' +import { DropdownWindow } from '../../Windows/Common/Dropdown/DropdownWindow' export interface IPreviewOptions { + clientEntityFilePath?: string loadServerEntity?: boolean + geometryFilePath?: string + geometryIdentifier?: string } export class EntityModelTab extends GeometryPreviewTab { protected previewOptions: IPreviewOptions = {} - protected clientEntityWatcher: FileWatcher + protected clientEntityWatcher?: FileWatcher - constructor( - protected clientEntityFilePath: string, - tab: FileTab, - parent: TabSystem - ) { + constructor(options: IPreviewOptions, tab: FileTab, parent: TabSystem) { super(tab, parent) - this.clientEntityWatcher = new FileWatcher( - App.instance, - clientEntityFilePath - ) + this.previewOptions = options + + if (this.clientEntityFilePath) { + this.clientEntityWatcher = new FileWatcher( + App.instance, + this.clientEntityFilePath + ) - this.clientEntityWatcher.on((file) => this.reload(file)) + this.clientEntityWatcher.on((file) => this.reload(file)) + } else { + this.reload() + } } setPreviewOptions(previewOptions: IPreviewOptions) { - this.previewOptions = previewOptions + this.previewOptions = Object.assign(this.previewOptions, previewOptions) + } + get clientEntityFilePath() { + return this.previewOptions.clientEntityFilePath + } + get geometryFilePath() { + return this.previewOptions.geometryFilePath + } + get geometryIdentifier() { + return this.previewOptions.geometryIdentifier } async close() { const didClose = await super.close() - if (didClose) this.clientEntityWatcher.dispose() + if (didClose) this.clientEntityWatcher?.dispose() return didClose } async reload(file?: File) { - if (!file) file = await this.clientEntityWatcher.getFile() + if (!file) file = await this.clientEntityWatcher?.getFile() const runningAnims = this._renderContainer?.runningAnimations @@ -51,7 +66,7 @@ export class EntityModelTab extends GeometryPreviewTab { this.loadRenderContainer(file, runningAnims) } - async loadRenderContainer(file: File, runningAnims = new Set()) { + async loadRenderContainer(file?: File, runningAnims = new Set()) { if (this._renderContainer !== undefined) return await this.setupComplete const app = await App.getApp() @@ -59,6 +74,9 @@ export class EntityModelTab extends GeometryPreviewTab { const packIndexer = app.project.packIndexer.service if (!packIndexer) return + // No client entity connected, try to load from geometry only + if (file === undefined) return await this.fallbackToOnlyGeometry() + let clientEntity: any try { clientEntity = @@ -161,4 +179,90 @@ export class EntityModelTab extends GeometryPreviewTab { this.createModel() }) } + + /** + * Store chosen fallback texture to avoid showing the texture picker again upon reload + */ + protected chosenFallbackTexturePath?: string + /** + * This function enables bridge. to display models without a client entity. + * By default, bridge. will use the geometry file path and identifier as the geometry source and + * the user is prompted to choose any entity/block texture file for the model + */ + async fallbackToOnlyGeometry() { + // No geometry file connected, no way to fallback to geometry only + if (!this.geometryFilePath || !this.geometryIdentifier) return + + const app = await App.getApp() + const packIndexer = app.project.packIndexer.service + + // Helper method for loading all textures from a specific textures/ subfolder + const loadTextures = (location: 'entity' | 'blocks') => + app.fileSystem + .readdir( + app.project.config.resolvePackPath( + 'resourcePack', + `textures/${location}` + ) + ) + .then((path) => + path.map((path) => ({ + text: `textures/${location}/${path}`, + value: app.project.config.resolvePackPath( + 'resourcePack', + `textures/${location}/${path}` + ), + })) + ) + .catch(() => <{ text: string; value: string }[]>[]) + + if (this.chosenFallbackTexturePath === undefined) { + // Load all textures from the entity and blocks folders + const textures = (await loadTextures('entity')).concat( + await loadTextures('blocks') + ) + + // Prompt user to select a texture + const choiceWindow = new DropdownWindow({ + options: textures, + name: 'preview.chooseTexture', + }) + + // Get selected texture + this.chosenFallbackTexturePath = await choiceWindow.fired + } + + // Load all available animations (file paths & identifiers) + const allAnimations = await packIndexer.getAllFiles('clientAnimation') + const allAnimationIdentifiers = await packIndexer.getCacheDataFor( + 'clientAnimation', + undefined, + 'identifier' + ) + + // Create fallback render container + this._renderContainer = markRaw( + new RenderDataContainer(app, { + identifier: this.geometryIdentifier, + texturePaths: [this.chosenFallbackTexturePath], + connectedAnimations: new Set(allAnimationIdentifiers), + }) + ) + + this._renderContainer.createGeometry(this.geometryFilePath) + + // Create animations so they are ready to be used within the render container + allAnimations.forEach((filePath) => + this._renderContainer!.createAnimation(filePath) + ) + + // Once the renderContainer is ready loading, create the initial model... + this.renderContainer.ready.then(() => { + this.createModel() + }) + // ...and listen to further changes to the files for hot-reloading + this._renderContainer.on(() => { + this.createModel() + }) + } } diff --git a/src/components/Editors/EntityModel/create/fromClientEntity.ts b/src/components/Editors/EntityModel/create/fromClientEntity.ts index 8d52495aa..cd3685bd4 100644 --- a/src/components/Editors/EntityModel/create/fromClientEntity.ts +++ b/src/components/Editors/EntityModel/create/fromClientEntity.ts @@ -6,5 +6,9 @@ export async function createFromClientEntity( tabSystem: TabSystem, tab: FileTab ) { - return new EntityModelTab(tab.getPath(), tab, tabSystem) + return new EntityModelTab( + { clientEntityFilePath: tab.getPath() }, + tab, + tabSystem + ) } diff --git a/src/components/Editors/EntityModel/create/fromEntity.ts b/src/components/Editors/EntityModel/create/fromEntity.ts index 91421315b..1fd3efe8c 100644 --- a/src/components/Editors/EntityModel/create/fromEntity.ts +++ b/src/components/Editors/EntityModel/create/fromEntity.ts @@ -33,7 +33,11 @@ export async function createFromEntity(tabSystem: TabSystem, tab: FileTab) { return } - const previewTab = new EntityModelTab(clientEntity[0], tab, tabSystem) + const previewTab = new EntityModelTab( + { clientEntityFilePath: clientEntity[0] }, + tab, + tabSystem + ) previewTab.setPreviewOptions({ loadServerEntity: true }) return previewTab diff --git a/src/components/Editors/EntityModel/create/fromGeometry.ts b/src/components/Editors/EntityModel/create/fromGeometry.ts index 02f347fe7..b54969433 100644 --- a/src/components/Editors/EntityModel/create/fromGeometry.ts +++ b/src/components/Editors/EntityModel/create/fromGeometry.ts @@ -55,17 +55,26 @@ export async function createFromGeometry(tabSystem: TabSystem, tab: FileTab) { const block = await packIndexer.find('block', 'geometryIdentifier', [ choice, ]) + // Connected block found if (block.length > 0) { const previewTab = new BlockModelTab(block[0], tab, tabSystem) previewTab.setPreviewOptions({ loadComponents: false }) return previewTab } - - new InformationWindow({ - description: 'preview.failedClientEntityLoad', - }) - return } - return new EntityModelTab(clientEntity[0], tab, tabSystem) + /** + * If we reach this point either... + * - ...a connected client entity was found (clientEntity.length > 0)... + * - ...or no connected client entity and no connected block was found -> Fallback to geometry preview in this case. + */ + return new EntityModelTab( + { + clientEntityFilePath: clientEntity[0], + geometryFilePath: tab.getPath(), + geometryIdentifier: choice, + }, + tab, + tabSystem + ) } diff --git a/src/components/Editors/Sound/SoundTab.ts b/src/components/Editors/Sound/SoundTab.ts new file mode 100644 index 000000000..b8b69eaf5 --- /dev/null +++ b/src/components/Editors/Sound/SoundTab.ts @@ -0,0 +1,109 @@ +import { FileTab, TReadOnlyMode } from '/@/components/TabSystem/FileTab' +import { loadHandleAsDataURL } from '/@/utils/loadAsDataUrl' +import SoundTabComponent from './SoundTab.vue' +import { AnyFileHandle } from '../../FileSystem/Types' +import { addDisposableEventListener } from '/@/utils/disposableListener' +import { IDisposable } from '/@/types/disposable' + +export class SoundTab extends FileTab { + component = SoundTabComponent + dataUrl?: string = undefined + + audio: HTMLAudioElement | null = null + intervalId: number | null = null + currentTime = 0 + timeTriggeredManually = false + isPlaying = false + loadedAudioMetadata = false + audioShouldLoop = false + disposables: IDisposable[] = [] + + get icon() { + return 'mdi-file-music-outline' + } + get iconColor() { + return 'resourcePack' + } + + static is(fileHandle: AnyFileHandle) { + const fileName = fileHandle.name + return fileName.endsWith('.mp3') || fileName.endsWith('.ogg') + } + + setReadOnly(val: TReadOnlyMode) { + this.readOnlyMode = val + } + + // Tab events + async setup() { + this.dataUrl = await loadHandleAsDataURL(this.fileHandle) + this.audio = document.createElement('audio') + if (!this.audio) return + + this.audio.preload = 'metadata' + this.audio.loop = this.audioShouldLoop + this.audio.src = this.dataUrl + + this.intervalId = window.setInterval( + () => this.updateCurrentTime(), + 100 + ) + + this.disposables = [ + addDisposableEventListener('play', () => this.onPlay(), this.audio), + addDisposableEventListener( + 'pause', + () => this.onPause(), + this.audio + ), + addDisposableEventListener( + 'loadedmetadata', + () => this.onLoadedMetadata(), + this.audio + ), + ] + + await super.setup() + } + onDestroy() { + if (this.intervalId) window.clearInterval(this.intervalId) + this.intervalId = null + + this.disposables.forEach((disposable) => disposable.dispose()) + this.disposables = [] + this.audio = null + } + + // Sound element events + onPlay() { + this.isPlaying = true + } + onPause() { + this.isPlaying = false + } + onLoadedMetadata() { + this.loadedAudioMetadata = true + } + + updateCurrentTime() { + this.timeTriggeredManually = false + this.currentTime = this.audio?.currentTime ?? 0 + } + toggleAudioLoop() { + this.audioShouldLoop = !this.audioShouldLoop + if (this.audio) this.audio.loop = this.audioShouldLoop + } + setCurrentTime(time: number) { + if (!this.audio) return + + if (!this.timeTriggeredManually) { + this.timeTriggeredManually = true + return + } + if (Number.isNaN(time)) return + + this.audio.currentTime = Math.round(time * 100) / 100 + } + + _save() {} +} diff --git a/src/components/Editors/Sound/SoundTab.vue b/src/components/Editors/Sound/SoundTab.vue new file mode 100644 index 000000000..bce13148c --- /dev/null +++ b/src/components/Editors/Sound/SoundTab.vue @@ -0,0 +1,105 @@ + + + diff --git a/src/components/FileSystem/saveOrDownload.ts b/src/components/FileSystem/saveOrDownload.ts index b287cc446..b9e6a38af 100644 --- a/src/components/FileSystem/saveOrDownload.ts +++ b/src/components/FileSystem/saveOrDownload.ts @@ -12,7 +12,7 @@ export async function saveOrDownload( fileSystem: FileSystem ) { const notification = createNotification({ - icon: 'mdi-export', + icon: 'mdi-download', color: 'success', textColor: 'white', message: 'general.successfulExport.title', diff --git a/src/components/ImportFile/BBModel.ts b/src/components/ImportFile/BBModel.ts index 455cf2ed7..c5c13fb15 100644 --- a/src/components/ImportFile/BBModel.ts +++ b/src/components/ImportFile/BBModel.ts @@ -494,7 +494,10 @@ export class BBModelImporter extends FileImporter { anim.timeline![this.getTimecodeString(kf.time)] = this.compileBedrockKeyframe(kf, animator) }) - } else if (animator.type === 'bone') { + } else if ( + animator.type === 'bone' || + animator.type === undefined // No defined type: Default is type "bone" + ) { let bone_tag: any = (anim.bones![animator.name] = {}) let channels: any = {} diff --git a/src/components/ImportFile/ZipImporter.ts b/src/components/ImportFile/ZipImporter.ts index e9f202fd2..e4e74ed59 100644 --- a/src/components/ImportFile/ZipImporter.ts +++ b/src/components/ImportFile/ZipImporter.ts @@ -35,11 +35,11 @@ export class ZipImporter extends FileImporter { (await fs.directoryExists('import/projects')) || (await fs.directoryExists('import/extensions')) ) { - await importFromBrproject(fileHandle, false, false) + await importFromBrproject(fileHandle, false) } else { for await (const pack of tmpHandle.values()) { if (await fs.fileExists(`import/${pack.name}/manifest.json`)) { - await importFromMcaddon(fileHandle, false, false) + await importFromMcaddon(fileHandle, false) break } } diff --git a/src/components/Projects/Export/AsBrproject.ts b/src/components/Projects/Export/AsBrproject.ts index cc7ac811f..8f78220d7 100644 --- a/src/components/Projects/Export/AsBrproject.ts +++ b/src/components/Projects/Export/AsBrproject.ts @@ -7,6 +7,16 @@ export async function exportAsBrproject(name?: string) { const app = App.instance app.windows.loadingWindow.open() + const savePath = `${app.project.projectPath}/builds/${ + name ?? app.project.name + }.brproject` + + /** + * Make sure to delete old export so the .brproject file doesn't include itself + * This would cause an issue where the ZIP package keeps growing with every export + */ + await app.fileSystem.unlink(savePath) + /** * .brproject files come in two variants: * - Complete global package including the data/ & extensions/ folder for browsers using the file system polyfill @@ -17,9 +27,6 @@ export async function exportAsBrproject(name?: string) { ? app.fileSystem.baseDirectory : app.project.baseDirectory ) - const savePath = `${app.project.projectPath}/builds/${ - name ?? app.project.name - }.brproject` try { await saveOrDownload( diff --git a/src/components/Projects/Import/fromBrproject.ts b/src/components/Projects/Import/fromBrproject.ts index 619591a3d..067df5af6 100644 --- a/src/components/Projects/Import/fromBrproject.ts +++ b/src/components/Projects/Import/fromBrproject.ts @@ -8,10 +8,10 @@ import { App } from '/@/App' import { basename } from '/@/utils/path' import { Project } from '../Project/Project' import { LocaleManager } from '../../Locales/Manager' +import { findSuitableFolderName } from '/@/utils/directory/findSuitableName' export async function importFromBrproject( fileHandle: AnyFileHandle, - isFirstImport = false, unzip = true ) { const app = await App.getApp() @@ -20,7 +20,7 @@ export async function importFromBrproject( create: true, }) - if (!isFirstImport) await app.projectManager.projectReady.fired + await app.projectManager.projectReady.fired // Unzip .brproject file, do not unzip if already unzipped if (unzip) { @@ -60,7 +60,7 @@ export async function importFromBrproject( } // Ask user whether he wants to save the current project if we are going to delete it later in the import process - if (isUsingFileSystemPolyfill.value && !isFirstImport) { + if (isUsingFileSystemPolyfill.value && !app.hasNoProjects) { const confirmWindow = new ConfirmationWindow({ description: 'windows.projectChooser.openNewProject.saveCurrentProject', @@ -72,17 +72,26 @@ export async function importFromBrproject( } } + const projectName = await findSuitableFolderName( + basename(fileHandle.name, '.brproject'), + await fs.getDirectoryHandle('projects') + ) + // Get the new project path const importProject = importFrom === 'import' - ? `projects/${basename(fileHandle.name, '.brproject')}` + ? `projects/${projectName}` : importFrom.replace('import/', '') // Move imported project to the user's project directory await fs.move(importFrom, importProject) // Get current project name let currentProject: Project | undefined - if (!isFirstImport) currentProject = app.project + if (!app.hasNoProjects) currentProject = app.project + + // Remove old project if browser is using fileSystem polyfill + if (isUsingFileSystemPolyfill.value && !app.hasNoProjects) + await app.projectManager.removeProject(currentProject!) // Add new project await app.projectManager.addProject( @@ -90,9 +99,5 @@ export async function importFromBrproject( true ) - // Remove old project if browser is using fileSystem polyfill - if (isUsingFileSystemPolyfill.value && !isFirstImport) - await app.projectManager.removeProject(currentProject!) - await fs.unlink('import') } diff --git a/src/components/Projects/Import/fromMcaddon.ts b/src/components/Projects/Import/fromMcaddon.ts index 42c911b3d..bc551afcf 100644 --- a/src/components/Projects/Import/fromMcaddon.ts +++ b/src/components/Projects/Import/fromMcaddon.ts @@ -13,10 +13,10 @@ import { defaultPackPaths } from '../Project/Config' import { InformationWindow } from '../../Windows/Common/Information/InformationWindow' import { basename } from '/@/utils/path' import { getPackId, IManifestModule } from '/@/utils/manifest/getPackId' +import { findSuitableFolderName } from '/@/utils/directory/findSuitableName' export async function importFromMcaddon( fileHandle: AnyFileHandle, - isFirstImport = false, unzip = true ) { const app = await App.getApp() @@ -25,7 +25,7 @@ export async function importFromMcaddon( create: true, }) - if (!isFirstImport) await app.projectManager.projectReady.fired + await app.projectManager.projectReady.fired // Unzip .mcaddon file if (unzip) { @@ -35,12 +35,13 @@ export async function importFromMcaddon( unzipper.createTask(app.taskManager) await unzipper.unzip(data) } - const projectName = fileHandle.name - .replace('.mcaddon', '') - .replace('.zip', '') + const projectName = await findSuitableFolderName( + fileHandle.name.replace('.mcaddon', '').replace('.zip', ''), + await fs.getDirectoryHandle('projects') + ) // Ask user whether they want to save the current project if we are going to delete it later in the import process - if (isUsingFileSystemPolyfill.value && !isFirstImport) { + if (isUsingFileSystemPolyfill.value && !app.hasNoProjects) { const confirmWindow = new ConfirmationWindow({ description: 'windows.projectChooser.openNewProject.saveCurrentProject', @@ -124,15 +125,15 @@ export async function importFromMcaddon( await fs.mkdir(`projects/${projectName}/.bridge/extensions`) await fs.mkdir(`projects/${projectName}/.bridge/compiler`) + // Remove old project if browser is using fileSystem polyfill + if (isUsingFileSystemPolyfill.value && !app.hasNoProjects) + await app.projectManager.removeProject(app.project) + // Add new project await app.projectManager.addProject( await fs.getDirectoryHandle(`projects/${projectName}`), true ) - // Remove old project if browser is using fileSystem polyfill - if (isUsingFileSystemPolyfill.value && !isFirstImport) - await app.projectManager.removeProject(app.project) - await fs.unlink('import') } diff --git a/src/components/Projects/Import/fromMcpack.ts b/src/components/Projects/Import/fromMcpack.ts index dee884810..066888b6c 100644 --- a/src/components/Projects/Import/fromMcpack.ts +++ b/src/components/Projects/Import/fromMcpack.ts @@ -12,10 +12,10 @@ import { FileSystem } from '/@/components/FileSystem/FileSystem' import { defaultPackPaths } from '../Project/Config' import { InformationWindow } from '../../Windows/Common/Information/InformationWindow' import { getPackId, IManifestModule } from '/@/utils/manifest/getPackId' +import { findSuitableFolderName } from '/@/utils/directory/findSuitableName' export async function importFromMcpack( fileHandle: AnyFileHandle, - isFirstImport = false, unzip = true ) { const app = await App.getApp() @@ -24,7 +24,7 @@ export async function importFromMcpack( create: true, }) - if (!isFirstImport) await app.projectManager.projectReady.fired + await app.projectManager.projectReady.fired // Unzip .mcpack file if (unzip) { @@ -34,12 +34,14 @@ export async function importFromMcpack( unzipper.createTask(app.taskManager) await unzipper.unzip(data) } - const projectName = fileHandle.name - .replace('.mcpack', '') - .replace('.zip', '') + // Make sure that we don't replace an existing project + const projectName = await findSuitableFolderName( + fileHandle.name.replace('.mcpack', '').replace('.zip', ''), + await fs.getDirectoryHandle('projects') + ) // Ask user whether they want to save the current project if we are going to delete it later in the import process - if (isUsingFileSystemPolyfill.value && !isFirstImport) { + if (isUsingFileSystemPolyfill.value && !app.hasNoProjects) { const confirmWindow = new ConfirmationWindow({ description: 'windows.projectChooser.openNewProject.saveCurrentProject', @@ -93,15 +95,15 @@ export async function importFromMcpack( await fs.mkdir(`projects/${projectName}/.bridge/extensions`) await fs.mkdir(`projects/${projectName}/.bridge/compiler`) + if (isUsingFileSystemPolyfill.value && !app.hasNoProjects) + // Remove old project if browser is using fileSystem polyfill + await app.projectManager.removeProject(app.project) + // Add new project await app.projectManager.addProject( await fs.getDirectoryHandle(`projects/${projectName}`), true ) - if (isUsingFileSystemPolyfill.value && !isFirstImport) - // Remove old project if browser is using fileSystem polyfill - await app.projectManager.removeProject(app.project) - await fs.unlink('import') } diff --git a/src/components/Sidebar/SidebarElement.ts b/src/components/Sidebar/SidebarElement.ts index f64a07469..d8d64742a 100644 --- a/src/components/Sidebar/SidebarElement.ts +++ b/src/components/Sidebar/SidebarElement.ts @@ -12,6 +12,10 @@ export interface ISidebar { displayName?: string group?: string isVisible?: boolean | (() => boolean) + /** + * Change the default visibility setting of the sidebar element + */ + defaultVisibility?: boolean component?: Component sidebarContent?: SidebarContent disabled?: () => boolean @@ -84,6 +88,9 @@ export class SidebarElement { return this.config.isVisible ?? !!this.isVisibleSetting } + get defaultVisibility() { + return this.config.defaultVisibility ?? true + } get icon() { return this.config.icon } diff --git a/src/components/Sidebar/setup.ts b/src/components/Sidebar/setup.ts index 5bbc02dd2..861649d6e 100644 --- a/src/components/Sidebar/setup.ts +++ b/src/components/Sidebar/setup.ts @@ -5,6 +5,7 @@ import { SettingsWindow } from '/@/components/Windows/Settings/SettingsWindow' import { isUsingFileSystemPolyfill } from '/@/components/FileSystem/Polyfill' import { createVirtualProjectWindow } from '/@/components/FileSystem/Virtual/ProjectWindow' import { createCompilerSidebar } from '../Compiler/Sidebar/create' +import { exportAsMcaddon } from '../Projects/Export/AsMcaddon' export async function setupSidebar() { createSidebar({ @@ -64,6 +65,23 @@ export async function setupSidebar() { createCompilerSidebar() + /** + * Enable one click exports of projects on mobile + * This should help users export projects faster + */ + createSidebar({ + id: 'quickExport', + displayName: 'sidebar.quickExport.name', + icon: 'mdi-export', + // Only show quick export option for devices on which com.mojang syncing is not available + defaultVisibility: isUsingFileSystemPolyfill.value, + disabled: () => App.instance.isNoProjectSelected, + + onClick: () => { + exportAsMcaddon() + }, + }) + createSidebar({ id: 'extensions', displayName: 'sidebar.extensions.name', @@ -77,7 +95,8 @@ export async function setupSidebar() { SettingsWindow.loadedSettings.once((settingsState) => { for (const sidebar of Object.values(App.sidebar.elements)) { sidebar.isVisibleSetting = - settingsState?.sidebar?.sidebarElements?.[sidebar.uuid] ?? true + settingsState?.sidebar?.sidebarElements?.[sidebar.uuid] ?? + sidebar.defaultVisibility } }) } diff --git a/src/components/TabSystem/CommonTab.ts b/src/components/TabSystem/CommonTab.ts index fbb1976fa..98135f238 100644 --- a/src/components/TabSystem/CommonTab.ts +++ b/src/components/TabSystem/CommonTab.ts @@ -102,7 +102,7 @@ export abstract class Tab extends Signal { } get iconColor() { if (!this.hasFired) return 'accent' - return App.packType.get(this.getPath())?.color + return App.packType.get(this.getPath(), true)?.color } get isSelected(): boolean { diff --git a/src/components/TabSystem/TabProvider.ts b/src/components/TabSystem/TabProvider.ts index 8931fd4f8..195df07e7 100644 --- a/src/components/TabSystem/TabProvider.ts +++ b/src/components/TabSystem/TabProvider.ts @@ -1,5 +1,6 @@ import { ImageTab } from '../Editors/Image/ImageTab' import { TargaTab } from '../Editors/Image/TargaTab' +import { SoundTab } from '../Editors/Sound/SoundTab' import { TextTab } from '../Editors/Text/TextTab' import { TreeTab } from '../Editors/TreeEditor/Tab' import { FileTab } from './FileTab' @@ -10,6 +11,7 @@ export class TabProvider { TreeTab, ImageTab, TargaTab, + SoundTab, ]) static get tabs() { return [...this._tabs].reverse() diff --git a/src/components/Toolbar/Category/view.ts b/src/components/Toolbar/Category/view.ts index 993304ae7..34b246130 100644 --- a/src/components/Toolbar/Category/view.ts +++ b/src/components/Toolbar/Category/view.ts @@ -59,6 +59,47 @@ export function setupViewCategory(app: App) { }) ) + view.addItem( + app.actionManager.create({ + icon: 'mdi-arrow-u-left-bottom', + name: 'toolbar.view.cursorUndo.name', + description: 'toolbar.view.cursorUndo.description', + keyBinding: 'ctrl + mouseBack', + onTrigger: async () => { + const tabSystem = app.project.tabSystem + if (!tabSystem) return + + // Await monacoEditor being created + await tabSystem.fired + tabSystem?.monacoEditor?.trigger( + 'keybinding', + 'cursorUndo', + null + ) + }, + }) + ) + view.addItem( + app.actionManager.create({ + icon: 'mdi-arrow-u-right-top', + name: 'toolbar.view.cursorRedo.name', + description: 'toolbar.view.cursorRedo.description', + keyBinding: 'mouseForward', + onTrigger: async () => { + const tabSystem = app.project.tabSystem + if (!tabSystem) return + + // Await monacoEditor being created + await tabSystem.fired + tabSystem?.monacoEditor?.trigger( + 'keybinding', + 'cursorRedo', + null + ) + }, + }) + ) + view.addItem(new Divider()) view.addItem(app.actionManager.create(ViewCompilerOutput(undefined, true))) diff --git a/src/components/UIElements/DirectoryViewer/ContextMenu/Actions/Edit/Paste.ts b/src/components/UIElements/DirectoryViewer/ContextMenu/Actions/Edit/Paste.ts index c41603a8c..1749062f1 100644 --- a/src/components/UIElements/DirectoryViewer/ContextMenu/Actions/Edit/Paste.ts +++ b/src/components/UIElements/DirectoryViewer/ContextMenu/Actions/Edit/Paste.ts @@ -1,8 +1,11 @@ import { DirectoryWrapper } from '../../../DirectoryView/DirectoryWrapper' import { clipboard } from './Copy' +import { + findSuitableFileName, + findSuitableFolderName, +} from '/@/utils/directory/findSuitableName' import { App } from '/@/App' import { AnyHandle } from '/@/components/FileSystem/Types' -import { basename, extname } from '/@/utils/path' export const PasteAction = (directoryWrapper: DirectoryWrapper) => ({ icon: 'mdi-content-paste', @@ -21,9 +24,9 @@ export const PasteAction = (directoryWrapper: DirectoryWrapper) => ({ let newHandle: AnyHandle if (handleToPaste.kind === 'file') { - const newName = findSuitableFileName( + const newName = await findSuitableFileName( handleToPaste.name, - directoryWrapper + directoryWrapper.handle ) newHandle = await directoryWrapper.handle.getFileHandle(newName, { @@ -33,9 +36,9 @@ export const PasteAction = (directoryWrapper: DirectoryWrapper) => ({ } else if (handleToPaste.kind === 'directory') { app.windows.loadingWindow.open() - const newName = findSuitableFolderName( + const newName = await findSuitableFolderName( handleToPaste.name, - directoryWrapper + directoryWrapper.handle ) newHandle = await directoryWrapper.handle.getDirectoryHandle( @@ -55,49 +58,3 @@ export const PasteAction = (directoryWrapper: DirectoryWrapper) => ({ return newHandle }, }) - -function findSuitableFileName( - name: string, - directoryWrapper: DirectoryWrapper -) { - const children = directoryWrapper.children.value - const fileExt = extname(name) - let newName = basename(name, fileExt) - - while (children?.find((child) => child.name === newName + fileExt)) { - if (!newName.includes(' copy')) { - // 1. Add "copy" to the end of the name - newName = `${newName} copy` - } else { - // 2. Add a number to the end of the name - // Get the number from the end of the name - const number = parseInt(newName.match(/copy (\d+)/)?.[1] ?? '1') - // Remove the last number and add the new one - newName = newName.replace(/ \d+$/, '') + ` ${number + 1}` - } - } - - return newName + fileExt -} -function findSuitableFolderName( - name: string, - directoryWrapper: DirectoryWrapper -) { - const children = directoryWrapper.children.value - let newName = name - - while (children?.find((child) => child.name === newName)) { - console.log(newName) - if (!newName.includes(' copy')) { - // 1. Add "copy" to the end of the name - newName = `${newName} copy` - } else { - // 2. Add a number to the end of the name - const number = parseInt(newName.match(/copy (\d+)/)?.[1] ?? '1') - newName = newName.replace(/ \d+$/, '') + ` ${number + 1}` - } - console.log('After: ' + newName) - } - - return newName -} diff --git a/src/components/Windows/Common/Dropdown/Dropdown.vue b/src/components/Windows/Common/Dropdown/Dropdown.vue index c8fd6efe4..5903f1a24 100644 --- a/src/components/Windows/Common/Dropdown/Dropdown.vue +++ b/src/components/Windows/Common/Dropdown/Dropdown.vue @@ -12,7 +12,7 @@ @closeWindow="onClose" >