diff --git a/src/backend/router.ts b/src/backend/router.ts index 0e1078f..94162dc 100644 --- a/src/backend/router.ts +++ b/src/backend/router.ts @@ -38,7 +38,7 @@ router.get('/login', async ctx => { host = meta.uri.replace(/^https?:\/\//, ''); const name = 'みす廃あらーと'; const description = 'ついついノートしすぎていませんか?'; - const permission = ['write:notes', 'write:notifications']; + const permission = ['write:notes', 'write:notifications', 'write:drive', 'read:account', 'write:account']; if (meta.features.miauth) { // MiAuthを使用する diff --git a/src/frontend/components/NekomimiPage.tsx b/src/frontend/components/NekomimiPage.tsx index 59fc226..edca50a 100644 --- a/src/frontend/components/NekomimiPage.tsx +++ b/src/frontend/components/NekomimiPage.tsx @@ -1,21 +1,75 @@ import React, { ChangeEventHandler, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import ReactCrop, { Crop } from 'react-image-crop'; import 'react-image-crop/dist/ReactCrop.css'; +import { useDispatch } from 'react-redux'; +import { useGetSessionQuery } from '../services/session'; +import { showModal } from '../store/slices/screen'; export const NekomimiPage: React.VFC = () => { + const {t} = useTranslation(); + const dispatch = useDispatch(); + const [blobUrl, setBlobUrl] = useState(null); + const [fileName, setFileName] = useState(null); + const [percentage, setPercentage] = useState(0); + const [isUploading, setUploading] = useState(false); const [image, setImage] = useState(null); const [crop, setCrop] = useState>({unit: '%', width: 100, aspect: 1 / 1}); const [completedCrop, setCompletedCrop] = useState(); const previewCanvasRef = useRef(null); + const {data} = useGetSessionQuery(undefined); + + const beginUpload = async () => { + + if (!previewCanvasRef.current) return; + if (!data) return; + + const canvas = previewCanvasRef.current; + const blob = await new Promise(res => canvas.toBlob(res)); + if (!blob) return; + + const formData = new FormData(); + formData.append('i', data.token); + formData.append('force', 'true'); + formData.append('file', blob); + formData.append('name', `(Cropped) ${fileName ?? 'File'}`); + + await new Promise((res, rej) => { + const xhr = new XMLHttpRequest(); + xhr.open('POST', `https://${data.host}/api/drive/files/create`, true); + xhr.onload = (e) => { + setPercentage(100); + const {id: avatarId} = JSON.parse(xhr.responseText); + fetch(`https://${data.host}/api/i/update`, { + method: 'POST', + body: JSON.stringify({ i: data.token, avatarId }), + }).then(() => res()).catch(rej); + }; + xhr.onerror = rej; + xhr.upload.onprogress = e => { + setPercentage(Math.floor(e.loaded / e.total * 100)); + }; + xhr.send(formData); + }); + + dispatch(showModal({ + type: 'dialog', + icon: 'info', + message: t('saved'), + })); + }; + const onChangeFile: ChangeEventHandler = (e) => { if (e.target.files === null || e.target.files.length === 0) return; + const file = e.target.files[0]; const reader = new FileReader(); + setFileName(file.name); reader.addEventListener('load', () => setBlobUrl(reader.result as string)); - reader.readAsDataURL(e.target.files[0]); + reader.readAsDataURL(file); setCrop({unit: '%', width: 100, aspect: 1 / 1}); }; @@ -52,9 +106,19 @@ export const NekomimiPage: React.VFC = () => { ); }, [completedCrop]); + const onClickUpload = async () => { + setUploading(true); + setPercentage(0); + try { + await beginUpload(); + } finally { + setUploading(false); + } + }; + return (
-

ネコミミアジャスター

+

{t('catAdjuster')}

{blobUrl && (
@@ -66,7 +130,7 @@ export const NekomimiPage: React.VFC = () => { />
-

プレビュー

+

{t('preview')}

{ }} />
- +
)} diff --git a/src/frontend/langs/ja-JP.json b/src/frontend/langs/ja-JP.json index 81a5d90..a55bf5f 100644 --- a/src/frontend/langs/ja-JP.json +++ b/src/frontend/langs/ja-JP.json @@ -42,6 +42,9 @@ "translatedByTheCommunity": "Misskey Toolsはボランティアによって翻訳されています。", "helpTranslation": "翻訳に協力する", "announcements": "お知らせ", + "upload": "アップロード", + "preview": "プレビュー", + "catAdjuster": "ねこみみアジャスター", "_welcomeMessage": { "pattern1": "ついついノートしすぎていませんか?", "pattern2": "Misskey, しすぎていませんか?", @@ -159,8 +162,5 @@ "no": "キャンセル", "success": "アカウントを解除しました。トップ画面に戻ります。", "failure": "アカウントを解除できませんでした。" - }, - "_catAdjuster": { - "title": "ねこみみアジャスター" } } diff --git a/src/frontend/pages/index.session.tsx b/src/frontend/pages/index.session.tsx index 3b3dc6f..b9890be 100644 --- a/src/frontend/pages/index.session.tsx +++ b/src/frontend/pages/index.session.tsx @@ -30,7 +30,7 @@ export const IndexSessionPage: React.VFC = () => { const it: TabItem[] = []; it.push({ label: t('_nav.misshai'), key: 'misshai' }); it.push({ label: t('_nav.accounts'), key: 'accounts' }); - it.push({ label: 'ネコミミ', key: 'nekomimi' }); + it.push({ label: t('_nav.catAdjuster'), key: 'nekomimi' }); if (data?.isAdmin) { it.push({ label: 'Admin', key: 'admin' }); } diff --git a/src/frontend/pages/index.welcome.tsx b/src/frontend/pages/index.welcome.tsx index 6f8c697..e3a4a94 100644 --- a/src/frontend/pages/index.welcome.tsx +++ b/src/frontend/pages/index.welcome.tsx @@ -39,7 +39,7 @@ export const IndexWelcomePage: React.VFC = () => { {t('_missHai.showRanking')}
-

{t('_catAdjuster.title')}

+

{t('catAdjuster')}

{t('_welcome.catAdjusterDescription')}