Skip to content

Commit

Permalink
feat: image cropping (misskey-dev#8808)
Browse files Browse the repository at this point in the history
* wip

* wip

* wip
  • Loading branch information
syuilo authored Jun 11, 2022
1 parent 1838511 commit ecb3c43
Show file tree
Hide file tree
Showing 9 changed files with 424 additions and 98 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ You should also include the user name that made the change.
- プッシュ通知にクリックやactionを設定 #7667 @tamaina
- ドライブに画像ファイルをアップロードするときオリジナル画像を破棄してwebpublicのみ保持するオプション @tamaina
- Server: always remove completed tasks of job queue @Johann150
- Client: アバターの設定で画像をクロップできるように @syuilo
- Client: make emoji stand out more on reaction button @Johann150
- Client: display URL of QR code for TOTP registration @tamaina
- Client: render quote renote CWs as MFM @pixeldesu
Expand Down
2 changes: 2 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,8 @@ oneWeek: "1週間"
reflectMayTakeTime: "反映されるまで時間がかかる場合があります。"
failedToFetchAccountInformation: "アカウント情報の取得に失敗しました"
rateLimitExceeded: "レート制限を超えました"
cropImage: "画像のクロップ"
cropImageAsk: "画像をクロップしますか?"

_emailUnavailable:
used: "既に使用されています"
Expand Down
19 changes: 10 additions & 9 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
"dependencies": {
"@discordapp/twemoji": "14.0.2",
"@fortawesome/fontawesome-free": "6.1.1",
"@rollup/plugin-alias": "3.1.9",
"@rollup/plugin-json": "4.1.0",
"@syuilo/aiscript": "0.11.1",
"@vitejs/plugin-vue": "2.3.3",
"@vue/compiler-sfc": "3.2.37",
"abort-controller": "3.0.0",
"autobind-decorator": "2.4.0",
"autosize": "5.0.1",
Expand All @@ -26,6 +30,7 @@
"chartjs-plugin-zoom": "1.2.1",
"compare-versions": "4.1.3",
"content-disposition": "0.5.4",
"cropperjs": "2.0.0-beta",
"date-fns": "2.28.0",
"escape-regexp": "0.0.1",
"eventemitter3": "4.0.7",
Expand All @@ -51,6 +56,7 @@
"random-seed": "0.3.0",
"reflect-metadata": "0.1.13",
"rndstr": "1.0.0",
"rollup": "2.75.6",
"s-age": "1.1.2",
"sass": "1.52.3",
"seedrandom": "3.0.5",
Expand All @@ -64,21 +70,16 @@
"tsc-alias": "1.6.9",
"tsconfig-paths": "4.0.0",
"twemoji-parser": "14.0.0",
"typescript": "4.7.3",
"uuid": "8.3.2",
"v-debounce": "0.1.2",
"vanilla-tilt": "1.7.2",
"vite": "2.9.10",
"vue": "3.2.37",
"vue-prism-editor": "2.0.0-alpha.2",
"vue-router": "4.0.16",
"vuedraggable": "4.0.1",
"websocket": "1.0.34",
"@vitejs/plugin-vue": "2.3.3",
"@vue/compiler-sfc": "3.2.37",
"@rollup/plugin-alias": "3.1.9",
"@rollup/plugin-json": "4.1.0",
"rollup": "2.75.6",
"typescript": "4.7.3",
"vite": "2.9.10",
"ws": "8.8.0"
},
"devDependencies": {
Expand All @@ -102,11 +103,11 @@
"@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.27.1",
"@typescript-eslint/parser": "5.27.1",
"eslint": "8.17.0",
"eslint-plugin-vue": "9.1.0",
"cross-env": "7.0.3",
"cypress": "10.0.3",
"eslint": "8.17.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-vue": "9.1.0",
"start-server-and-test": "1.14.0"
}
}
171 changes: 171 additions & 0 deletions packages/client/src/components/cropper-dialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<template>
<XModalWindow
ref="dialogEl"
:width="800"
:height="500"
:scroll="false"
:with-ok-button="true"
@close="cancel()"
@ok="ok()"
@closed="$emit('closed')"
>
<template #header>{{ $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="file.url" style="display: none;" @load="onImageLoad">
</div>
</div>
</template>
</XModalWindow>
</template>

<script lang="ts" setup>
import { nextTick, onMounted } from 'vue';
import * as misskey from 'misskey-js';
import Cropper from 'cropperjs';
import tinycolor from 'tinycolor2';
import XModalWindow from '@/components/ui/modal-window.vue';
import * as os from '@/os';
import { $i } from '@/account';
import { defaultStore } from '@/store';
import { apiUrl } from '@/config';
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;
}>();
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);
formData.append('i', $i.token);
if (defaultStore.state.uploadFolder) {
formData.append('folderId', defaultStore.state.uploadFolder);
}
fetch(apiUrl + '/drive/files/create', {
method: 'POST',
body: formData,
})
.then(response => response.json())
.then(f => {
res(f);
});
});
});
os.promiseDialog(promise);
const f = await promise;
emit('ok', f);
dialogEl.close();
};
const cancel = () => {
emit('cancel');
dialogEl.close();
};
const onImageLoad = () => {
loading = false;
if (cropper) {
cropper.getCropperImage()!.$center('contain');
cropper.getCropperSelection()!.$center();
}
};
onMounted(() => {
cropper = new Cropper(imgEl, {
});
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>
Loading

0 comments on commit ecb3c43

Please sign in to comment.