diff --git a/package-lock.json b/package-lock.json index 2c64259da..1c495f4a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,7 @@ "vue-loading-overlay": "^3.3.3", "vue-timeago": "^5.1.2", "vue-tribute": "^1.0.7", - "webgl-media-editor": "^0.0.1", + "webgl-media-editor": "^0.0.6", "zuck.js": "^1.6.0" }, "devDependencies": { @@ -2323,6 +2323,12 @@ "m3u8-parser": "~4.7.1" } }, + "node_modules/@reactively/core": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@reactively/core/-/core-0.0.8.tgz", + "integrity": "sha512-5uAnNf2gQSm3gM7z6Lx079H1/MuDQQI+5aYfwyDFGR9nHZj8yblLY/6aOJVWp+NcBwXVBKuWQ28qWHD9F1qN1w==", + "license": "ISC" + }, "node_modules/@thaunknown/simple-peer": { "version": "10.0.11", "resolved": "https://registry.npmjs.org/@thaunknown/simple-peer/-/simple-peer-10.0.11.tgz", @@ -5645,6 +5651,15 @@ "node": ">=8" } }, + "node_modules/fine-jsx": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/fine-jsx/-/fine-jsx-0.0.5.tgz", + "integrity": "sha512-UKQ0ymyZnA605yf7np/wAv3iTs6i9oRKgyYmz+dX+F3VanYEBr60zRQ+WPcYzXMtl9NghNxT736qHfDBjoXVDg==", + "license": "AGPL-3.0-only", + "dependencies": { + "@reactively/core": "^0.0.8" + } + }, "node_modules/fizzy-ui-utils": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/fizzy-ui-utils/-/fizzy-ui-utils-2.0.7.tgz", @@ -10749,16 +10764,28 @@ "node": ">= 8" } }, + "node_modules/webgl-effects": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/webgl-effects/-/webgl-effects-0.0.3.tgz", + "integrity": "sha512-P+qxcO0QyydUnHHwnsge2ckou85Pnsdgn0BKAjrhD9LiPFz5i2hq8rT8AdS7wNdXrXyqlM1Y0id+AB0gKTDtpQ==", + "license": "AGPL-3.0-only", + "dependencies": { + "gl-matrix": "^3.4.3", + "twgl.js": "^5.5.4" + } + }, "node_modules/webgl-media-editor": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/webgl-media-editor/-/webgl-media-editor-0.0.1.tgz", - "integrity": "sha512-TxnuRl3rpWa1Cia/pn+vh+0iz3yDNwzsrnRGJ61YkdZAYuimu2afBivSHv0RK73hKza6Y/YoRCkuEcsFmtxPNw==", + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/webgl-media-editor/-/webgl-media-editor-0.0.6.tgz", + "integrity": "sha512-hqpIY+a+ay3QzXKECC4pFSHS0dVogV3GlBWzuSwBzEeGZcs7MeEYxLhFdqUa1D2xFtNnXb0pAo+1lCndYDKP2A==", "license": "AGPL-3.0-only", "dependencies": { "cropperjs": "^1.6.2", + "fine-jsx": "^0.0.5", "gl-matrix": "^3.4.3", "throttle-debounce": "^5.0.2", - "twgl.js": "^5.5.4" + "twgl.js": "^5.5.4", + "webgl-effects": "^0.0.3" } }, "node_modules/webidl-conversions": { diff --git a/package.json b/package.json index 691972ced..46f3839c5 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "vue-loading-overlay": "^3.3.3", "vue-timeago": "^5.1.2", "vue-tribute": "^1.0.7", - "webgl-media-editor": "^0.0.1", + "webgl-media-editor": "^0.0.6", "zuck.js": "^1.6.0" }, "collective": { diff --git a/resources/assets/js/components/ComposeModal.vue b/resources/assets/js/components/ComposeModal.vue index 1ec98c64f..389eba782 100644 --- a/resources/assets/js/components/ComposeModal.vue +++ b/resources/assets/js/components/ComposeModal.vue @@ -1015,19 +1015,29 @@ export default { }, created() { - this.editor = new MediaEditor({ - effects: filterEffects, - onEdit: (index, {effect, intensity, crop}) => { - if (index >= this.files.length) return - const file = this.files[index] + try { + this.editor = new MediaEditor({ + effects: filterEffects, + onEdit: (sourceIndex, {effect, intensity, crop}) => { + if (sourceIndex >= this.files.length) return + const file = this.files[sourceIndex] - this.$set(file, 'editState', { effect, intensity, crop }) - }, - onRenderPreview: (sourceIndex, previewUrl) => { - const media = this.media[sourceIndex] - if (media) media.preview_url = previewUrl - }, - }) + this.$set(file, 'editState', { effect, intensity, crop }) + }, + onRenderPreview: (sourceIndex, previewUrl) => { + if (sourceIndex >= this.files.length) return + const file = this.files[sourceIndex] + const { editState } = file + const media = this.media[sourceIndex] + + // If the image was edited, use the preview image from the editor. + if (editState && (editState.crop || editState.effect !== -1)) media.preview_url = previewUrl + // When no edits are applied, use the original media URL. + // This limits broken previews with firefox's resistFingerprinting setting. + else media.preview_url = media.url + }, + }) + } catch {} }, computed: { @@ -1054,8 +1064,9 @@ export default { }, destroyed() { - this.files.forEach(fileInfo => { - URL.revokeObjectURL(fileInfo.url); + this.media.forEach(media => { + URL.revokeObjectURL(media.url); + URL.revokeObjectURL(media.preview_url); }) this.files.length = this.media.length = 0 this.editor = undefined @@ -1161,7 +1172,7 @@ export default { const type = file.type.replace(/\/.*/, '') const url = URL.createObjectURL(file) - const preview_url = type === 'image' ? url : '/storage/no-preview.png' + const preview_url = type === 'image' ? URL.createObjectURL(file) : '/storage/no-preview.png' this.files.push({ file, editState: undefined }) this.media.push({ url, preview_url, type }) @@ -1182,7 +1193,16 @@ export default { const media = this.media[i] if (media.type === 'image' && fileInfo.editState) { - file = await this.editor.toBlob(i) + const { editState, cropperBlob } = fileInfo + + // If the WebGL editor is supported by the browser, apply the edits and use the resulting blob + if (this.editor && (editState.effect !== -1 || !!editState.crop)) { + file = await this.editor.toBlob(i) + } + // Otherwise, only the cropped result from cropper.js may be used + else if (cropperBlob) { + file = cropperBlob + } } let form = new FormData(); @@ -1555,12 +1575,29 @@ export default { break; case 'cropPhoto': - const { editState } = this.files[this.carouselCursor] + const file = this.files[this.carouselCursor] + const { cropper } = this.$refs + + // update the file state in this vue component const croppedState = { - ...editState, - crop: this.$refs.cropper.getData() + ...file.editState, + crop: cropper.getData() } - this.editor.setEditState(this.carouselCursor, croppedState) + + if (this.editor) { + // also update the file state in the WebGL editor + this.editor.setEditState(this.carouselCursor, croppedState) + } else { + // if the browser can't run the WebGL editor, get the cropped image from cropper.js + cropper.getCroppedCanvas().toBlob((blob) => { + const { media } = this.media[this.carouselCursor] + + file.croppedBlob = blob + URL.revokeObjectURL(media.preview_url) + media.preview_url = URL.createObjectURL(blob) + }) + } + this.page = 2; break;