diff --git a/Dotnet/AppApi/Cef/ImageUploading.cs b/Dotnet/AppApi/Cef/ImageUploading.cs
new file mode 100644
index 00000000..2c435c72
--- /dev/null
+++ b/Dotnet/AppApi/Cef/ImageUploading.cs
@@ -0,0 +1,33 @@
+using System;
+using System.IO;
+using System.Security.Cryptography;
+using librsync.net;
+
+namespace VRCX;
+
+public partial class AppApi
+{
+ public string MD5File(string blob)
+ {
+ var fileData = Convert.FromBase64CharArray(blob.ToCharArray(), 0, blob.Length);
+ using var md5 = MD5.Create();
+ var md5Hash = md5.ComputeHash(fileData);
+ return Convert.ToBase64String(md5Hash);
+ }
+
+ public string SignFile(string blob)
+ {
+ var fileData = Convert.FromBase64String(blob);
+ using var sig = Librsync.ComputeSignature(new MemoryStream(fileData));
+ using var memoryStream = new MemoryStream();
+ sig.CopyTo(memoryStream);
+ var sigBytes = memoryStream.ToArray();
+ return Convert.ToBase64String(sigBytes);
+ }
+
+ public string FileLength(string blob)
+ {
+ var fileData = Convert.FromBase64String(blob);
+ return fileData.Length.ToString();
+ }
+}
\ No newline at end of file
diff --git a/Dotnet/AppApi/Common/AppApiCommon.cs b/Dotnet/AppApi/Common/AppApiCommon.cs
index 4c6a6e79..70c88ad7 100644
--- a/Dotnet/AppApi/Common/AppApiCommon.cs
+++ b/Dotnet/AppApi/Common/AppApiCommon.cs
@@ -28,14 +28,6 @@ namespace VRCX
}
};
- public string MD5File(string blob)
- {
- var fileData = Convert.FromBase64CharArray(blob.ToCharArray(), 0, blob.Length);
- using var md5 = MD5.Create();
- var md5Hash = md5.ComputeHash(fileData);
- return Convert.ToBase64String(md5Hash);
- }
-
public int GetColourFromUserID(string userId)
{
var hash = _hasher.ComputeHash(Encoding.UTF8.GetBytes(userId));
diff --git a/Dotnet/VRCX-Cef.csproj b/Dotnet/VRCX-Cef.csproj
index 46728430..ef3e4931 100644
--- a/Dotnet/VRCX-Cef.csproj
+++ b/Dotnet/VRCX-Cef.csproj
@@ -61,6 +61,14 @@
+
+
+ libs\Blake2Sharp.dll
+
+
+ libs\librsync.net.dll
+
+
diff --git a/Dotnet/libs/Blake2Sharp.dll b/Dotnet/libs/Blake2Sharp.dll
new file mode 100644
index 00000000..43b09138
Binary files /dev/null and b/Dotnet/libs/Blake2Sharp.dll differ
diff --git a/Dotnet/libs/README.md b/Dotnet/libs/README.md
index 1d5cdf46..a62daf8a 100644
--- a/Dotnet/libs/README.md
+++ b/Dotnet/libs/README.md
@@ -1,3 +1,11 @@
+### librsync.net.dll
+
+- [https://github.com/braddodson/librsync.net](https://github.com/braddodson/librsync.net)
+
+### Blake2Sharp.dll
+
+- [https://github.com/BLAKE2/BLAKE2/tree/master/csharp](https://github.com/BLAKE2/BLAKE2/tree/master/csharp)
+
### openvr_api.dll
- [https://github.com/ValveSoftware/openvr](https://github.com/ValveSoftware/openvr)
diff --git a/Dotnet/libs/librsync.net.dll b/Dotnet/libs/librsync.net.dll
new file mode 100644
index 00000000..0324aff5
Binary files /dev/null and b/Dotnet/libs/librsync.net.dll differ
diff --git a/src/api/image.js b/src/api/image.js
new file mode 100644
index 00000000..9d523897
--- /dev/null
+++ b/src/api/image.js
@@ -0,0 +1,297 @@
+import { request } from '../service/request';
+import { useAvatarStore, useWorldStore } from '../stores';
+
+const imageReq = {
+ async uploadAvatarFailCleanup(id) {
+ const avatarStore = useAvatarStore();
+ try {
+ const json = await request(`file/${id}`, {
+ method: 'GET'
+ });
+ const fileId = json.id;
+ const fileVersion = json.versions[json.versions.length - 1].version;
+ request(`file/${fileId}/${fileVersion}/signature/finish`, {
+ method: 'PUT'
+ }).catch(err => console.error('Failed to finish signature:', err));
+ request(`file/${fileId}/${fileVersion}/file/finish`, {
+ method: 'PUT'
+ }).catch(err => console.error('Failed to finish file:', err));
+ } catch (error) {
+ console.error('Failed to cleanup avatar upload:', error);
+ }
+ avatarStore.avatarDialog.loading = false;
+ },
+
+ async uploadAvatarImage(params, fileId) {
+ try {
+ return await request(`file/${fileId}`, {
+ method: 'POST',
+ params
+ }).then((json) => {
+ const args = {
+ json,
+ params,
+ fileId
+ };
+ return args;
+ });
+ } catch (err) {
+ console.error(err);
+ imageReq.uploadAvatarFailCleanup(fileId);
+ throw err;
+ }
+ },
+
+ async uploadAvatarImageFileStart(params) {
+ try {
+ return await request(
+ `file/${params.fileId}/${params.fileVersion}/file/start`,
+ {
+ method: 'PUT'
+ }
+ ).then((json) => {
+ const args = {
+ json,
+ params
+ };
+ return args;
+ });
+ } catch (err) {
+ console.error(err);
+ imageReq.uploadAvatarFailCleanup(params.fileId);
+ }
+ },
+
+ uploadAvatarImageFileFinish(params) {
+ return request(
+ `file/${params.fileId}/${params.fileVersion}/file/finish`,
+ {
+ method: 'PUT',
+ params: {
+ maxParts: 0,
+ nextPartNumber: 0
+ }
+ }
+ ).then((json) => {
+ const args = {
+ json,
+ params
+ };
+ return args;
+ });
+ },
+
+ async uploadAvatarImageSigStart(params) {
+ try {
+ return await request(
+ `file/${params.fileId}/${params.fileVersion}/signature/start`,
+ {
+ method: 'PUT'
+ }
+ ).then((json) => {
+ const args = {
+ json,
+ params
+ };
+ return args;
+ });
+ } catch (err) {
+ console.error(err);
+ imageReq.uploadAvatarFailCleanup(params.fileId);
+ }
+ },
+
+ uploadAvatarImageSigFinish(params) {
+ return request(
+ `file/${params.fileId}/${params.fileVersion}/signature/finish`,
+ {
+ method: 'PUT',
+ params: {
+ maxParts: 0,
+ nextPartNumber: 0
+ }
+ }
+ ).then((json) => {
+ const args = {
+ json,
+ params
+ };
+ return args;
+ });
+ },
+
+ setAvatarImage(params) {
+ return request(`avatars/${params.id}`, {
+ method: 'PUT',
+ params
+ }).then((json) => {
+ const args = {
+ json,
+ params
+ };
+ return args;
+ });
+ },
+
+ async uploadWorldFailCleanup(id) {
+ const worldStore = useWorldStore();
+ try {
+ const json = await request(`file/${id}`, {
+ method: 'GET'
+ });
+ const fileId = json.id;
+ const fileVersion = json.versions[json.versions.length - 1].version;
+ request(`file/${fileId}/${fileVersion}/signature/finish`, {
+ method: 'PUT'
+ }).catch(err => console.error('Failed to finish signature:', err));
+ request(`file/${fileId}/${fileVersion}/file/finish`, {
+ method: 'PUT'
+ }).catch(err => console.error('Failed to finish file:', err));
+ } catch (error) {
+ console.error('Failed to cleanup world upload:', error);
+ }
+ worldStore.worldDialog.loading = false;
+ },
+
+ async uploadWorldImage(params, fileId) {
+ try {
+ return await request(`file/${fileId}`, {
+ method: 'POST',
+ params
+ }).then((json) => {
+ const args = {
+ json,
+ params,
+ fileId
+ };
+ return args;
+ });
+ } catch (err) {
+ console.error(err);
+ imageReq.uploadWorldFailCleanup(fileId);
+ }
+ return void 0;
+ },
+
+ async uploadWorldImageFileStart(params) {
+ try {
+ return await request(
+ `file/${params.fileId}/${params.fileVersion}/file/start`,
+ {
+ method: 'PUT'
+ }
+ ).then((json) => {
+ const args = {
+ json,
+ params
+ };
+ return args;
+ });
+ } catch (err) {
+ console.error(err);
+ imageReq.uploadWorldFailCleanup(params.fileId);
+ }
+ return void 0;
+ },
+
+ uploadWorldImageFileFinish(params) {
+ return request(
+ `file/${params.fileId}/${params.fileVersion}/file/finish`,
+ {
+ method: 'PUT',
+ params: {
+ maxParts: 0,
+ nextPartNumber: 0
+ }
+ }
+ ).then((json) => {
+ const args = {
+ json,
+ params
+ };
+ return args;
+ });
+ },
+
+ async uploadWorldImageSigStart(params) {
+ try {
+ return await request(
+ `file/${params.fileId}/${params.fileVersion}/signature/start`,
+ {
+ method: 'PUT'
+ }
+ ).then((json) => {
+ const args = {
+ json,
+ params
+ };
+ return args;
+ });
+ } catch (err) {
+ console.error(err);
+ imageReq.uploadWorldFailCleanup(params.fileId);
+ }
+ return void 0;
+ },
+
+ uploadWorldImageSigFinish(params) {
+ return request(
+ `file/${params.fileId}/${params.fileVersion}/signature/finish`,
+ {
+ method: 'PUT',
+ params: {
+ maxParts: 0,
+ nextPartNumber: 0
+ }
+ }
+ ).then((json) => {
+ const args = {
+ json,
+ params
+ };
+ return args;
+ });
+ },
+
+ setWorldImage(params) {
+ const worldStore = useWorldStore();
+ return request(`worlds/${params.id}`, {
+ method: 'PUT',
+ params
+ }).then((json) => {
+ const args = {
+ json,
+ params
+ };
+ args.ref = worldStore.applyWorld(json);
+ return args;
+ });
+ },
+
+ getAvatarImages(params) {
+ return request(`file/${params.fileId}`, {
+ method: 'GET'
+ }).then((json) => {
+ const args = {
+ json,
+ params
+ };
+ return args;
+ });
+ },
+
+ getWorldImages(params) {
+ return request(`file/${params.fileId}`, {
+ method: 'GET',
+ params
+ }).then((json) => {
+ const args = {
+ json,
+ params
+ };
+ return args;
+ });
+ }
+};
+
+export default imageReq;
diff --git a/src/api/index.js b/src/api/index.js
index 25a49272..36c30f2c 100644
--- a/src/api/index.js
+++ b/src/api/index.js
@@ -21,6 +21,7 @@ import groupRequest from './group';
import authRequest from './auth';
import inventoryRequest from './inventory';
import propRequest from './prop';
+import imageRequest from './image';
window.request = {
request,
@@ -40,7 +41,8 @@ window.request = {
authRequest,
groupRequest,
inventoryRequest,
- propRequest
+ propRequest,
+ imageRequest
};
export {
@@ -61,5 +63,6 @@ export {
authRequest,
groupRequest,
inventoryRequest,
- propRequest
+ propRequest,
+ imageRequest
};
diff --git a/src/components/dialogs/AvatarDialog/ChangeAvatarImageDialog.vue b/src/components/dialogs/AvatarDialog/ChangeAvatarImageDialog.vue
index e87a4d3d..3b770cc2 100644
--- a/src/components/dialogs/AvatarDialog/ChangeAvatarImageDialog.vue
+++ b/src/components/dialogs/AvatarDialog/ChangeAvatarImageDialog.vue
@@ -35,7 +35,7 @@
-
![]()
+
@@ -45,18 +45,21 @@
import { ElMessage } from 'element-plus';
import { Upload } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
- import { computed, ref } from 'vue';
+ import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
- import { avatarRequest } from '../../../api';
+ import { avatarRequest, imageRequest } from '../../../api';
import { handleImageUploadInput } from '../../../shared/utils/imageUpload';
import { useAvatarStore } from '../../../stores';
+ import { $throw } from '../../../service/request';
+ import { AppDebug } from '../../../service/appConfig';
+ import { extractFileId } from '../../../shared/utils';
const { t } = useI18n();
const { avatarDialog } = storeToRefs(useAvatarStore());
const { applyAvatar } = useAvatarStore();
- const props = defineProps({
+ defineProps({
changeAvatarImageDialogVisible: {
type: Boolean,
required: true
@@ -68,7 +71,14 @@
});
const changeAvatarImageDialogLoading = ref(false);
- const currentImageUrl = computed(() => props.previousImageUrl);
+ const avatarImage = ref({
+ base64File: '',
+ fileMd5: '',
+ base64SignatureFile: '',
+ signatureMd5: '',
+ fileId: '',
+ avatarId: ''
+ });
const emit = defineEmits(['update:changeAvatarImageDialogVisible', 'update:previousImageUrl']);
@@ -106,9 +116,15 @@
try {
const base64File = await resizeImageToFitLimits(btoa(r.result.toString()));
// 10MB
- await initiateUpload(base64File);
+ if (LINUX) {
+ // use new website upload process on Linux, we're missing the needed libraries for Unity method
+ // website method clears avatar name and is missing world image uploading
+ await initiateUpload(base64File);
+ return;
+ }
+ await initiateUploadLegacy(base64File, file);
} catch (error) {
- console.error('Avatar image upload process failed:', error);
+ console.error('avatar image upload process failed:', error);
} finally {
finalize();
}
@@ -123,6 +139,164 @@
}
}
+ async function initiateUploadLegacy(base64File, file) {
+ const fileMd5 = await AppApi.MD5File(base64File);
+ const fileSizeInBytes = parseInt(file.size, 10);
+ const base64SignatureFile = await AppApi.SignFile(base64File);
+ const signatureMd5 = await AppApi.MD5File(base64SignatureFile);
+ const signatureSizeInBytes = parseInt(await AppApi.FileLength(base64SignatureFile), 10);
+ const avatarId = avatarDialog.value.id;
+ const { imageUrl } = avatarDialog.value.ref;
+ const fileId = extractFileId(imageUrl);
+ avatarImage.value = {
+ base64File,
+ fileMd5,
+ base64SignatureFile,
+ signatureMd5,
+ fileId,
+ avatarId
+ };
+ const params = {
+ fileMd5,
+ fileSizeInBytes,
+ signatureMd5,
+ signatureSizeInBytes
+ };
+ const res = await imageRequest.uploadAvatarImage(params, fileId);
+ return avatarImageInit(res);
+ }
+
+ async function avatarImageInit(args) {
+ const fileId = args.json.id;
+ const fileVersion = args.json.versions[args.json.versions.length - 1].version;
+ const params = {
+ fileId,
+ fileVersion
+ };
+ const res = await imageRequest.uploadAvatarImageFileStart(params);
+ return avatarImageFileStart(res);
+ }
+
+ async function avatarImageFileStart(args) {
+ const { url } = args.json;
+ const { fileId, fileVersion } = args.params;
+ const params = {
+ url,
+ fileId,
+ fileVersion
+ };
+ return uploadAvatarImageFileAWS(params);
+ }
+
+ async function uploadAvatarImageFileAWS(params) {
+ const json = await webApiService.execute({
+ url: params.url,
+ uploadFilePUT: true,
+ fileData: avatarImage.value.base64File,
+ fileMIME: 'image/png',
+ headers: {
+ 'Content-MD5': avatarImage.value.fileMd5
+ }
+ });
+
+ if (json.status !== 200) {
+ changeAvatarImageDialogLoading.value = false;
+ $throw(json.status, 'avatar image upload failed', params.url);
+ }
+ const args = {
+ json,
+ params
+ };
+ return avatarImageFileAWS(args);
+ }
+
+ async function avatarImageFileAWS(args) {
+ const { fileId, fileVersion } = args.params;
+ const params = {
+ fileId,
+ fileVersion
+ };
+ const res = await imageRequest.uploadAvatarImageFileFinish(params);
+ return avatarImageFileFinish(res);
+ }
+
+ async function avatarImageFileFinish(args) {
+ const { fileId, fileVersion } = args.params;
+ const params = {
+ fileId,
+ fileVersion
+ };
+ const res = await imageRequest.uploadAvatarImageSigStart(params);
+ return avatarImageSigStart(res);
+ }
+
+ async function avatarImageSigStart(args) {
+ const { url } = args.json;
+ const { fileId, fileVersion } = args.params;
+ const params = {
+ url,
+ fileId,
+ fileVersion
+ };
+ return uploadAvatarImageSigAWS(params);
+ }
+
+ async function uploadAvatarImageSigAWS(params) {
+ const json = await webApiService.execute({
+ url: params.url,
+ uploadFilePUT: true,
+ fileData: avatarImage.value.base64SignatureFile,
+ fileMIME: 'application/x-rsync-signature',
+ headers: {
+ 'Content-MD5': avatarImage.value.signatureMd5
+ }
+ });
+
+ if (json.status !== 200) {
+ changeAvatarImageDialogLoading.value = false;
+ $throw(json.status, 'avatar image upload failed', params.url);
+ }
+ const args = {
+ json,
+ params
+ };
+ return avatarImageSigAWS(args);
+ }
+
+ async function avatarImageSigAWS(args) {
+ const { fileId, fileVersion } = args.params;
+ const params = {
+ fileId,
+ fileVersion
+ };
+ const res = await imageRequest.uploadAvatarImageSigFinish(params);
+ return avatarImageSigFinish(res);
+ }
+ async function avatarImageSigFinish(args) {
+ const { fileId, fileVersion } = args.params;
+ const params = {
+ id: avatarImage.value.avatarId,
+ imageUrl: `${AppDebug.endpointDomain}/file/${fileId}/${fileVersion}/file`
+ };
+ const res = await imageRequest.setAvatarImage(params);
+ return avatarImageSet(res);
+ }
+
+ function avatarImageSet(args) {
+ changeAvatarImageDialogLoading.value = false;
+ if (args.json.imageUrl === args.params.imageUrl) {
+ ElMessage({
+ message: t('message.avatar.image_changed'),
+ type: 'success'
+ });
+ emit('update:previousImageUrl', args.json.imageUrl);
+ } else {
+ $throw(0, 'avatar image change failed', args.params.imageUrl);
+ }
+ }
+
+ // ------------ Upload Process End ------------
+
async function initiateUpload(base64File) {
const args = await avatarRequest.uploadAvatarImage(base64File);
const fileUrl = args.json.versions[args.json.versions.length - 1].file.url;
@@ -131,8 +305,8 @@
imageUrl: fileUrl
});
const ref = applyAvatar(avatarArgs.json);
- emit('update:previousImageUrl', ref.imageUrl);
changeAvatarImageDialogLoading.value = false;
+ emit('update:previousImageUrl', ref.imageUrl);
ElMessage({
message: t('message.avatar.image_changed'),
type: 'success'
diff --git a/src/components/dialogs/WorldDialog/ChangeWorldImageDialog.vue b/src/components/dialogs/WorldDialog/ChangeWorldImageDialog.vue
index 25ce239c..e826dea6 100644
--- a/src/components/dialogs/WorldDialog/ChangeWorldImageDialog.vue
+++ b/src/components/dialogs/WorldDialog/ChangeWorldImageDialog.vue
@@ -47,9 +47,12 @@
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
- import { worldRequest } from '../../../api';
+ import { worldRequest, imageRequest } from '../../../api';
import { handleImageUploadInput } from '../../../shared/utils/imageUpload';
import { useWorldStore } from '../../../stores';
+ import { $throw } from '../../../service/request';
+ import { AppDebug } from '../../../service/appConfig';
+ import { extractFileId } from '../../../shared/utils';
const { t } = useI18n();
@@ -68,6 +71,14 @@
});
const changeWorldImageDialogLoading = ref(false);
+ const worldImage = ref({
+ base64File: '',
+ fileMd5: '',
+ base64SignatureFile: '',
+ signatureMd5: '',
+ fileId: '',
+ worldId: ''
+ });
const emit = defineEmits(['update:changeWorldImageDialogVisible', 'update:previousImageUrl']);
@@ -105,7 +116,8 @@
try {
const base64File = await resizeImageToFitLimits(btoa(r.result.toString()));
// 10MB
- await initiateUpload(base64File);
+ await initiateUploadLegacy(base64File, file);
+ // await initiateUpload(base64File);
} catch (error) {
console.error('World image upload process failed:', error);
} finally {
@@ -122,6 +134,164 @@
}
}
+ async function initiateUploadLegacy(base64File, file) {
+ const fileMd5 = await AppApi.MD5File(base64File);
+ const fileSizeInBytes = parseInt(file.size, 10);
+ const base64SignatureFile = await AppApi.SignFile(base64File);
+ const signatureMd5 = await AppApi.MD5File(base64SignatureFile);
+ const signatureSizeInBytes = parseInt(await AppApi.FileLength(base64SignatureFile), 10);
+ const worldId = worldDialog.value.id;
+ const { imageUrl } = worldDialog.value.ref;
+ const fileId = extractFileId(imageUrl);
+ worldImage.value = {
+ base64File,
+ fileMd5,
+ base64SignatureFile,
+ signatureMd5,
+ fileId,
+ worldId
+ };
+ const params = {
+ fileMd5,
+ fileSizeInBytes,
+ signatureMd5,
+ signatureSizeInBytes
+ };
+ const res = await imageRequest.uploadWorldImage(params, fileId);
+ return worldImageInit(res);
+ }
+
+ async function worldImageInit(args) {
+ const fileId = args.json.id;
+ const fileVersion = args.json.versions[args.json.versions.length - 1].version;
+ const params = {
+ fileId,
+ fileVersion
+ };
+ const res = await imageRequest.uploadWorldImageFileStart(params);
+ return worldImageFileStart(res);
+ }
+
+ async function worldImageFileStart(args) {
+ const { url } = args.json;
+ const { fileId, fileVersion } = args.params;
+ const params = {
+ url,
+ fileId,
+ fileVersion
+ };
+ return uploadWorldImageFileAWS(params);
+ }
+
+ async function uploadWorldImageFileAWS(params) {
+ const json = await webApiService.execute({
+ url: params.url,
+ uploadFilePUT: true,
+ fileData: worldImage.value.base64File,
+ fileMIME: 'image/png',
+ headers: {
+ 'Content-MD5': worldImage.value.fileMd5
+ }
+ });
+
+ if (json.status !== 200) {
+ changeWorldImageDialogLoading.value = false;
+ $throw(json.status, 'World image upload failed', params.url);
+ }
+ const args = {
+ json,
+ params
+ };
+ return worldImageFileAWS(args);
+ }
+
+ async function worldImageFileAWS(args) {
+ const { fileId, fileVersion } = args.params;
+ const params = {
+ fileId,
+ fileVersion
+ };
+ const res = await imageRequest.uploadWorldImageFileFinish(params);
+ return worldImageFileFinish(res);
+ }
+
+ async function worldImageFileFinish(args) {
+ const { fileId, fileVersion } = args.params;
+ const params = {
+ fileId,
+ fileVersion
+ };
+ const res = await imageRequest.uploadWorldImageSigStart(params);
+ return worldImageSigStart(res);
+ }
+
+ async function worldImageSigStart(args) {
+ const { url } = args.json;
+ const { fileId, fileVersion } = args.params;
+ const params = {
+ url,
+ fileId,
+ fileVersion
+ };
+ return uploadWorldImageSigAWS(params);
+ }
+
+ async function uploadWorldImageSigAWS(params) {
+ const json = await webApiService.execute({
+ url: params.url,
+ uploadFilePUT: true,
+ fileData: worldImage.value.base64SignatureFile,
+ fileMIME: 'application/x-rsync-signature',
+ headers: {
+ 'Content-MD5': worldImage.value.signatureMd5
+ }
+ });
+
+ if (json.status !== 200) {
+ changeWorldImageDialogLoading.value = false;
+ $throw(json.status, 'World image upload failed', params.url);
+ }
+ const args = {
+ json,
+ params
+ };
+ return worldImageSigAWS(args);
+ }
+
+ async function worldImageSigAWS(args) {
+ const { fileId, fileVersion } = args.params;
+ const params = {
+ fileId,
+ fileVersion
+ };
+ const res = await imageRequest.uploadWorldImageSigFinish(params);
+ return worldImageSigFinish(res);
+ }
+ async function worldImageSigFinish(args) {
+ const { fileId, fileVersion } = args.params;
+ const params = {
+ id: worldImage.value.worldId,
+ imageUrl: `${AppDebug.endpointDomain}/file/${fileId}/${fileVersion}/file`
+ };
+ const res = await imageRequest.setWorldImage(params);
+ return worldImageSet(res);
+ }
+
+ function worldImageSet(args) {
+ changeWorldImageDialogLoading.value = false;
+ if (args.json.imageUrl === args.params.imageUrl) {
+ ElMessage({
+ message: t('message.world.image_changed'),
+ type: 'success'
+ });
+ emit('update:previousImageUrl', args.json.imageUrl);
+ } else {
+ $throw(0, 'World image change failed', args.params.imageUrl);
+ }
+ }
+
+ // ------------ Upload Process End ------------
+
async function initiateUpload(base64File) {
const args = await worldRequest.uploadWorldImage(base64File);
const fileUrl = args.json.versions[args.json.versions.length - 1].file.url;
diff --git a/src/components/dialogs/WorldDialog/WorldDialog.vue b/src/components/dialogs/WorldDialog/WorldDialog.vue
index 017ab2d7..7650efc6 100644
--- a/src/components/dialogs/WorldDialog/WorldDialog.vue
+++ b/src/components/dialogs/WorldDialog/WorldDialog.vue
@@ -283,7 +283,7 @@
{{ t('dialog.world.actions.change_allowed_video_player_domains') }}
-
+
{{ t('dialog.world.actions.change_image') }}
WINDOWS);
+
const memo = computed({
get() {
return worldDialog.value.memo;
@@ -966,7 +968,7 @@
treeData.value = [];
}
- function showChangeAvatarImageDialog() {
+ function showChangeWorldImageDialog() {
const { imageUrl } = worldDialog.value.ref;
previousImageUrl.value = imageUrl;
changeWorldImageDialogVisible.value = true;
@@ -1120,7 +1122,7 @@
openExternalLink(replaceVrcPackageUrl(worldDialog.value.ref.unityPackageUrl));
break;
case 'Change Image':
- showChangeAvatarImageDialog();
+ showChangeWorldImageDialog();
break;
case 'Refresh':
showWorldDialog(D.id);
diff --git a/src/types/globals.d.ts b/src/types/globals.d.ts
index bfbfa88d..20773262 100644
--- a/src/types/globals.d.ts
+++ b/src/types/globals.d.ts
@@ -186,7 +186,6 @@ declare global {
SetUserAgent(): Promise;
// Common Functions
- MD5File(blob: string): Promise;
GetColourFromUserID(userId: string): Promise;
OpenLink(url: string): Promise;
GetLaunchCommand(): Promise;
@@ -207,6 +206,11 @@ declare global {
GetFileBase64(path: string): Promise;
TryOpenInstanceInVrc(launchUrl: string): Promise;
+ // Image Upload (Cef Only)
+ MD5File(blob: string): Promise;
+ SignFile(blob: string): Promise;
+ FileLength(blob: string): Promise;
+
// Folders
GetVRChatAppDataLocation(): Promise;
GetVRChatPhotosLocation(): Promise;