magnetar/fe_calckey/frontend/client/src/components/MkCropperDialog.vue

188 lines
4.8 KiB
Vue

<template>
<XModalWindow
ref="dialogEl"
:width="800"
:height="500"
:scroll="false"
:with-ok-button="true"
@close="cancel()"
@ok="ok()"
@closed="$emit('closed')"
>
<template #header>{{ i18n.ts.cropImage }}</template>
<template #default="{ width, height }">
<div
class="mk-cropper-dialog"
:style="`--vw: ${width}px; --vh: ${height}px;`"
>
<Transition name="fade">
<div v-if="loading" class="loading">
<MkLoading />
</div>
</Transition>
<div class="container">
<img
ref="imgEl"
:src="imgUrl"
style="display: none"
@load="onImageLoad"
/>
</div>
</div>
</template>
</XModalWindow>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, ref } from "vue";
import * as misskey from "calckey-js";
import Cropper from "cropperjs";
import tinycolor from "tinycolor2";
import XModalWindow from "@/components/MkModalWindow.vue";
import * as os from "@/os";
import { $i } from "@/account";
import { defaultStore } from "@/store";
import { apiUrl, url } from "@/config";
import { query } from "@/scripts/url";
import { i18n } from "@/i18n";
const emit = defineEmits<{
(ev: "ok", cropped: misskey.entities.DriveFile): void;
(ev: "cancel"): void;
(ev: "closed"): void;
}>();
const props = defineProps<{
file: misskey.entities.DriveFile;
aspectRatio: number;
}>();
const imgUrl = `${url}/proxy/image.webp?${query({
url: props.file.url,
})}`;
let dialogEl = ref<InstanceType<typeof XModalWindow>>();
let imgEl = ref<HTMLImageElement>();
let cropper: Cropper | null = null;
let loading = ref(true);
const ok = async () => {
const promise = new Promise<misskey.entities.DriveFile>(async (res) => {
const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas();
croppedCanvas.toBlob((blob) => {
const formData = new FormData();
formData.append("file", blob);
if (defaultStore.state.uploadFolder) {
formData.append("folderId", defaultStore.state.uploadFolder);
}
fetch(apiUrl + "/drive/files/create", {
method: "POST",
body: formData,
headers: {
authorization: `Bearer ${$i.token}`,
},
})
.then((response) => response.json())
.then((f) => {
res(f);
});
});
});
os.promiseDialog(promise);
const f = await promise;
emit("ok", f);
dialogEl.value.close();
};
const cancel = () => {
emit("cancel");
dialogEl.value.close();
};
const onImageLoad = () => {
loading.value = false;
if (cropper) {
cropper.getCropperImage()!.$center("contain");
cropper.getCropperSelection()!.$center();
}
};
onMounted(() => {
cropper = new Cropper(imgEl.value, {});
const computedStyle = getComputedStyle(document.documentElement);
const selection = cropper.getCropperSelection()!;
selection.themeColor = tinycolor(
computedStyle.getPropertyValue("--accent"),
).toHexString();
selection.aspectRatio = props.aspectRatio;
selection.initialAspectRatio = props.aspectRatio;
selection.outlined = true;
window.setTimeout(() => {
cropper.getCropperImage()!.$center("contain");
selection.$center();
}, 100);
// モーダルオープンアニメーションが終わったあとで再度調整
window.setTimeout(() => {
cropper.getCropperImage()!.$center("contain");
selection.$center();
}, 500);
});
</script>
<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease 0.5s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.mk-cropper-dialog {
display: flex;
flex-direction: column;
width: var(--vw);
height: var(--vh);
position: relative;
> .loading {
position: absolute;
z-index: 10;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
-webkit-backdrop-filter: var(--blur, blur(10px));
backdrop-filter: var(--blur, blur(10px));
background: rgba(0, 0, 0, 0.5);
}
> .container {
flex: 1;
width: 100%;
height: 100%;
> ::v-deep(cropper-canvas) {
width: 100%;
height: 100%;
> cropper-selection > cropper-handle[action="move"] {
background: transparent;
}
}
}
}
</style>