diff --git a/src/app/helpers/images.ts b/src/app/helpers/images.ts index 7d55e9228f537fba500d6a80016ac9415577e4fc..2f4eceda5d33f65aed85a109990347e12878bf89 100644 --- a/src/app/helpers/images.ts +++ b/src/app/helpers/images.ts @@ -3,7 +3,7 @@ import { useFilesWebsocketStore } from '@/app/stores/filesWebsocket'; import type { IImage } from '@/app/interfaces/entities'; import { useWebsocketStore } from '@/app/stores/websocket'; import { useInterfaceStore } from '@/app/stores/interface'; -import { editEntity } from '@/app/helpers/index'; +import { imageScaleOptions } from '@/components/entities/settings/lists/options'; export const setDefaultPageBackground = () => { const interfaceStore = useInterfaceStore(); @@ -16,14 +16,14 @@ export const addUrlsToImageEntities = (entities: IEntity[]) => { let index = 0; const entitiesToReturn = entities.map((entity: IEntity) => { if (!entity?.image_width) return entity; - if (entity.imageUrl) return entity; - if (filesWebsocketStore.imageUrl) { + if (entity.image_url) return entity; + if (filesWebsocketStore.image_url) { // редактирование ÑущноÑти Ð¸Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ - entity.imageUrl = filesWebsocketStore.imageUrl; + entity.image_url = filesWebsocketStore.image_url; filesWebsocketStore.cleanImageUrl(); } else { filesBuffer[index] = new Blob([filesBuffer[index].data], { type: 'image/jpeg' }); - entity.imageUrl = URL.createObjectURL(filesBuffer[index]); + entity.image_url = URL.createObjectURL(filesBuffer[index]); index += 1; } return entity; @@ -38,8 +38,8 @@ export const checkIsImage = (entity: IEntity) => { } const entityToReturn = { ...entity }; const filesWebsocketStore = useFilesWebsocketStore(); - filesWebsocketStore.saveImageUrl(entityToReturn.imageUrl!); - delete entityToReturn.imageUrl; + filesWebsocketStore.saveImageUrl(entityToReturn.image_url!); + delete entityToReturn.image_url; return entityToReturn; }; @@ -58,134 +58,76 @@ export const cropImage = async (newUrl: string, entity: IImage) => { websocketStore.sendData(data); }; -export const getImageScalesToRemove = (entity: IImage) => { +export const getImageScalesToRemove = ( + entity: IImage, + isText?: boolean, + isEntityWidthFull: boolean +) => { const valuesToRemove = []; let scale = entity.image_scale; if (scale[0] === 'x') scale = scale.slice(1); const initialImageWidth = Math.ceil(+entity.image_width / +scale); - const initialImageHeight = Math.ceil(+entity.image_height / +scale); - console.log(initialImageWidth, initialImageHeight); - if (initialImageWidth <= 400 || initialImageHeight <= 400) { + console.log(initialImageWidth); + if (initialImageWidth <= 20) { valuesToRemove.push('x0.25'); - if ( - initialImageWidth <= 200 || - initialImageHeight <= 200 || - (initialImageWidth >= 1600 && (entity.text ?? null)) - ) { + if (initialImageWidth <= 10) { valuesToRemove.push('x0.5'); - if ( - initialImageWidth <= 95 || - initialImageHeight <= 95 || - (initialImageWidth >= 1066 && (entity.text ?? null)) - ) { - valuesToRemove.push('x0.75'); - } } } - if ( - (initialImageWidth >= 800 && (entity.text ?? null)) || - entity.image_width > initialImageWidth - ) { + if (initialImageWidth <= 7 || (!isEntityWidthFull && isText && initialImageWidth > 66)) { + valuesToRemove.push('x0.75'); + } + if (initialImageWidth > 75 && isText) { valuesToRemove.push('x1'); } if ( - initialImageWidth >= 960 || - initialImageHeight >= 560 || - (initialImageWidth >= 640 && (entity.text ?? null)) + initialImageWidth > 80 || + (initialImageWidth > 60 && isText) || + (!isEntityWidthFull && isText && initialImageWidth > 40) ) { valuesToRemove.push('x1.25'); if ( - initialImageWidth >= 800 || - initialImageHeight >= 467 || - (initialImageWidth >= 533 && (entity.text ?? null)) + initialImageWidth > 66 || + (initialImageWidth > 50 && isText) || + (!isEntityWidthFull && isText && initialImageWidth > 33) ) { valuesToRemove.push('x1.5'); if ( - initialImageWidth >= 685 || - initialImageHeight >= 400 || - (initialImageWidth >= 457 && (entity.text ?? null)) + initialImageWidth > 57 || + (initialImageWidth > 42 && isText) || + (!isEntityWidthFull && isText && initialImageWidth > 28) ) { valuesToRemove.push('x1.75'); if ( - initialImageWidth >= 600 || - initialImageHeight >= 350 || - (initialImageWidth >= 400 && (entity.text ?? null)) + initialImageWidth > 50 || + (initialImageWidth > 37 && isText) || + (!isEntityWidthFull && isText && initialImageWidth > 25) ) { valuesToRemove.push('x2'); } } } } - console.log('valuesToRemove: ', valuesToRemove); return valuesToRemove; }; -export const getImageSpeedDialSizeSmallerLabelsToRemove = (entity: IImage) => { - const valuesToRemove = []; - const initialImageWidth = Math.ceil(entity.image_width / +entity.image_scale); - const initialImageHeight = Math.ceil(entity.image_height / +entity.image_scale); - if (initialImageWidth <= 400 || initialImageHeight <= 400) { - valuesToRemove.push('x0.25'); - if ( - initialImageWidth <= 200 || - initialImageHeight <= 200 || - (initialImageWidth >= 1600 && entity.text_position) - ) { - valuesToRemove.push('x0.5'); - if ( - initialImageWidth <= 95 || - initialImageHeight <= 95 || - (initialImageWidth >= 1066 && entity.text_position) - ) { - valuesToRemove.push('x0.75'); - } - } - } - if ( - (initialImageWidth >= 800 && entity.text_position) || - entity.image_width < initialImageWidth - ) { - valuesToRemove.push('x1'); - } - return valuesToRemove; -}; +export const filterImageScaleOptions = ( + entityData: IImage, + isText: boolean, + isEntityWidthFull: boolean +) => { + const scalesToRemove = getImageScalesToRemove(entityData, isText, isEntityWidthFull); + let initialScales = imageScaleOptions; -export const getImageSpeedDialSizeBiggerLabelsToRemove = (entity: IImage) => { - const valuesToRemove = []; - const initialImageWidth = Math.ceil(entity.image_width / +entity.image_scale); - const initialImageHeight = Math.ceil(entity.image_height / +entity.image_scale); - if ((initialImageWidth >= 800 && entity.text) || entity.image_width > initialImageWidth) { - valuesToRemove.push('x1'); - } - if ( - initialImageWidth >= 960 || - initialImageHeight >= 560 || - (initialImageWidth >= 640 && entity.text) - ) { - valuesToRemove.push('x1.25'); - if ( - initialImageWidth >= 800 || - initialImageHeight >= 467 || - (initialImageWidth >= 533 && entity.text) - ) { - valuesToRemove.push('x1.5'); - if ( - initialImageWidth >= 685 || - initialImageHeight >= 400 || - (initialImageWidth >= 457 && entity.text) - ) { - valuesToRemove.push('x1.75'); - if ( - initialImageWidth >= 600 || - initialImageHeight >= 350 || - (initialImageWidth >= 400 && entity.text) - ) { - valuesToRemove.push('x2'); - } - } - } + if (!scalesToRemove.length) return imageScaleOptions; + + initialScales = initialScales.filter((item) => !~scalesToRemove.indexOf(item.label)); + + for (let i = 0; i < initialScales.length; i++) { + initialScales[i].value = i; } - return valuesToRemove; + + return initialScales; }; export const scaleImage = (entityData: IImage, prevScale: string) => { @@ -194,7 +136,5 @@ export const scaleImage = (entityData: IImage, prevScale: string) => { if (prevScale[0] === 'x') prevScale = prevScale.slice(1); const initialWidth = Math.ceil(+entityData.image_width / +prevScale); entityData.image_width = Math.ceil(initialWidth * +scale); - const initialHeight = Math.ceil(+entityData.image_height / +prevScale); - entityData.image_height = Math.ceil(initialHeight * +scale); return entityData; }; diff --git a/src/app/helpers/index.ts b/src/app/helpers/index.ts index af4a3b0db4b87c8b0361364faffd2bb33a6cd3ae..92df535cb2387cdf1144062bffff1ab5cf3515ca 100644 --- a/src/app/helpers/index.ts +++ b/src/app/helpers/index.ts @@ -51,6 +51,15 @@ export const editEntity = (newState: IEntity) => { websocketStore.sendData(data); }; +export const returnOriginalImageSize = (newState: IEntity) => { + const websocketStore = useWebsocketStore(); + const data = { + event: 'returnOriginalImageSize', + body: { ...newState } + }; + websocketStore.sendData(data); +}; + export const deleteEntity = (entityUuid: string) => { const dataStore = useDataStore(); const websocketStore = useWebsocketStore(); diff --git a/src/app/interfaces/entities.ts b/src/app/interfaces/entities.ts index 050bca6928ddbe72c06377bc32985449dea5fe3e..d2793221d98d45920c356a62a23e660e702dc5dd 100644 --- a/src/app/interfaces/entities.ts +++ b/src/app/interfaces/entities.ts @@ -52,9 +52,13 @@ export interface IImage extends IEntity { font_size?: '16' | '20' | '24' | '40' | '64' | null; paragraph_size?: 'full' | 'half' | null; text_position?: 'left' | 'right' | null; - imageUrl: string; + file_width_initial: number; + file_height_initial: number; + image_url_initial: string; + image_url: string; image_width: number; - image_height: number; + file_width: number; + file_height: number; image_scale: string; entity_title_position: 'left' | 'center' | 'right'; entity_position: 'left' | 'center' | 'right'; diff --git a/src/app/interfaces/environment.ts b/src/app/interfaces/environment.ts index ca52344f5a818ea6d65ae5df621b8ae844afd34e..5f85a646585abe85647e839a1a6fbcdf74562f2e 100644 --- a/src/app/interfaces/environment.ts +++ b/src/app/interfaces/environment.ts @@ -25,9 +25,13 @@ export interface IEntity { paragraph_size?: string | null; text_position?: string | null; image_buffer?: string; - imageUrl?: string; + file_width_initial?: number; + file_height_initial?: number; + image_url_initial?: string; + image_url?: string; image_width?: number; - image_height?: number; + file_width?: number; + file_height?: number; entity_position?: string; entity_title_position?: string; image_scale?: string; diff --git a/src/app/interfaces/index.ts b/src/app/interfaces/index.ts index b518d74ab4e5202f9a0e27135ffa581a3716aa42..852252c9b81a028cd3af260b1a7a59f185124552 100644 --- a/src/app/interfaces/index.ts +++ b/src/app/interfaces/index.ts @@ -1,5 +1,7 @@ export interface IImageMainInfo { - imageUrl: string; - image_width: number; - image_height: number; + image_url: string; + image_width?: number; + image_height?: number; + file_width?: number; + file_height?: number; } diff --git a/src/app/router/index.ts b/src/app/router/index.ts index 10f5fc7bd982c2edf7daa6f1dcff7a1b4ddfcb9f..6f89ee165adbe46401472e3d03be2038cd6fc932 100644 --- a/src/app/router/index.ts +++ b/src/app/router/index.ts @@ -12,6 +12,11 @@ const router = createRouter({ path: '/signUp', name: 'signUp', component: () => import('@/pages/authorization/signUp.vue') + }, + { + path: '/', + name: 'emptyPage', + component: () => import('@/pages/[uuid]/SheetPage.vue') } ] }); diff --git a/src/app/stores/filesWebsocket.ts b/src/app/stores/filesWebsocket.ts index 43e5e6bcc61b7889a25d4321d7648649d37ad6a9..d0f7c03d079a1ad416f6208065d88a71dbcfa492 100644 --- a/src/app/stores/filesWebsocket.ts +++ b/src/app/stores/filesWebsocket.ts @@ -6,7 +6,7 @@ export const useFilesWebsocketStore = defineStore('filesWebsocketStore', () => { const socket = ref(); const filesBuffer = ref([]); - const imageUrl = ref(); + const image_url = ref(); onMounted(() => { socket.value = new WebSocket('ws://localhost:5001'); @@ -26,17 +26,17 @@ export const useFilesWebsocketStore = defineStore('filesWebsocketStore', () => { filesBuffer.value = []; } function saveImageUrl(url: string) { - imageUrl.value = url; + image_url.value = url; } function cleanImageUrl() { - imageUrl.value = ''; + image_url.value = ''; } function sendData(data: unknown) { socket.value.send(data); } return { filesBuffer, - imageUrl, + image_url, cleanFilesBuffer, removeFirstFilesBuffer, saveImageUrl, diff --git a/src/app/stores/websocket.ts b/src/app/stores/websocket.ts index fc1bdaeb8371bd5df3fd4222e77cb159285158bb..067c6472ca6de1f6fd57454d457a93878dbdd519 100644 --- a/src/app/stores/websocket.ts +++ b/src/app/stores/websocket.ts @@ -6,6 +6,7 @@ import { addUrlsToImageEntities } from '@/app/helpers/images'; import { useFilesWebsocketStore } from '@/app/stores/filesWebsocket'; import { useAuthorizationStore } from '@/app/stores/authorization'; import cookies from '@/app/plugins/Cookie'; +import { editEntity } from '@/app/helpers'; export const useWebsocketStore = defineStore('websocketStore', () => { const dataStore = useDataStore(); @@ -24,6 +25,7 @@ export const useWebsocketStore = defineStore('websocketStore', () => { const initialDataToSend = ref(); const isInitialAddUrlsToImageEntitiesFinished = ref<boolean>(false); const file = ref(); + const originalUrl = ref(); onMounted(() => { socket.value = new WebSocket('ws://localhost:5000'); @@ -85,7 +87,8 @@ export const useWebsocketStore = defineStore('websocketStore', () => { case 'createEntity': { const newState = [...entities.value]; if (response.data?.image_width) { - response.data.imageUrl = filesWebsocketStore.imageUrl; + response.data.image_url_initial = filesWebsocketStore.image_url; + response.data.image_url = filesWebsocketStore.image_url; filesWebsocketStore.cleanImageUrl(); } newState.push(response.data); @@ -142,7 +145,7 @@ export const useWebsocketStore = defineStore('websocketStore', () => { newState = newState.map((entity: IEntity) => { if (entity.entity_uuid !== response.data.entity_uuid) return entity; if (response.data?.image_width) { - response.data.imageUrl = filesWebsocketStore.imageUrl; + response.data.image_url = filesWebsocketStore.image_url; filesWebsocketStore.cleanImageUrl(); } return response.data; @@ -150,6 +153,23 @@ export const useWebsocketStore = defineStore('websocketStore', () => { dataStore.editEntities(newState); break; } + case 'returnOriginalSizeImage': { + let newState = [...entities.value]; + newState = newState.map((entity: IEntity) => { + if (entity.entity_uuid !== response.data.entity_uuid) return entity; + const filesBuffer = filesWebsocketStore.filesBuffer; + filesBuffer[0] = new Blob([filesBuffer[0].data], { type: 'image/jpeg' }); + entity.image_url = URL.createObjectURL(filesBuffer[0]); + originalUrl.value = entity.image_url; + // entity.file_width = entity.file_width_initial; + // entity.file_height = entity.file_height_initial; + console.log('entity.image_url ', entity.image_url); + editEntity(entity); + return entity; + }); + dataStore.editEntities(newState); + break; + } case 'changeEntitiesOrder': { const mainEntity = response.data.main; const mainEntityIndex = entities.value.findIndex( diff --git a/src/components/CreateEntityMenu.vue b/src/components/CreateEntityMenu.vue index 2bb2a00642d671ee09452b624907bb191efe15c4..201447e5d944af6a20f0651bce94c5cbf8eb0894 100644 --- a/src/components/CreateEntityMenu.vue +++ b/src/components/CreateEntityMenu.vue @@ -31,16 +31,21 @@ const addImage = async (files: FileList) => { const blob = await response.blob(); const buffer = await blob.arrayBuffer(); const { width: windowWidth } = useWindowSize(); - const maxWidth = windowWidth.value - 128; - const maxHeight = 700; - if (image.width > maxWidth) { - image.height = Math.floor((maxWidth / image.width) * image.height); - image.width = maxWidth; - } + const maxHeight = 600; + const initWidth = image.width; if (image.height > maxHeight) { - image.width = Math.floor((maxHeight / image.height) * image.width); - image.height = maxHeight; + const coefficient = maxHeight / image.height; + image.width *= coefficient; + } + let imageWidth = Math.ceil((image.width / (windowWidth.value - 128)) * 100); + if (imageWidth > 100) { + imageWidth = 100; + } + if (imageWidth < 5) { + imageWidth = 5; } + console.log(`image.width: ${image.width}, + image.height: ${image.height}`); emit('createEntity', { entity_type: 'image', entity_order: entitiesCount.value + 1, @@ -50,8 +55,11 @@ const addImage = async (files: FileList) => { font_size: '24', text_position: 'right', paragraph_size: 'full', - image_width: image.width, - image_height: image.height, + image_width: imageWidth, + file_width: initWidth, + file_height: image.height, + file_width_initial: initWidth, + file_height_initial: image.height, image_scale: 'x1' }); }; diff --git a/src/components/entities/settings/ImageSettings.vue b/src/components/entities/settings/ImageSettings.vue index ea46e0fce2876f394f03d05ed73a1cb1a028dccf..6f646f5d64612a05c4022de3b674dfeeaf67bbc5 100644 --- a/src/components/entities/settings/ImageSettings.vue +++ b/src/components/entities/settings/ImageSettings.vue @@ -3,20 +3,19 @@ import type { IImage } from '@/app/interfaces/entities'; import { convertThemeToColorWhiteDefault, deleteEntity, editEntity } from '@/app/helpers'; import type { TTheme } from '@/app/interfaces/environment'; import cookies from '@/app/plugins/Cookie'; -import { useWindowSize } from '@vueuse/core'; import { cropImage } from '@/app/helpers/images'; interface Props { entityData: IImage; } const props = defineProps<Props>(); -const emit = defineEmits(['saveChanges']); +const emit = defineEmits(['saveChanges', 'returnOriginalSize']); const entityData = computed(() => props.entityData); +const prevEntityData = { ...entityData.value }; const newEntityData = ref({ ...entityData.value }); watch(entityData, () => (newEntityData.value = entityData.value)); const isModal = ref<boolean>(false); const isModalCropImage = ref<boolean>(false); -const { width: windowWidth } = useWindowSize(); const changeFontSize = (newSize: '16' | '20' | '24' | '40' | '64') => { entityData.value.font_size = newSize; @@ -29,51 +28,64 @@ const isText = ref(!!entityData.value.text); const isEntityWidthFull = ref(entityData.value.paragraph_size === 'full'); const isModalToDeleteImage = ref<boolean>(false); const textContainerWidth = computed(() => { - if (!isEntityWidthFull.value) - return (windowWidth.value - 160 - newEntityData.value.image_width) / 2; - return windowWidth.value - 160 - newEntityData.value.image_width; + if (!isEntityWidthFull.value) return (100 - newEntityData.value.image_width) / 2; + return 100 - newEntityData.value.image_width; }); const maxLines = computed(() => { if (isText.value) { - return Math.floor(newEntityData.value.image_height / newEntityData.value.font_size); + return Math.floor(newEntityData.value.file_height / +newEntityData.value.font_size); } else { return 0; } }); const saveChanges = () => { const entityPosition = isEntityWidthFull.value ? 'full' : 'half'; - if (entityPosition !== entityData.value.paragraph_size) { + if (entityPosition !== prevEntityData.paragraph_size) { newEntityData.value.paragraph_size = entityPosition; } - if (isTitle.value !== !!entityData.value.title) { + if (isTitle.value !== !!prevEntityData.title) { if (isTitle.value) { newEntityData.value.title = 'Title'; } else { newEntityData.value.title = null; } } - if (isText.value !== !!entityData.value.text) { + if (isText.value !== !!prevEntityData.text) { if (isText.value) { newEntityData.value.text = 'Text'; } else { newEntityData.value.text = null; } } - if (JSON.stringify(entityData.value) !== JSON.stringify(newEntityData.value)) { + if (JSON.stringify(prevEntityData) !== JSON.stringify(newEntityData.value)) { emit('saveChanges', newEntityData.value); } isModal.value = false; }; -const saveImage = async (newUrl: string, newWidth: number, newHeight: number) => { - entityData.value.imageUrl = newUrl; - entityData.value.image_width = newWidth; - entityData.value.image_height = newHeight; - await cropImage(newUrl, entityData.value); +const saveImage = async ( + newUrl: string, + newWidth: number, + newFileWidth: number, + newFileHeight: number +) => { + newEntityData.value.image_url = newUrl; + newEntityData.value.image_width = newWidth; + newEntityData.value.file_width = newFileWidth; + newEntityData.value.file_height = newFileHeight; + await cropImage(newUrl, newEntityData.value); isModalCropImage.value = false; }; const toggleConfirmDeleteImageModal = () => { isModalToDeleteImage.value = !isModalToDeleteImage.value; }; +const returnOriginalSize = () => { + const newState = entityData.value; + newState.image_url = newState.image_url_initial; + newState.file_width = newState.file_width_initial; + newState.file_height = newState.file_height_initial; + newEntityData.value = newState; + emit('returnOriginalSize'); +}; const deleteImage = () => { deleteEntity(entityData.value.entity_uuid); isModalToDeleteImage.value = false; @@ -92,7 +104,6 @@ const openCropImageModal = () => (isModalCropImage.value = true); </button> <Modal v-model:isVisible="isModal" theme="black" width="90%" ><template #header><h3 class="w-max mx-auto">Edit image block</h3></template> - <!-- <pre>{{ newEntityData }}</pre>--> <CropImageModal v-model:isVisible="isModalCropImage" v-model:imageInfo="newEntityData" @@ -125,7 +136,10 @@ const openCropImageModal = () => (isModalCropImage.value = true); {{ newEntityData.title ?? 'Title' }} </h3> </div> - <div style="gap: 32px" class="flex" :style="`height: ${newEntityData.image_height / 2}px`"> + <div + :style="`gap: 32px; justify-content: ${newEntityData.entity_position === 'right' ? 'end' : newEntityData.entity_position === 'left' ? 'start' : 'center'};`" + class="flex w-full" + > <div :class="[ 'relative leading-none', @@ -133,69 +147,79 @@ const openCropImageModal = () => (isModalCropImage.value = true); 'order-3': newEntityData.text_position === 'left' } ]" - :style="`width: ${newEntityData.image_width / 2}px; height: ${newEntityData.image_height / 2}px; min-width: 50px; min-height: 50px`" + :style="`width: ${newEntityData.image_width}%`" > <img - :src="entityData?.imageUrl" + :src="newEntityData?.image_url" :alt="`Image ${newEntityData?.title}` || 'Image'" - :width="newEntityData.image_width / 2" - :height="newEntityData.image_height / 2" - style="min-height: 50px; max-height: 350px" + style="max-height: 350px" class="object-contain order-1" /> </div> <div v-show="isText" - class="textContainer grow relative leading-none border-2 border-dashed rounded-2xl" - :style="`border-color: var(--${themeColor}-400); width: ${textContainerWidth / 2}px; height: ${newEntityData.image_height / 2}px`" + class="textContainer relative leading-none border-2 border-dashed rounded-2xl indent-5 order-2 p-1 overflow-hidden contain-inline-size text" + :style="`border-color: var(--${themeColor}-400); width: ${textContainerWidth}%; font-size: ${newEntityData.font_size / 2}px`" > - <p - class="w-full indent-5 leading-normal order-2 p-1 overflow-hidden contain-inline-size text" - :style="`font-size: ${newEntityData.font_size / 2}px; width: ${textContainerWidth / 2}px; height: ${newEntityData.image_height / 2}px;`" - > - {{ newEntityData.text ?? 'Text' }} - </p> + {{ newEntityData.text ?? 'Text' }} </div> </div> </section> </div> - <div class="absolute top-4 right-16 z-10 hover:brightness-80 transition-all"> + <div class="absolute flex gap-4 top-4 left-4 z-10 hover:brightness-80 transition-all"> <Button - label="Delete" + label="Save" textColor="white" - theme="red" + :theme="themeColor" textStyle="bold" size="medium" - @click.prevent="toggleConfirmDeleteImageModal" + @click.prevent="saveChanges" > <template #icon> - <TrashIcon color="white" size="25" /> + <SaveIcon color="white" size="25" /> + </template> + </Button> + <Button + v-if="newEntityData.file_width > 200 || newEntityData.file_height > 200" + label="Crop image" + textColor="black" + theme="white" + textStyle="bold" + size="medium" + @click.prevent="openCropImageModal" + > + <template #icon> + <CropIcon color="black" size="25" /> </template> </Button> </div> - <div class="absolute flex gap-4 top-4 left-4 z-10 hover:brightness-80 transition-all"> + <div class="absolute flex gap-4 top-4 right-16 z-10 hover:brightness-80 transition-all"> <Button - label="Save" + v-show=" + newEntityData.file_width !== newEntityData.file_width_initial || + newEntityData.file_height !== newEntityData.file_height_initial + " + label="Original size" textColor="white" :theme="themeColor" textStyle="bold" size="medium" - @click.prevent="saveChanges" + @click.prevent="returnOriginalSize" > <template #icon> - <SaveIcon color="white" size="25" /> + <TrashIcon color="white" size="25" /> </template> </Button> <Button - label="CropImage" - textColor="black" - theme="white" + label="Delete" + textColor="white" + theme="red" textStyle="bold" size="medium" - @click.prevent="openCropImageModal" + @click.prevent="toggleConfirmDeleteImageModal" > <template #icon> - <CropIcon color="black" size="25" /> + <TrashIcon color="white" size="25" /> </template> </Button> </div> diff --git a/src/components/entities/settings/ParagraphSettings.vue b/src/components/entities/settings/ParagraphSettings.vue index 8f434d8726775d1bde5ab9d7ad6aa1fb9df52379..c969552ed9616e86acc22198be53c118796708cf 100644 --- a/src/components/entities/settings/ParagraphSettings.vue +++ b/src/components/entities/settings/ParagraphSettings.vue @@ -10,8 +10,8 @@ interface Props { const props = defineProps<Props>(); const emit = defineEmits(['saveChanges']); const entityData = computed(() => props.entityData); +const prevEntityData = { ...entityData.value }; const newEntityData = ref({ ...entityData.value }); -watch(entityData, () => (newEntityData.value = entityData.value)); const isModal = ref<boolean>(false); const isModalToDeleteParagraph = ref<boolean>(false); const changeFontSize = (newSize: '16' | '20' | '24' | '40' | '64') => { @@ -20,8 +20,8 @@ const changeFontSize = (newSize: '16' | '20' | '24' | '40' | '64') => { }; const themeColor: TTheme = cookies.get('favorite_color'); const themeColorConverted = convertThemeToColorWhiteDefault(themeColor); -const isTitle = ref(!!entityData.value.title); -const isEntityWidthFull = ref(entityData.value.paragraph_size === 'full'); +const isTitle = ref(!!newEntityData.value.title); +const isEntityWidthFull = ref(newEntityData.value.paragraph_size === 'full'); const maxLines = computed(() => { if (isTitle.value) { @@ -84,17 +84,17 @@ const entityTitlePositionOptions = ref([ ]); const saveChanges = () => { const entityPosition = isEntityWidthFull.value ? 'full' : 'half'; - if (entityPosition !== entityData.value.entity_position) { + if (entityPosition !== prevEntityData.entity_position) { newEntityData.value.paragraph_size = entityPosition; } - if (isTitle.value !== !!entityData.value.title) { + if (isTitle.value !== !!prevEntityData.title) { if (isTitle.value) { newEntityData.value.title = 'Title'; } else { newEntityData.value.title = null; } } - if (JSON.stringify(entityData.value) !== JSON.stringify(newEntityData.value)) { + if (JSON.stringify(prevEntityData) !== JSON.stringify(newEntityData.value)) { emit('saveChanges', newEntityData.value); } isModal.value = false; diff --git a/src/components/entities/settings/lists/ImageSettingsList.vue b/src/components/entities/settings/lists/ImageSettingsList.vue index 576207c8246aefcb649d36f013ed837560636d90..4d6ed8e055229c7b9286165c90d35350894f0b5a 100644 --- a/src/components/entities/settings/lists/ImageSettingsList.vue +++ b/src/components/entities/settings/lists/ImageSettingsList.vue @@ -12,7 +12,7 @@ import { entityTextPositionOptions, isEntityWidthFullOptions } from './options'; -import { getImageScalesToRemove, scaleImage } from '@/app/helpers/images'; +import { filterImageScaleOptions, scaleImage } from '@/app/helpers/images'; interface Props { newEntityData: IImage; isTitle: boolean; @@ -28,16 +28,15 @@ const emit = defineEmits([ 'update:isEntityWidthFull' ]); const { newEntityData, isTitle, isText, isEntityWidthFull } = useVModels(props, emit); -const scalesToRemove = getImageScalesToRemove(newEntityData.value); -if (scalesToRemove.length) { - imageScaleOptions.value = imageScaleOptions.value.filter( - (item) => !~scalesToRemove.indexOf(item.label) +const scales = ref(imageScaleOptions); +scales.value = filterImageScaleOptions(newEntityData.value, isText.value, isEntityWidthFull.value); +watch([isText, isEntityWidthFull], () => { + scales.value = filterImageScaleOptions( + newEntityData.value, + isText.value, + isEntityWidthFull.value ); - for (let i = 0; i < imageScaleOptions.value.length; i++) { - imageScaleOptions.value[i].value = i; - } -} -console.log('imageScaleOptions: ', imageScaleOptions.value); +}); watch( () => newEntityData.value.image_scale, (cur, prev) => { @@ -54,8 +53,8 @@ watch( v-model:value="newEntityData.image_scale" width="300px" size="small" - :max="imageScaleOptions.length - 1" - :options="imageScaleOptions" + :max="scales.length - 1" + :options="scales" :isSmooth="true" backgroundColor="white" :theme="themeColor" @@ -110,18 +109,22 @@ watch( </div> </li> <li class="flex flex-col items-center justify-between gap-4" style="min-width: 150px"> - <div> - <p class="py-2 text-center">Text</p> - <div class="flex items-center"> - <ToggleButton - v-model:value="isText" - :theme="themeColor" - :options="entityIsTextOptions" - rounded="true" - :border="themeColor" - :activeBGColor="themeColor" - /> - </div> + <div style="height: 108px"> + <Transition name="fading"> + <div v-show="newEntityData.image_width <= 75" class="flex flex-col items-center"> + <p class="py-2 text-center">Text</p> + <div class="flex items-center"> + <ToggleButton + v-model:value="isText" + :theme="themeColor" + :options="entityIsTextOptions" + rounded="true" + :border="themeColor" + :activeBGColor="themeColor" + /> + </div> + </div> + </Transition> </div> <div style="height: 108px" class="flex gap-8 items-center justify-between"> <Transition name="fading"> @@ -143,7 +146,10 @@ watch( </div> <div style="height: 108px" class="flex gap-8 items-center justify-between"> <Transition name="fading"> - <div v-show="isText" class="flex flex-col items-center"> + <div + v-show="isText && newEntityData.image_width <= 50" + class="flex flex-col items-center" + > <p class="py-2">Text width</p> <ToggleButton v-model:value="isEntityWidthFull" diff --git a/src/components/entities/settings/lists/options.ts b/src/components/entities/settings/lists/options.ts index e0b10428b1f475402dbc8c2ff7bfa94a663bd513..df95f941edb8995ef58816eff2147db1961ebaf1 100644 --- a/src/components/entities/settings/lists/options.ts +++ b/src/components/entities/settings/lists/options.ts @@ -1,6 +1,6 @@ import type { IToggleButtonOption, ISliderOption } from '@/app/interfaces/ui'; -const imageScaleOptions = ref<ISliderOption[]>([ +const imageScaleOptions: ISliderOption[] = [ { label: 'x0.25', value: '0', @@ -41,7 +41,7 @@ const imageScaleOptions = ref<ISliderOption[]>([ value: '7', color: 'var(--red-800)' } -]); +]; const entityIsTitleOptions = ref<IToggleButtonOption[]>([ { label: 'Off', diff --git a/src/modules/CropImageModal.vue b/src/modules/CropImageModal.vue index 6977648828d31e2aad8663586e3062d70b965296..31e0be0c0f7bb7fc65b3cc9cbdd3af00726f3c62 100644 --- a/src/modules/CropImageModal.vue +++ b/src/modules/CropImageModal.vue @@ -23,52 +23,6 @@ const stageSize = ref({ height: 200 }); -watch( - () => isVisible.value, - () => { - if (imageInfo.value.imageUrl && isVisible.value) { - imageInstance.src = imageInfo.value.imageUrl; - imageInstance.onload = () => { - const imageHeight = imageInfo.value.image_height; - const imageWidth = imageInfo.value.image_width; - imageInstance.src = imageInfo.value.imageUrl; - if (imageWidth < (imageHeight * windowWidth.value) / windowHeight.value) { - imageInstance.onload = () => { - if (imageHeight < windowHeight.value * 0.75 - 20) { - stageSize.value.width = imageWidth; - stageSize.value.height = imageHeight; - imageInstance.width = stageSize.value.width; - imageInstance.height = stageSize.value.height; - } else { - stageSize.value.height = windowHeight.value * 0.75 - 20; - imageInstance.height = stageSize.value.height; - imageInstance.width = +(stageSize.value.height / imageHeight).toFixed(2) * imageWidth; - stageSize.value.width = imageInstance.width; - } - }; - } else { - imageInstance.onload = () => { - if (imageWidth < windowWidth.value * 0.63 - 60) { - stageSize.value.width = imageWidth; - stageSize.value.height = imageHeight; - imageInstance.width = stageSize.value.width; - imageInstance.height = stageSize.value.height; - } else { - stageSize.value.width = windowWidth.value * 0.63 - 60; - imageInstance.width = stageSize.value.width; - imageInstance.height = +(stageSize.value.width / imageWidth).toFixed(2) * imageHeight; - stageSize.value.height = imageInstance.height; - } - }; - } - }; - } - }, - { - immediate: true - } -); - const modalWidth = computed(() => { if ( stageSize.value.width < windowWidth.value * 0.63 - 60 || @@ -77,13 +31,69 @@ const modalWidth = computed(() => { return (100 * (stageSize.value.width + 60)) / windowWidth.value; } else return 63; }); + +watch( + () => isVisible.value, + () => { + if (!imageInfo.value.image_url || !isVisible.value) return; + + imageInstance.src = imageInfo.value.image_url; + imageInstance.onload = () => { + const imageWidth = (imageInfo.value.image_width / 100) * (windowWidth.value - 128); + const imageHeight = (imageWidth / imageInfo.value.file_width) * imageInfo.value.file_height; + console.log(`file width: ${imageInfo.value.file_width}, + file height: ${imageInfo.value.file_height}`); + console.log(`width: ${imageWidth}, + height: ${imageHeight}`); + imageInstance.src = imageInfo.value.image_url; + if (imageWidth < (imageHeight * windowWidth.value) / windowHeight.value) { + imageInstance.onload = () => { + if (imageHeight < windowHeight.value * 0.75 - 20) { + stageSize.value.width = imageWidth; + stageSize.value.height = imageHeight; + imageInstance.width = stageSize.value.width; + imageInstance.height = stageSize.value.height; + } else { + stageSize.value.height = windowHeight.value * 0.75 - 20; + imageInstance.height = stageSize.value.height; + imageInstance.width = +(stageSize.value.height / imageHeight).toFixed(2) * imageWidth; + stageSize.value.width = imageInstance.width; + } + }; + } else { + imageInstance.onload = () => { + if (imageWidth < windowWidth.value * 0.63 - 60) { + stageSize.value.width = imageWidth; + stageSize.value.height = imageHeight; + imageInstance.width = stageSize.value.width; + imageInstance.height = stageSize.value.height; + } else { + stageSize.value.width = windowWidth.value * 0.63 - 60; + imageInstance.width = stageSize.value.width; + imageInstance.height = +(stageSize.value.width / imageWidth).toFixed(2) * imageHeight; + stageSize.value.height = imageInstance.height; + } + }; + } + }; + }, + { + immediate: true + } +); const onCropperChange = async ({ canvas }) => { imageInstance.width = canvas.width; imageInstance.height = canvas.height; finalImageUrl.value = canvas.toDataURL(); }; const submitForm = () => { - emit('saveImage', finalImageUrl.value, imageInstance.width, imageInstance.height); + emit( + 'saveImage', + finalImageUrl.value, + Math.ceil((imageInstance.width / windowWidth.value) * 100), + imageInstance.width, + imageInstance.height + ); finalImageUrl.value = ''; }; </script> diff --git a/src/modules/entities/EntityItem.vue b/src/modules/entities/EntityItem.vue index bf826024af4df2e196220067423cab7e6f491e2f..c5112fa24f6f0facc2d3a6815c1f516312a2081a 100644 --- a/src/modules/entities/EntityItem.vue +++ b/src/modules/entities/EntityItem.vue @@ -25,7 +25,7 @@ const entity = useVModel(props, 'entity', emit); /> <ImageItem v-if="entity.entity_type === 'image'" - :entityData="entity as IImage" + v-model:entityData="entity as IImage" :isEditMode="isEditMode" /> </div> diff --git a/src/modules/entities/ImageItem.vue b/src/modules/entities/ImageItem.vue index 7cb32de3ac69745dbf498a26583bcf757b8bc8f9..1713958cb6dba063f683713f0803f12f222d7cff 100644 --- a/src/modules/entities/ImageItem.vue +++ b/src/modules/entities/ImageItem.vue @@ -1,17 +1,18 @@ <script setup lang="ts"> -import { useWindowSize } from '@vueuse/core'; import type { IImage } from '@/app/interfaces/entities'; -import { editEntity } from '@/app/helpers'; +import { editEntity, returnOriginalImageSize } from '@/app/helpers'; import { cropImage } from '@/app/helpers/images'; import type { IEntity } from '@/app/interfaces/environment'; import { useDataStore } from '@/app/stores/data'; +import { useVModel } from '@vueuse/core'; interface Props { entityData: IImage; isEditMode: boolean; } const props = defineProps<Props>(); -const entityData = ref(props.entityData); +const emit = defineEmits(['update:entityData']); +const entityData = useVModel(props, 'entityData', emit); const dataStore = useDataStore(); const entities = computed(() => dataStore.entities); @@ -21,12 +22,10 @@ const entityIndex = computed(() => const entitiesLength = computed(() => entities.value.length); const isModalCropImage = ref<boolean>(false); -const { width: windowWidth } = useWindowSize(); const textContainerWidth = computed(() => { - if (entityData.value?.paragraph_size === 'half') - return (windowWidth.value - 160 - entityData.value.image_width) / 2; - return windowWidth.value - 160 - entityData.value.image_width; + if (entityData.value?.paragraph_size === 'half') return (100 - entityData.value.image_width) / 2; + return 100 - entityData.value.image_width; }); const editTitle = () => { @@ -39,22 +38,20 @@ const saveChanges = (newState: IImage) => { editEntity(newState); entityData.value = newState; }; -const saveImage = async (newUrl: string, newWidth: number, newHeight: number) => { - entityData.value.imageUrl = newUrl; +const returnOriginalSize = () => { + const newState = entityData.value; + newState.file_width = newState.file_width_initial; + newState.file_height = newState.file_height_initial; + entityData.value = newState; + returnOriginalImageSize(newState); + // entityData.value.image_url = newState.image_url_initial; +}; +const saveImage = async (newUrl: string, newWidth: number) => { + entityData.value.image_url = newUrl; entityData.value.image_width = newWidth; - entityData.value.image_height = newHeight; await cropImage(newUrl, entityData.value); isModalCropImage.value = false; }; -const scaleImage = (scale: string) => { - const initialWidth = Math.ceil(entityData.value.image_width / +entityData.value.image_scale); - entityData.value.image_width = initialWidth * +scale; - const initialHeight = Math.ceil(entityData.value.image_height / +entityData.value.image_scale); - entityData.value.image_height = initialHeight * +scale; - entityData.value.image_scale = scale; - editEntity({ ...entityData.value }); -}; -const openCropImageModal = () => (isModalCropImage.value = true); </script> <template> @@ -80,7 +77,17 @@ const openCropImageModal = () => (isModalCropImage.value = true); :isEditMode="isEditMode" @editTitle="editTitle" /> - <div style="gap: 32px" class="flex" :style="`height: ${entityData.image_height}px`"> + <div + style="gap: 32px" + :class="[ + 'flex', + { + 'justify-start': entityData.entity_position === 'left', + 'justify-center': entityData.entity_position === 'center', + 'justify-end': entityData.entity_position === 'right' + } + ]" + > <div :class="[ 'imageContainer relative leading-none', @@ -88,13 +95,11 @@ const openCropImageModal = () => (isModalCropImage.value = true); 'order-3': entityData.text_position === 'left' } ]" - :style="`width: ${entityData.image_width}px; height: ${entityData.image_height}px; min-width: 100px; min-height: 100px`" + :style="`width: ${entityData.image_width}%`" > <img - :src="entityData?.imageUrl" + :src="entityData?.image_url" :alt="`Image ${entityData?.title}` || 'Image'" - :width="entityData.image_width" - :height="entityData.image_height" style="min-height: 100px; max-height: 700px" class="object-contain order-1" /> @@ -102,7 +107,7 @@ const openCropImageModal = () => (isModalCropImage.value = true); <div v-show="entityData.text || entityData.text === ''" class="textContainer relative leading-none" - :style="`width: ${textContainerWidth}px; height: ${entityData.image_height}px`" + :style="`width: ${textContainerWidth}%`" > <textarea ref="textarea" @@ -115,13 +120,18 @@ const openCropImageModal = () => (isModalCropImage.value = true); ]" placeholder="Enter text..." rows="7" - :style="`font-size: ${entityData.font_size}px; height: ${entityData.image_height}px;`" + :style="`font-size: ${entityData.font_size}px`" spellcheck="false" @change="editText" /> </div> </div> - <ImageSettings v-if="isEditMode" :entityData="entityData" @saveChanges="saveChanges" /> + <ImageSettings + v-if="isEditMode" + :entityData="entityData" + @saveChanges="saveChanges" + @returnOriginalSize="returnOriginalSize" + /> <EntityPositionSettings v-if="isEditMode && entitiesLength > 1" :entityUuid="entityData.entity_uuid" diff --git a/src/output.css b/src/output.css index 4e7c8f97d6b7038322fd1d314905d3cf5bf0be47..bd7b43be6cc9f423bf206be85719c2a6b6ccb1ed 100644 --- a/src/output.css +++ b/src/output.css @@ -638,6 +638,10 @@ video { z-index: 10; } +.z-20 { + z-index: 20; +} + .z-40 { z-index: 40; } @@ -646,10 +650,6 @@ video { z-index: 50; } -.z-20 { - z-index: 20; -} - .order-1 { order: 1; } diff --git a/src/pages/[uuid]/SheetPage.vue b/src/pages/[uuid]/SheetPage.vue index fead8d6153a6781d09be45e20c487f7b8e684b61..08773d6bbef9d59d2a773d57727d37fd8d7419fe 100644 --- a/src/pages/[uuid]/SheetPage.vue +++ b/src/pages/[uuid]/SheetPage.vue @@ -26,7 +26,7 @@ const isMenuVisible = ref<boolean>(false); const isEditMode = ref<boolean>(false); const isModalUploadFile = ref<boolean>(false); const backgroundImageInfo = ref<IImageMainInfo>({ - imageUrl: backgroundUrl.value, + image_url: backgroundUrl.value, image_width: 0, image_height: 0 }); @@ -64,7 +64,7 @@ const uploadFile = ($event: Event) => { const url = URL.createObjectURL(file); image.src = url; image.onload = function () { - backgroundImageInfo.value.imageUrl = url; + backgroundImageInfo.value.image_url = url; backgroundImageInfo.value.image_width = image.width; backgroundImageInfo.value.image_height = image.height; isModalUploadFile.value = true; diff --git a/src/shared/ui/Button.vue b/src/shared/ui/Button.vue index 161a8fd62ab11eee226785e1d92061d55184c828..54968a63aa8e95560b4d586deb7d1a11d4c2158d 100644 --- a/src/shared/ui/Button.vue +++ b/src/shared/ui/Button.vue @@ -144,6 +144,9 @@ const buttonPadding = computed(() => { border-radius: 5px; transition: filter 0.2s ease-in-out; } +.text { + line-height: 1; +} .icon { display: flex; align-items: center; diff --git a/src/shared/ui/Modal.vue b/src/shared/ui/Modal.vue index 6d9f41c3409eff858fee6db7089ca0296999b292..0b58d426eb83cd16f5ab1bd0e9386464aeddcdd3 100644 --- a/src/shared/ui/Modal.vue +++ b/src/shared/ui/Modal.vue @@ -116,7 +116,7 @@ const isVisible = useVModel(props, 'isVisible', emit); .modalHeader { font-weight: bold; font-size: 1.5rem; - padding-right: 50px; + padding: 0 50px; margin-bottom: 20px; min-height: 1rem; }