ねこみみアジャスター完成

This commit is contained in:
Xeltica 2022-01-27 21:46:27 +09:00
parent bc0eeb5461
commit 9c20f5d8d5
5 changed files with 76 additions and 10 deletions

View File

@ -38,7 +38,7 @@ router.get('/login', async ctx => {
host = meta.uri.replace(/^https?:\/\//, ''); host = meta.uri.replace(/^https?:\/\//, '');
const name = 'みす廃あらーと'; const name = 'みす廃あらーと';
const description = 'ついついノートしすぎていませんか?'; const description = 'ついついノートしすぎていませんか?';
const permission = ['write:notes', 'write:notifications']; const permission = ['write:notes', 'write:notifications', 'write:drive', 'read:account', 'write:account'];
if (meta.features.miauth) { if (meta.features.miauth) {
// MiAuthを使用する // MiAuthを使用する

View File

@ -1,21 +1,75 @@
import React, { ChangeEventHandler, useEffect, useRef, useState } from 'react'; import React, { ChangeEventHandler, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import ReactCrop, { Crop } from 'react-image-crop'; import ReactCrop, { Crop } from 'react-image-crop';
import 'react-image-crop/dist/ReactCrop.css'; 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 = () => { export const NekomimiPage: React.VFC = () => {
const {t} = useTranslation();
const dispatch = useDispatch();
const [blobUrl, setBlobUrl] = useState<string | null>(null); const [blobUrl, setBlobUrl] = useState<string | null>(null);
const [fileName, setFileName] = useState<string | null>(null);
const [percentage, setPercentage] = useState(0);
const [isUploading, setUploading] = useState(false);
const [image, setImage] = useState<HTMLImageElement | null>(null); const [image, setImage] = useState<HTMLImageElement | null>(null);
const [crop, setCrop] = useState<Partial<Crop>>({unit: '%', width: 100, aspect: 1 / 1}); const [crop, setCrop] = useState<Partial<Crop>>({unit: '%', width: 100, aspect: 1 / 1});
const [completedCrop, setCompletedCrop] = useState<Crop>(); const [completedCrop, setCompletedCrop] = useState<Crop>();
const previewCanvasRef = useRef<HTMLCanvasElement>(null); const previewCanvasRef = useRef<HTMLCanvasElement>(null);
const {data} = useGetSessionQuery(undefined);
const beginUpload = async () => {
if (!previewCanvasRef.current) return;
if (!data) return;
const canvas = previewCanvasRef.current;
const blob = await new Promise<Blob | null>(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<void>((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<HTMLInputElement> = (e) => { const onChangeFile: ChangeEventHandler<HTMLInputElement> = (e) => {
if (e.target.files === null || e.target.files.length === 0) return; if (e.target.files === null || e.target.files.length === 0) return;
const file = e.target.files[0];
const reader = new FileReader(); const reader = new FileReader();
setFileName(file.name);
reader.addEventListener('load', () => setBlobUrl(reader.result as string)); reader.addEventListener('load', () => setBlobUrl(reader.result as string));
reader.readAsDataURL(e.target.files[0]); reader.readAsDataURL(file);
setCrop({unit: '%', width: 100, aspect: 1 / 1}); setCrop({unit: '%', width: 100, aspect: 1 / 1});
}; };
@ -52,9 +106,19 @@ export const NekomimiPage: React.VFC = () => {
); );
}, [completedCrop]); }, [completedCrop]);
const onClickUpload = async () => {
setUploading(true);
setPercentage(0);
try {
await beginUpload();
} finally {
setUploading(false);
}
};
return ( return (
<div className="fade"> <div className="fade">
<h2></h2> <h2>{t('catAdjuster')}</h2>
<input type="file" className="input-field" accept="image/*" onChange={onChangeFile} /> <input type="file" className="input-field" accept="image/*" onChange={onChangeFile} />
{blobUrl && ( {blobUrl && (
<div className="row mt-2"> <div className="row mt-2">
@ -66,7 +130,7 @@ export const NekomimiPage: React.VFC = () => {
/> />
</div> </div>
<div className="col-4 col-12-sm"> <div className="col-4 col-12-sm">
<h3 className="text-100 text-bold"></h3> <h3 className="text-100 text-bold">{t('preview')}</h3>
<div className="cat mt-4 mb-2" style={{position: 'relative', width: 96, height: 96}}> <div className="cat mt-4 mb-2" style={{position: 'relative', width: 96, height: 96}}>
<canvas <canvas
ref={previewCanvasRef} ref={previewCanvasRef}
@ -80,7 +144,9 @@ export const NekomimiPage: React.VFC = () => {
}} }}
/> />
</div> </div>
<button className="btn primary"></button> <button className="btn primary" onClick={onClickUpload} disabled={isUploading}>
{isUploading ? `${percentage}%` : t('upload')}
</button>
</div> </div>
</div> </div>
)} )}

View File

@ -42,6 +42,9 @@
"translatedByTheCommunity": "Misskey Toolsはボランティアによって翻訳されています。", "translatedByTheCommunity": "Misskey Toolsはボランティアによって翻訳されています。",
"helpTranslation": "翻訳に協力する", "helpTranslation": "翻訳に協力する",
"announcements": "お知らせ", "announcements": "お知らせ",
"upload": "アップロード",
"preview": "プレビュー",
"catAdjuster": "ねこみみアジャスター",
"_welcomeMessage": { "_welcomeMessage": {
"pattern1": "ついついノートしすぎていませんか?", "pattern1": "ついついノートしすぎていませんか?",
"pattern2": "Misskey, しすぎていませんか?", "pattern2": "Misskey, しすぎていませんか?",
@ -159,8 +162,5 @@
"no": "キャンセル", "no": "キャンセル",
"success": "アカウントを解除しました。トップ画面に戻ります。", "success": "アカウントを解除しました。トップ画面に戻ります。",
"failure": "アカウントを解除できませんでした。" "failure": "アカウントを解除できませんでした。"
},
"_catAdjuster": {
"title": "ねこみみアジャスター"
} }
} }

View File

@ -30,7 +30,7 @@ export const IndexSessionPage: React.VFC = () => {
const it: TabItem[] = []; const it: TabItem[] = [];
it.push({ label: t('_nav.misshai'), key: 'misshai' }); it.push({ label: t('_nav.misshai'), key: 'misshai' });
it.push({ label: t('_nav.accounts'), key: 'accounts' }); it.push({ label: t('_nav.accounts'), key: 'accounts' });
it.push({ label: 'ネコミミ', key: 'nekomimi' }); it.push({ label: t('_nav.catAdjuster'), key: 'nekomimi' });
if (data?.isAdmin) { if (data?.isAdmin) {
it.push({ label: 'Admin', key: 'admin' }); it.push({ label: 'Admin', key: 'admin' });
} }

View File

@ -39,7 +39,7 @@ export const IndexWelcomePage: React.VFC = () => {
<Link to="/ranking">{t('_missHai.showRanking')}</Link> <Link to="/ranking">{t('_missHai.showRanking')}</Link>
</article> </article>
<div className="col-4 col-12-sm"> <div className="col-4 col-12-sm">
<h3><i className="bi bi-crop"/> {t('_catAdjuster.title')}</h3> <h3><i className="bi bi-crop"/> {t('catAdjuster')}</h3>
<p>{t('_welcome.catAdjusterDescription')}</p> <p>{t('_welcome.catAdjusterDescription')}</p>
</div> </div>
</div> </div>