import React, { FC, ReactNode, useEffect, useState } from 'react';
import style from './ConfigDialog.module.scss';
import classNames from 'classnames';
import { Icon, Sound, Theme } from '../themes/interface';
import { QRCodeCanvas } from 'qrcode.react';
import Bmob from 'hydrogen-js-sdk';
import {
captureElement,
CUSTOM_THEME_FILE_VALIDATE_STORAGE_KEY,
LAST_CUSTOM_THEME_ID_STORAGE_KEY,
CUSTOM_THEME_STORAGE_KEY,
deleteThemeUnusedSounds,
getFileBase64String,
linkReg,
randomString,
wrapThemeDefaultSounds,
LAST_UPLOAD_TIME_STORAGE_KEY,
} from '../utils';
import { copy } from 'clipboard';
import { CloseIcon } from './CloseIcon';
import WxQrCode from './WxQrCode';
const InputContainer: FC<{
label: string;
required?: boolean;
children?: ReactNode;
}> = ({ label, children, required }) => {
return (
<>
>
);
};
interface CustomIcon extends Icon {
content: string;
}
interface CustomTheme extends Theme {
icons: CustomIcon[];
}
const ConfigDialog: FC<{
closeMethod: () => void;
previewMethod: (theme: Theme) => void;
}> = ({ closeMethod, previewMethod }) => {
// 错误提示
const [configError, setConfigError] = useState('');
// 生成链接
const [genLink, setGenLink] = useState('');
// 主题大对象
const [customTheme, setCustomTheme] = useState({
title: '',
sounds: [],
pure: false,
icons: new Array(10).fill(0).map(() => ({
name: randomString(4),
content: '',
clickSound: '',
tripleSound: '',
})),
});
function updateCustomTheme(key: keyof CustomTheme, value: any) {
if (['sounds', 'icons'].includes(key)) {
if (Array.isArray(value)) {
setCustomTheme({
...customTheme,
[key]: [...value],
});
} else {
setCustomTheme({
...customTheme,
[key]: [...customTheme[key as 'sounds' | 'icons'], value],
});
}
} else {
setCustomTheme({
...customTheme,
[key]: value,
});
}
}
useEffect(() => {
console.log(customTheme);
}, [customTheme]);
// 音效
const [newSound, setNewSound] = useState({ name: '', src: '' });
const [soundError, setSoundError] = useState('');
const onNewSoundChange = (key: keyof Sound, value: string) => {
setNewSound({
...newSound,
[key]: value,
});
};
const onAddNewSoundClick = () => {
setSoundError('');
let error = '';
if (!linkReg.test(newSound.src)) error = '请输入https链接';
if (!newSound.name) error = '请输入音效名称';
if (customTheme.sounds.find((s) => s.name === newSound.name))
error = '名称已存在';
if (error) {
setSoundError(error);
} else {
updateCustomTheme('sounds', newSound);
setNewSound({ name: '', src: '' });
}
};
const onDeleteSoundClick = (idx: number) => {
const deleteSoundName = customTheme.sounds[idx].name;
const findIconUseIdx = customTheme.icons.findIndex(
({ clickSound, tripleSound }) =>
[clickSound, tripleSound].includes(deleteSoundName)
);
if (findIconUseIdx !== -1) {
return setSoundError(
`第${findIconUseIdx + 1}项图标有使用该音效,请取消后再删除`
);
}
const newSounds = customTheme.sounds.slice();
newSounds.splice(idx, 1);
updateCustomTheme('sounds', newSounds);
};
// 本地文件选择
const [bgmError, setBgmError] = useState('');
const [backgroundError, setBackgroundError] = useState('');
const [iconErrors, setIconErrors] = useState(
new Array(10).fill('')
);
// 文件体积校验开关
const [enableFileSizeValidate, setEnableFileSizeValidate] =
useState(
localStorage.getItem(CUSTOM_THEME_FILE_VALIDATE_STORAGE_KEY) !==
'false'
);
useEffect(() => {
localStorage.setItem(
CUSTOM_THEME_FILE_VALIDATE_STORAGE_KEY,
enableFileSizeValidate + ''
);
}, [enableFileSizeValidate]);
const makeIconErrors = (idx: number, error: string) =>
new Array(10)
.fill('')
.map((item, _idx) => (idx === _idx ? error : iconErrors[_idx]));
const onFileChange: (props: {
type: 'bgm' | 'background' | 'sound' | 'icon';
file?: File;
idx?: number;
}) => void = ({ type, file, idx }) => {
if (!file) return;
switch (type) {
case 'bgm':
setBgmError('');
if (enableFileSizeValidate && file.size > 80 * 1024) {
return setBgmError('请选择80k以内全损音质的文件');
}
getFileBase64String(file)
.then((res) => {
updateCustomTheme('bgm', res);
})
.catch((e) => {
setBgmError(e);
});
break;
case 'background':
setBackgroundError('');
if (enableFileSizeValidate && file.size > 80 * 1024) {
return setBackgroundError('请选择80k以内全损画质的图片');
}
getFileBase64String(file)
.then((res) => {
updateCustomTheme('background', res);
})
.catch((e) => {
setBackgroundError(e);
});
break;
case 'sound':
setSoundError('');
if (enableFileSizeValidate && file.size > 10 * 1024) {
return setSoundError('请选择10k以内的音频文件');
}
getFileBase64String(file)
.then((res) => {
onNewSoundChange('src', res);
})
.catch((e) => {
setSoundError(e);
});
break;
case 'icon':
if (idx == null) return;
setIconErrors(makeIconErrors(idx, ''));
if (enableFileSizeValidate && file.size > 5 * 1024) {
return setIconErrors(
makeIconErrors(idx, '请选择5k以内的图片文件')
);
}
getFileBase64String(file)
.then((res) => {
updateCustomTheme(
'icons',
customTheme.icons.map((icon, _idx) =>
_idx === idx ? { ...icon, content: res } : icon
)
);
})
.catch((e) => {
setIconErrors(makeIconErrors(idx, e));
});
break;
}
};
// 图标更新
const updateIcons = (key: keyof CustomIcon, value: string, idx: number) => {
const newIcons = customTheme.icons.map((icon, _idx) =>
_idx === idx
? {
...icon,
[key]: value,
}
: icon
);
updateCustomTheme('icons', newIcons);
};
// 初始化
useEffect(() => {
const lastId = localStorage.getItem(LAST_CUSTOM_THEME_ID_STORAGE_KEY);
lastId && setGenLink(`${location.origin}?customTheme=${lastId}`);
try {
const configString = localStorage.getItem(CUSTOM_THEME_STORAGE_KEY);
if (configString) {
const parseRes = JSON.parse(configString);
if (typeof parseRes === 'object') {
setCustomTheme(parseRes);
}
}
} catch (e) {
console.log(e);
}
}, []);
// 校验主题
const validateTheme: () => Promise = async () => {
// 校验
if (!customTheme.title) return Promise.reject('请输入标题');
if (customTheme.bgm && !linkReg.test(customTheme.bgm))
return Promise.reject('bgm请输入https链接');
if (customTheme.background && !linkReg.test(customTheme.background))
return Promise.reject('背景图请输入https链接');
if (!customTheme.maxLevel || customTheme.maxLevel < 5)
return Promise.reject('请输入大于5的关卡数');
const findIconError = iconErrors.find((i) => !!i);
if (findIconError)
return Promise.reject(`图标素材有错误:${findIconError}`);
const findUnfinishedIconIdx = customTheme.icons.findIndex(
(icon) => !icon.content
);
if (findUnfinishedIconIdx !== -1) {
setIconErrors(makeIconErrors(findUnfinishedIconIdx, '请填写链接'));
return Promise.reject(
`第${findUnfinishedIconIdx + 1}图标素材未完成`
);
}
return Promise.resolve('');
};
// 预览
const onPreviewClick = () => {
setConfigError('');
validateTheme()
.then(() => {
const cloneTheme = JSON.parse(JSON.stringify(customTheme));
wrapThemeDefaultSounds(cloneTheme);
previewMethod(cloneTheme);
localStorage.setItem(
CUSTOM_THEME_STORAGE_KEY,
JSON.stringify(customTheme)
);
closeMethod();
})
.catch((e) => {
setConfigError(e);
});
};
// 生成二维码和链接
const [uploading, setUploading] = useState(false);
const onGenQrLinkClick = () => {
if (uploading) return;
if (!enableFileSizeValidate)
return setConfigError('请先开启文件大小校验');
let passTime = Number.MAX_SAFE_INTEGER;
const lastUploadTime = localStorage.getItem(
LAST_UPLOAD_TIME_STORAGE_KEY
);
if (lastUploadTime) {
passTime = Date.now() - Number(lastUploadTime);
}
if (passTime < 1000 * 60 * 15) {
return setConfigError(
`为节省请求数,15分钟内只能生成一次二维码,还剩大约${
15 - Math.round(passTime / 1000 / 60)
}分钟,先本地预览调整下吧~`
);
}
setUploading(true);
setConfigError('');
validateTheme()
.then(() => {
const cloneTheme = JSON.parse(JSON.stringify(customTheme));
deleteThemeUnusedSounds(cloneTheme);
const stringify = JSON.stringify(cloneTheme);
localStorage.setItem(CUSTOM_THEME_STORAGE_KEY, stringify);
const query = Bmob.Query('config');
query.set('content', stringify);
query
.save()
.then((res) => {
localStorage.setItem(
LAST_CUSTOM_THEME_ID_STORAGE_KEY,
//@ts-ignore
res.objectId
);
localStorage.setItem(
LAST_UPLOAD_TIME_STORAGE_KEY,
Date.now().toString()
);
setTimeout(() => {
setGenLink(
`${location.origin}?customTheme=${
/*@ts-ignore*/
res.objectId || id
}`
);
}, 3000);
})
.catch(({ error, code }) => {
setTimeout(() => {
setConfigError(
code === 10007
? '上传数据长度已超过bmob的限制'
: error
);
}, 3000);
})
.finally(() => {
setTimeout(() => {
setUploading(false);
}, 3000);
});
})
.catch((e) => {
setConfigError(e);
setUploading(false);
});
};
// 彩蛋
const [pureClickTime, setPureClickTime] = useState(0);
useEffect(() => {
updateCustomTheme(
'pure',
pureClickTime % 5 === 0 && pureClickTime !== 0
);
}, [pureClickTime]);
return (
自定义主题
updateCustomTheme('title', e.target.value)}
/>
updateCustomTheme('desc', e.target.value)}
/>
接口上传体积有限制,上传文件请全力压缩到80k以下
onFileChange({
type: 'bgm',
file: e.target.files?.[0],
})
}
/>
{bgmError && {bgmError}
}
updateCustomTheme('bgm', e.target.value)}
/>
{customTheme.bgm && }
接口上传体积有限制,上传文件请全力压缩到80k以下
onFileChange({
type: 'background',
file: e.target.files?.[0],
})
}
/>
{backgroundError && (
{backgroundError}
)}
使用图片或者纯色作为背景,图片可开启毛玻璃效果。如果你使用了深色的图片和颜色,请开启深色模式,此时标题等文字将变为亮色
updateCustomTheme('maxLevel', Number(e.target.value))
}
/>
{customTheme.sounds.map((sound, idx) => {
return (
{sound.name}
onDeleteSoundClick(idx)}
/>
);
})}
onNewSoundChange('name', e.target.value)}
/>
接口上传体积有限制,上传文件请全力压缩到10k以下
onFileChange({
type: 'sound',
file: e.target.files?.[0],
})
}
/>
onNewSoundChange('src', e.target.value)}
/>
{soundError && (
{soundError}
)}
接口上传体积有限制,上传文件请全力压缩到5k以下,推荐尺寸56*56
{customTheme.icons.map((icon, idx) => (
onFileChange({
type: 'icon',
file: e.target.files?.[0],
idx,
})
}
/>
{
setIconErrors(
makeIconErrors(
idx,
linkReg.test(e.target.value)
? ''
: '请输入https外链'
)
);
}}
onChange={(e) =>
updateIcons('content', e.target.value, idx)
}
/>
{iconErrors[idx] && (
{iconErrors[idx]}
)}
))}
{/*
??*/}
{genLink && (
{genLink}
)}
接口上传内容总体积有限制,上传文件失败请进一步压缩文件,或者使用外链(自行搜索【免费图床】【免费mp3外链】【对象存储服务】等关键词)。
本地整活,勾选右侧关闭文件大小校验👉
setEnableFileSizeValidate(!e.target.checked)
}
/>
(谨慎操作,单文件不超过1M为宜,文件过大可能导致崩溃,介时请刷新浏览器)
{configError &&
{configError}
}
{customTheme.pure && (
🎉🎉🎉恭喜发现彩蛋,生成的主题将开启纯净模式~
)}
setPureClickTime(pureClickTime + 1)} />
);
};
export default ConfigDialog;