update
This commit is contained in:
parent
c218a4ba5a
commit
a7176f8be6
|
@ -1,24 +1,30 @@
|
|||
export enum ClipPathTypes {
|
||||
RECT = 'rect',
|
||||
ELLIPSE = 'ellipse',
|
||||
POLYGON = 'polygon',
|
||||
}
|
||||
|
||||
export const CLIPPATHS = {
|
||||
rect: {
|
||||
name: '矩形',
|
||||
type: 'rect',
|
||||
type: ClipPathTypes.RECT,
|
||||
radius: '0',
|
||||
style: '',
|
||||
},
|
||||
roundRect: {
|
||||
name: '圆角矩形',
|
||||
type: 'rect',
|
||||
type: ClipPathTypes.RECT,
|
||||
radius: '10%',
|
||||
style: 'inset(0 0 0 0 round 10% 10% 10% 10%)',
|
||||
},
|
||||
ellipse: {
|
||||
name: '圆形',
|
||||
type: 'ellipse',
|
||||
type: ClipPathTypes.ELLIPSE,
|
||||
style: 'ellipse(50% 50% at 50% 50%)',
|
||||
},
|
||||
triangle: {
|
||||
name: '三角形',
|
||||
type: 'polygon',
|
||||
type: ClipPathTypes.POLYGON,
|
||||
style: 'polygon(50% 0%, 0% 100%, 100% 100%)',
|
||||
createPath: (width: number, height: number) => {
|
||||
return `M ${width / 2} 0 L 0 ${height} L ${width} ${height} Z`
|
||||
|
@ -26,7 +32,7 @@ export const CLIPPATHS = {
|
|||
},
|
||||
pentagon: {
|
||||
name: '五边形',
|
||||
type: 'polygon',
|
||||
type: ClipPathTypes.POLYGON,
|
||||
style: 'polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%)',
|
||||
createPath: (width: number, height: number) => {
|
||||
return `M ${width / 2} 0 L ${width} ${0.38 * height} L ${0.82 * width} ${height} L ${0.18 * width} ${height} L 0 ${0.38 * height} Z`
|
||||
|
@ -34,7 +40,7 @@ export const CLIPPATHS = {
|
|||
},
|
||||
rhombus: {
|
||||
name: '菱形',
|
||||
type: 'polygon',
|
||||
type: ClipPathTypes.POLYGON,
|
||||
style: 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)',
|
||||
createPath: (width: number, height: number) => {
|
||||
return `M ${width / 2} 0 L ${width} ${height / 2} L ${width / 2} ${height} L 0 ${height / 2} Z`
|
||||
|
@ -42,7 +48,7 @@ export const CLIPPATHS = {
|
|||
},
|
||||
star: {
|
||||
name: '五角星',
|
||||
type: 'polygon',
|
||||
type: ClipPathTypes.POLYGON,
|
||||
style: 'polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)',
|
||||
createPath: (width: number, height: number) => {
|
||||
return `M ${width / 2} 0 L ${0.61 * width} ${0.35 * height} L ${0.98 * width} ${0.35 * height} L ${0.68 * width} ${0.57 * height} L ${0.79 * width} ${0.91 * height} L ${0.50 * width} ${0.70 * height} L ${0.21 * width} ${0.91 * height} L ${0.32 * width} ${0.57 * height} L ${0.02 * width} ${0.35 * height} L ${0.39 * width} ${0.35 * height} Z`
|
||||
|
|
|
@ -26,3 +26,26 @@ export enum ElementLockCommands {
|
|||
LOCK = 'lock',
|
||||
UNLOCK = 'unlock',
|
||||
}
|
||||
|
||||
export type OperateBorderLineType = 't' | 'b' | 'l' | 'r'
|
||||
|
||||
export enum OperateBorderLineTypes {
|
||||
T = 't',
|
||||
B = 'b',
|
||||
L = 'l',
|
||||
R = 'r',
|
||||
}
|
||||
|
||||
export type OperateResizablePointType = 't-l' | 't-c' | 't-r' | 'm-l' | 'm-r' | 'b-l' | 'b-c' | 'b-r' | 'any'
|
||||
|
||||
export enum OperateResizablePointTypes {
|
||||
TL = 't-l',
|
||||
TC = 't-c',
|
||||
TR = 't-r',
|
||||
ML = 'm-l',
|
||||
MR = 'm-r',
|
||||
BL = 'b-l',
|
||||
BC = 'b-c',
|
||||
BR = 'b-r',
|
||||
ANY = 'any',
|
||||
}
|
|
@ -39,9 +39,9 @@ export interface PPTImageElement extends PPTElementBaseProps, PPTElementSizeProp
|
|||
filter?: string;
|
||||
clip?: {
|
||||
range: [[number, number], [number, number]];
|
||||
shape: string;
|
||||
shape: 'rect' | 'ellipse' | 'polygon';
|
||||
};
|
||||
flip?: string;
|
||||
flip?: { x?: number, y?: number };
|
||||
shadow?: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
class="editable-element"
|
||||
ref="elementRef"
|
||||
:id="'editable-element-' + elementInfo.elId"
|
||||
:style="{ zIndex: elementIndex }"
|
||||
>
|
||||
|
@ -10,7 +11,9 @@
|
|||
:canvasScale="canvasScale"
|
||||
:isActive="isActive"
|
||||
:isHandleEl="isHandleEl"
|
||||
:isActiveGroupElement="isActiveGroupElement"
|
||||
:isMultiSelect="isMultiSelect"
|
||||
:animationIndex="-1"
|
||||
:selectElement="selectElement"
|
||||
:rotateElement="rotateElement"
|
||||
:scaleElement="scaleElement"
|
||||
|
@ -59,12 +62,16 @@ export default defineComponent({
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isActiveGroupElement: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isMultiSelect: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
selectElement: {
|
||||
type: Function as PropType<(e: MouseEvent, element: PPTElement, canMove: boolean) => void>,
|
||||
type: Function as PropType<(e: MouseEvent, element: PPTElement, canMove?: boolean) => void>,
|
||||
required: true,
|
||||
},
|
||||
rotateElement: {
|
||||
|
|
|
@ -58,6 +58,16 @@ interface ClipData {
|
|||
path: string;
|
||||
}
|
||||
|
||||
export interface ClipedEmitData {
|
||||
range: ClipDataRange;
|
||||
position: {
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
type ScaleClipRangeType = 't-l' | 't-r' | 'b-l' | 'b-r'
|
||||
|
||||
export default defineComponent({
|
||||
|
@ -193,10 +203,12 @@ export default defineComponent({
|
|||
width: (topImgWrapperPosition.width - 100) / 100 * props.width,
|
||||
height: (topImgWrapperPosition.height - 100) / 100 * props.height,
|
||||
}
|
||||
emit('clip', {
|
||||
|
||||
const clipedEmitData: ClipedEmitData = {
|
||||
range: currentRange.value,
|
||||
position,
|
||||
})
|
||||
}
|
||||
emit('clip', clipedEmitData)
|
||||
}
|
||||
|
||||
const keyboardClip = (e: KeyboardEvent) => {
|
||||
|
|
|
@ -0,0 +1,368 @@
|
|||
<template>
|
||||
<div
|
||||
class="editable-element image"
|
||||
:class="{'lock': elementInfo.isLock}"
|
||||
:style="{
|
||||
top: elementInfo.top + 'px',
|
||||
left: elementInfo.left + 'px',
|
||||
width: elementInfo.width + 'px',
|
||||
height: elementInfo.height + 'px',
|
||||
transform: `rotate(${elementInfo.rotate}deg)`,
|
||||
}"
|
||||
@mousedown="handleSelectElement($event)"
|
||||
>
|
||||
<ImageClip
|
||||
v-if="isCliping"
|
||||
:imgUrl="elementInfo.imgUrl"
|
||||
:clipData="elementInfo.clip"
|
||||
:canvasScale="canvasScale"
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
:top="elementInfo.top"
|
||||
:left="elementInfo.left"
|
||||
:clipPath="clipShape.style"
|
||||
@clip="range => clip(range)"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="element-content"
|
||||
v-if="!isCliping"
|
||||
v-contextmenu="contextmenus"
|
||||
:style="{
|
||||
filter: elementInfo.shadow ? `drop-shadow(${elementInfo.shadow})` : '',
|
||||
transform: flip,
|
||||
}"
|
||||
>
|
||||
<RectBorder
|
||||
v-if="clipShape.type === 'rect'"
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
:radius="clipShape.radius"
|
||||
:borderColor="elementInfo.borderColor"
|
||||
:borderWidth="elementInfo.borderWidth"
|
||||
:borderStyle="elementInfo.borderStyle"
|
||||
/>
|
||||
<EllipseBorder
|
||||
v-else-if="clipShape.type === 'ellipse'"
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
:borderColor="elementInfo.borderColor"
|
||||
:borderWidth="elementInfo.borderWidth"
|
||||
:borderStyle="elementInfo.borderStyle"
|
||||
/>
|
||||
<PolygonBorder
|
||||
v-else-if="clipShape.type === 'polygon'"
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
:createPath="clipShape.createPath"
|
||||
:borderColor="elementInfo.borderColor"
|
||||
:borderWidth="elementInfo.borderWidth"
|
||||
:borderStyle="elementInfo.borderStyle"
|
||||
/>
|
||||
|
||||
<div class="img-wrapper" :style="{clipPath: clipShape.style}">
|
||||
<img
|
||||
:src="elementInfo.imgUrl"
|
||||
:draggable="false"
|
||||
:style="{
|
||||
top: imgPosition.top,
|
||||
left: imgPosition.left,
|
||||
width: imgPosition.width,
|
||||
height: imgPosition.height,
|
||||
filter: filter,
|
||||
}"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="operate"
|
||||
:class="{
|
||||
'active': isActive,
|
||||
'multi-select': isMultiSelect && isActive,
|
||||
'selected': isHandleEl,
|
||||
}"
|
||||
:style="{transform: `scale(${1 / canvasScale})`}"
|
||||
v-if="!isCliping"
|
||||
>
|
||||
<BorderLine
|
||||
class="el-border-line"
|
||||
v-for="line in borderLines"
|
||||
:key="line.type"
|
||||
:type="line.type"
|
||||
:style="line.style"
|
||||
/>
|
||||
<template v-if="!elementInfo.isLock && (isActiveGroupElement || !isMultiSelect)">
|
||||
<ResizablePoint
|
||||
class="el-resizable-point"
|
||||
v-for="point in resizablePoints"
|
||||
:key="point.type"
|
||||
:type="point.type"
|
||||
:style="point.style"
|
||||
@mousedown.stop="scaleElement($event, elementInfo, point.direction)"
|
||||
/>
|
||||
<RotateHandle
|
||||
class="el-rotate-handle"
|
||||
:style="{left: scaleWidth / 2 + 'px'}"
|
||||
@mousedown.stop="rotateElement(elementInfo)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<AnimationIndex v-if="animationIndex !== -1" :animationIndex="animationIndex" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref, PropType } from 'vue'
|
||||
|
||||
import { PPTImageElement } from '@/types/slides'
|
||||
import { ElementScaleHandler, OperateResizablePointTypes, OperateBorderLineTypes } from '@/types/edit'
|
||||
|
||||
import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
|
||||
import { OPERATE_KEYS } from '@/configs/element'
|
||||
|
||||
import RotateHandler from '@/views/_common/_operate/RotateHandler.vue'
|
||||
import ResizablePoint from '@/views/_common/_operate/ResizablePoint.vue'
|
||||
import BorderLine from '@/views/_common/_operate/BorderLine.vue'
|
||||
import AnimationIndex from '@/views/_common/_operate/AnimationIndex.vue'
|
||||
|
||||
import ImageClip, { ClipedEmitData } from './ImageClipHandler.vue'
|
||||
import ImageRectBorder from './ImageRectBorder.vue'
|
||||
import ImageEllipseBorder from './ImageEllipseBorder.vue'
|
||||
import ImagePolygonBorder from './ImagePolygonBorder.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'editable-element-image',
|
||||
components: {
|
||||
RotateHandler,
|
||||
ResizablePoint,
|
||||
BorderLine,
|
||||
AnimationIndex,
|
||||
ImageClip,
|
||||
ImageRectBorder,
|
||||
ImageEllipseBorder,
|
||||
ImagePolygonBorder,
|
||||
},
|
||||
props: {
|
||||
elementInfo: {
|
||||
type: Object as PropType<PPTImageElement>,
|
||||
required: true,
|
||||
},
|
||||
canvasScale: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isHandleEl: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isActiveGroupElement: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isMultiSelect: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
animationIndex: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
selectElement: {
|
||||
type: Function as PropType<(e: MouseEvent, element: PPTImageElement, canMove?: boolean) => void>,
|
||||
required: true,
|
||||
},
|
||||
rotateElement: {
|
||||
type: Function as PropType<(element: PPTImageElement) => void>,
|
||||
required: true,
|
||||
},
|
||||
scaleElement: {
|
||||
type: Function as PropType<(e: MouseEvent, element: PPTImageElement, command: ElementScaleHandler) => void>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const clipingImageElId = ref('')
|
||||
|
||||
const scaleWidth = computed(() => props.elementInfo ? props.elementInfo.width * props.canvasScale : 0)
|
||||
const scaleHeight = computed(() => props.elementInfo ? props.elementInfo.height * props.canvasScale : 0)
|
||||
const isCliping = computed(() => clipingImageElId.value === props.elementInfo.elId)
|
||||
|
||||
const imgPosition = computed(() => {
|
||||
if(!props.elementInfo || !props.elementInfo.clip) {
|
||||
return {
|
||||
top: '0',
|
||||
left: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}
|
||||
}
|
||||
|
||||
const [start, end] = props.elementInfo.clip.range
|
||||
|
||||
const widthScale = (end[0] - start[0]) / 100
|
||||
const heightScale = (end[1] - start[1]) / 100
|
||||
const left = start[0] / widthScale
|
||||
const top = start[1] / heightScale
|
||||
|
||||
return {
|
||||
left: -left + '%',
|
||||
top: -top + '%',
|
||||
width: 100 / widthScale + '%',
|
||||
height: 100 / heightScale + '%',
|
||||
}
|
||||
})
|
||||
|
||||
const clipShape = computed(() => {
|
||||
if(!props.elementInfo || !props.elementInfo.clip) return CLIPPATHS.rect
|
||||
const shape = props.elementInfo.clip.shape || ClipPathTypes.RECT
|
||||
|
||||
return CLIPPATHS[shape]
|
||||
})
|
||||
|
||||
const resizablePoints = computed(() => {
|
||||
return [
|
||||
{ type: OperateResizablePointTypes.TL, direction: OPERATE_KEYS.LEFT_TOP, style: {} },
|
||||
{ type: OperateResizablePointTypes.TC, direction: OPERATE_KEYS.TOP, style: {left: scaleWidth.value / 2 + 'px'} },
|
||||
{ type: OperateResizablePointTypes.TR, direction: OPERATE_KEYS.RIGHT_TOP, style: {left: scaleWidth.value + 'px'} },
|
||||
{ type: OperateResizablePointTypes.ML, direction: OPERATE_KEYS.LEFT, style: {top: scaleHeight.value / 2 + 'px'} },
|
||||
{ type: OperateResizablePointTypes.MR, direction: OPERATE_KEYS.RIGHT, style: {left: scaleWidth.value + 'px', top: scaleHeight.value / 2 + 'px'} },
|
||||
{ type: OperateResizablePointTypes.BL, direction: OPERATE_KEYS.LEFT_BOTTOM, style: {top: scaleHeight.value + 'px'} },
|
||||
{ type: OperateResizablePointTypes.BC, direction: OPERATE_KEYS.BOTTOM, style: {left: scaleWidth.value / 2 + 'px', top: scaleHeight.value + 'px'} },
|
||||
{ type: OperateResizablePointTypes.BR, direction: OPERATE_KEYS.RIGHT_BOTTOM, style: {left: scaleWidth.value + 'px', top: scaleHeight.value + 'px'} },
|
||||
]
|
||||
})
|
||||
|
||||
const borderLines = computed(() => {
|
||||
return [
|
||||
{ type: OperateBorderLineTypes.T, style: {width: scaleWidth.value + 'px'} },
|
||||
{ type: OperateBorderLineTypes.B, style: {top: scaleHeight.value + 'px', width: scaleWidth.value + 'px'} },
|
||||
{ type: OperateBorderLineTypes.L, style: {height: scaleHeight.value + 'px'} },
|
||||
{ type: OperateBorderLineTypes.R, style: {left: scaleWidth.value + 'px', height: scaleHeight.value + 'px'} },
|
||||
]
|
||||
})
|
||||
|
||||
const filter = computed(() => {
|
||||
if(!props.elementInfo.filter) return ''
|
||||
let filter = ''
|
||||
for(const key of Object.keys(props.elementInfo.filter)) {
|
||||
filter += `${key}(${props.elementInfo.filter[key]}) `
|
||||
}
|
||||
return filter
|
||||
})
|
||||
|
||||
const flip = computed(() => {
|
||||
if(!props.elementInfo.flip) return ''
|
||||
const { x, y } = props.elementInfo.flip
|
||||
if(x && y) return `rotateX(${x}deg) rotateY(${y}deg)`
|
||||
else if(x) return `rotateX(${x}deg)`
|
||||
else if(y) return `rotateY(${y}deg)`
|
||||
return ''
|
||||
})
|
||||
|
||||
const handleSelectElement = (e: MouseEvent) => {
|
||||
if(isCliping.value || props.elementInfo.isLock) return
|
||||
e.stopPropagation()
|
||||
props.selectElement(e, props.elementInfo)
|
||||
}
|
||||
|
||||
const clip = (data: ClipedEmitData) => {
|
||||
clipingImageElId.value = ''
|
||||
|
||||
if(!data) return
|
||||
|
||||
const { range, position } = data
|
||||
const originClip = props.elementInfo.clip || {}
|
||||
|
||||
const _props = {
|
||||
clip: { ...originClip, range },
|
||||
left: props.elementInfo.left + position.left,
|
||||
top: props.elementInfo.top + position.top,
|
||||
width: props.elementInfo.width + position.width,
|
||||
height: props.elementInfo.height + position.height,
|
||||
}
|
||||
console.log(_props)
|
||||
}
|
||||
|
||||
return {
|
||||
isCliping,
|
||||
imgPosition,
|
||||
clipShape,
|
||||
resizablePoints,
|
||||
borderLines,
|
||||
filter,
|
||||
flip,
|
||||
handleSelectElement,
|
||||
clip,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.editable-element {
|
||||
position: absolute;
|
||||
|
||||
&.lock .el-border-line {
|
||||
border-color: #888;
|
||||
}
|
||||
|
||||
&:hover .el-border-line {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.lock .element-content {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.element-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
cursor: move;
|
||||
|
||||
.img-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.operate {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
user-select: none;
|
||||
|
||||
&.active {
|
||||
.el-border-line,
|
||||
.el-resizable-point,
|
||||
.el-rotate-handle {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.multi-select:not(.selected) .el-border-line {
|
||||
border-color: rgba($color: $themeColor, $alpha: .5);
|
||||
}
|
||||
|
||||
.el-border-line,
|
||||
.el-resizable-point,
|
||||
.el-rotate-handle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,8 +1,6 @@
|
|||
<template>
|
||||
<div class="animation-index">
|
||||
<div class="index" v-for="(item, index) in animations" :key="index">
|
||||
{{index}}
|
||||
</div>
|
||||
{{animationIndex}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -13,8 +11,8 @@ import { PPTAnimation } from '@/types/slides'
|
|||
export default {
|
||||
name: 'animation-index',
|
||||
props: {
|
||||
animations: {
|
||||
type: Array as PropType<PPTAnimation[]>,
|
||||
animationIndex: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
@ -27,8 +25,6 @@ export default {
|
|||
top: 0;
|
||||
left: -22px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.index {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: #fff;
|
||||
|
|
|
@ -4,14 +4,13 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { PropType } from 'vue'
|
||||
|
||||
type BorderLineType = 't' | 'b' | 'l' | 'r'
|
||||
import { OperateBorderLineType } from '@/types/edit'
|
||||
|
||||
export default {
|
||||
name: 'border-line',
|
||||
props: {
|
||||
type: {
|
||||
type: String as PropType<BorderLineType>,
|
||||
type: String as PropType<OperateBorderLineType>,
|
||||
required: true,
|
||||
},
|
||||
isWide: {
|
||||
|
|
|
@ -4,14 +4,13 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { PropType } from 'vue'
|
||||
|
||||
type ResizablePointType = 't-l' | 't-c' | 't-r' | 'm-l' | 'm-r' | 'b-l' | 'b-c' | 'b-r' | 'any'
|
||||
import { OperateResizablePointType } from '@/types/edit'
|
||||
|
||||
export default {
|
||||
name: 'resizable-point',
|
||||
props: {
|
||||
type: {
|
||||
type: String as PropType<ResizablePointType>,
|
||||
type: String as PropType<OperateResizablePointType>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue