feat: 本地文件配置存储

This commit is contained in:
streakingman 2022-10-11 18:31:59 +08:00
parent 1890cc5a6f
commit 2d7de338fa
8 changed files with 684 additions and 129 deletions

View File

@ -1,6 +1,9 @@
import React, { FC } from 'react';
import React, { FC, MouseEventHandler } from 'react';
export const CloseIcon: FC<{ fill: string }> = ({ fill }) => {
export const CloseIcon: FC<{ fill: string; onClick?: MouseEventHandler }> = ({
fill,
onClick,
}) => {
return (
<svg
width="13"
@ -8,6 +11,7 @@ export const CloseIcon: FC<{ fill: string }> = ({ fill }) => {
viewBox="0 0 13 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
onClick={onClick}
>
<path
fillRule="evenodd"

View File

@ -10,6 +10,65 @@
}
}
@keyframes shake {
0% {
transform: translateX(0);
}
30% {
transform: translateX(30px);
}
80% {
transform: translateX(-30px);
}
100% {
transform: translateX(0);
}
}
.soundItem {
height: 30px;
position: relative;
.inner {
height: 30px;
min-width: 48px;
margin-left: 42px;
background-color: #888;
line-height: 30px;
padding: 0 12px;
border-bottom-right-radius: 15px;
border-top-right-radius: 15px;
font-size: 14px;
color: white;
position: relative;
z-index: 2;
display: flex;
gap: 8px;
align-items: center;
justify-content: space-between;
}
svg {
cursor: pointer;
transition: 0.3s;
&:hover {
transform: rotate(180deg);
}
}
audio {
height: 30px;
width: 100px;
position: absolute;
left: 0;
z-index: 1;
}
}
.dialog {
text-align: left;
overflow-y: auto;
@ -28,13 +87,14 @@
display: flex;
flex-flow: column nowrap !important;
gap: 8px;
overflow-x: hidden;
h2 {
text-align: center;
margin-top: -36px;
}
@media screen and (min-width: 1024px) {
@media screen and (min-width: 768px) {
margin: 36px 0;
border-radius: 16px;
box-shadow: 0 19px 38px rgb(0 0 0 / 30%), 0 15px 12px rgb(0 0 0 / 22%);
@ -47,6 +107,7 @@
.errorTip {
color: crimson;
animation: shake 0.2s 2;
}
.closeBtn {
@ -79,6 +140,10 @@
.inputContainer {
word-break: keep-all;
audio {
height: 30px;
}
input,
select {
flex-grow: 1;
@ -107,10 +172,11 @@
font-weight: 600;
}
.tip {
font-size: 14px;
opacity: 0.6;
word-break: break-all;
.imgPreview {
height: 30px;
border: 1px dashed rgb(0 0 0 / 30%);
min-width: 30px;
object-fit: contain;
}
&.required .label {
@ -121,40 +187,76 @@
}
}
.tip {
font-size: 12px;
opacity: 0.6;
word-break: break-all;
}
.iconInput {
display: flex;
flex-direction: column;
gap: 4px;
flex-grow: 1;
&Group {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 8px;
input {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
input[type='file'] {
padding: 3px 12px;
}
.iconPreview {
height: 109px;
width: 109px;
border: 1px dashed rgb(0 0 0 / 30%);
object-fit: cover;
flex-basis: 109px;
}
}
}
.divider {
width: 100%;
height: 0;
border-bottom: 1px solid rgb(0 0 0 / 8%);
}
.soundItem {
height: 30px;
position: relative;
.inner {
height: 30px;
min-width: 48px;
margin-left: 42px;
background-color: #888;
line-height: 30px;
padding: 0 12px;
border-bottom-right-radius: 15px;
border-top-right-radius: 15px;
font-size: 14px;
color: white;
position: relative;
z-index: 2;
display: flex;
gap: 8px;
align-items: center;
justify-content: space-between;
@keyframes gradient {
0% {
background-position: 100% 50%;
}
audio {
height: 30px;
width: 100px;
position: absolute;
left: 0;
z-index: 1;
50% {
background-position: 0 50%;
}
100% {
background-position: 100% 50%;
}
}
.uploadBtn {
background-image: linear-gradient(
-45deg,
#ee7752,
#e73c7e,
#23a6d5,
#23d5ab
);
background-size: 400% 400%;
background-position: 100% 50%;
&.uploading {
animation: gradient 1s ease infinite;
}
}

View File

@ -6,8 +6,12 @@ import { QRCodeCanvas } from 'qrcode.react';
import Bmob from 'hydrogen-js-sdk';
import {
captureElement,
CUSTOM_THEME_FILE_VALIDATE_STORAGE_KEY,
CUSTOM_THEME_ID_STORAGE_KEY,
CUSTOM_THEME_STORAGE_KEY,
LAST_UPLOAD_TIME_STORAGE_KEY,
deleteThemeUnusedSounds,
getFileBase64String,
linkReg,
randomString,
wrapThemeDefaultSounds,
} from '../utils';
@ -18,7 +22,7 @@ import WxQrCode from './WxQrCode';
const InputContainer: FC<{
label: string;
required?: boolean;
children: ReactNode;
children?: ReactNode;
}> = ({ label, children, required }) => {
return (
<>
@ -39,6 +43,16 @@ const InputContainer: FC<{
);
};
interface CustomIcon extends Icon {
content: string;
}
interface CustomTheme extends Theme<any> {
icons: CustomIcon[];
}
const id = localStorage.getItem(CUSTOM_THEME_ID_STORAGE_KEY);
const ConfigDialog: FC<{
closeMethod: () => void;
previewMethod: (theme: Theme<string>) => void;
@ -46,8 +60,12 @@ const ConfigDialog: FC<{
// 错误提示
const [configError, setConfigError] = useState<string>('');
// 生成链接
const [genLink, setGenLink] = useState<string>('');
const [customTheme, setCustomTheme] = useState<Theme<any>>({
const [genLink, setGenLink] = useState<string>(
id ? `${location.origin}?customTheme=${id}` : ''
);
// 主题大对象
const [customTheme, setCustomTheme] = useState<CustomTheme>({
title: '',
sounds: [],
icons: new Array(10).fill(0).map(() => ({
@ -57,9 +75,174 @@ const ConfigDialog: FC<{
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 [editSound, setEditSound] = useState<Sound>({ name: '', src: '' });
// 音效
const [newSound, setNewSound] = useState<Sound>({ name: '', src: '' });
const [soundError, setSoundError] = useState<string>('');
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<string>('');
const [backgroundError, setBackgroundError] = useState<string>('');
const [iconErrors, setIconErrors] = useState<string[]>(
new Array(10).fill('')
);
// 文件体积校验开关
const [enableFileSizeValidate, setEnableFileSizeValidate] =
useState<boolean>(
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(() => {
@ -76,23 +259,34 @@ const ConfigDialog: FC<{
}
}, []);
// 生成主题
const generateTheme: () => Promise<Theme<any>> = async () => {
// TODO 校验
const cloneTheme = JSON.parse(JSON.stringify(customTheme));
wrapThemeDefaultSounds(cloneTheme);
return Promise.resolve(cloneTheme);
// 校验主题
const validateTheme: () => Promise<string> = 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}`);
return Promise.resolve('');
};
// 预览
const onPreviewClick = () => {
setConfigError('');
generateTheme()
.then((theme) => {
previewMethod(theme);
validateTheme()
.then(() => {
const cloneTheme = JSON.parse(JSON.stringify(customTheme));
wrapThemeDefaultSounds(cloneTheme);
previewMethod(cloneTheme);
localStorage.setItem(
CUSTOM_THEME_STORAGE_KEY,
JSON.stringify(theme)
JSON.stringify(customTheme)
);
closeMethod();
})
@ -101,62 +295,66 @@ const ConfigDialog: FC<{
});
};
const [uploading, setUploading] = useState<boolean>(false);
// 生成二维码和链接
const [uploading, setUploading] = useState<boolean>(false);
const onGenQrLinkClick = () => {
if (uploading) return;
if (!enableFileSizeValidate)
return setConfigError('请先开启文件大小校验');
setUploading(true);
setConfigError('');
generateTheme()
.then((theme) => {
// 五分钟能只能上传一次
const lastUploadTime = localStorage.getItem(
LAST_UPLOAD_TIME_STORAGE_KEY
);
if (
lastUploadTime &&
new Date().getTime() - Number(lastUploadTime) <
1000 * 60 * 5
) {
setConfigError(
'五分钟内只能上传一次(用的人有点多十分抱歉😭),先保存预览看看效果把~'
);
setUploading(false);
return;
}
const stringify = JSON.stringify(theme);
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');
if (id) query.set('id', id);
// Bmob上限 384563
query.set('content', stringify);
query
.save()
.then((res) => {
//@ts-ignore
const link = `${location.origin}?customTheme=${res.objectId}`;
setGenLink(link);
if (!id) {
localStorage.setItem(
LAST_UPLOAD_TIME_STORAGE_KEY,
new Date().getTime().toString()
CUSTOM_THEME_ID_STORAGE_KEY,
//@ts-ignore
res.objectId
);
}
setTimeout(() => {
setGenLink(
`${location.origin}?customTheme=${
/*@ts-ignore*/
res.objectId || id
}`
);
}, 3000);
})
.catch(({ error }) => {
setConfigError(error);
setGenLink('');
.catch(({ error, code }) => {
setTimeout(() => {
setConfigError(
code === 10007
? '上传数据长度已超过bmob的限制'
: error
);
}, 3000);
})
.finally(() => {
setTimeout(() => {
setUploading(false);
}, 3000);
});
})
.catch((e) => {
setTimeout(() => {
setConfigError(e);
setGenLink('');
setUploading(false);
}, 3000);
});
};
// TODO HTML有点臭长了待优化
// @ts-ignore
return (
<div className={classNames(style.dialog)}>
<div className={style.closeBtn} onClick={closeMethod}>
@ -165,39 +363,120 @@ const ConfigDialog: FC<{
<h2></h2>
<InputContainer label={'标题'} required>
<input placeholder={'请输入标题'} />
<input
placeholder={'请输入标题'}
value={customTheme.title}
onChange={(e) => updateCustomTheme('title', e.target.value)}
/>
</InputContainer>
<InputContainer label={'描述'}>
<input placeholder={'请输入描述'} />
<input
placeholder={'请输入描述'}
value={customTheme.desc || ''}
onChange={(e) => updateCustomTheme('desc', e.target.value)}
/>
</InputContainer>
<InputContainer label={'BGM'}>
<input type={'file'} />
<input placeholder={'或者输入https外链'} />
<div className={style.tip}>
80k以下
</div>
<input
type={'file'}
accept={'.mp3'}
onChange={(e) =>
onFileChange({
type: 'bgm',
file: e.target.files?.[0],
})
}
/>
{bgmError && <div className={style.errorTip}>{bgmError}</div>}
<input
placeholder={'或者输入https外链'}
value={customTheme.bgm || ''}
onChange={(e) => updateCustomTheme('bgm', e.target.value)}
/>
{customTheme.bgm && <audio src={customTheme.bgm} controls />}
</InputContainer>
<InputContainer label={'背景图'}>
<input type={'file'} />
<input placeholder={'或者输入https外链'} />
<div className={style.tip}>
80k以下
</div>
<input
type={'file'}
accept={'.jpg,.png,.gif'}
onChange={(e) =>
onFileChange({
type: 'background',
file: e.target.files?.[0],
})
}
/>
{backgroundError && (
<div className={style.errorTip}>{backgroundError}</div>
)}
<div className={'flex-container flex-center'}>
<input
placeholder={'或者输入https外链'}
value={customTheme.background || ''}
onChange={(e) =>
updateCustomTheme('background', e.target.value)
}
/>
{customTheme.background && (
<img
alt="加载失败"
src={customTheme.background}
className={style.imgPreview}
/>
)}
</div>
<div className={'flex-container flex-center flex-no-wrap'}>
<span></span>
<input type={'checkbox'} />
<input
type={'checkbox'}
checked={!!customTheme.backgroundBlur}
onChange={(e) =>
updateCustomTheme(
'backgroundBlur',
e.target.checked
)
}
/>
<div className={'flex-spacer'} />
<span></span>
<input type={'checkbox'} />
<input
type={'checkbox'}
checked={!!customTheme.dark}
onChange={(e) =>
updateCustomTheme('dark', e.target.checked)
}
/>
<div className={'flex-spacer'} />
<span></span>
<input type={'color'} value="#fff" />
<input
type={'color'}
value={customTheme.backgroundColor || '#ffffff'}
onChange={(e) =>
updateCustomTheme('backgroundColor', e.target.value)
}
/>
</div>
<div className={style.tip}>
使使
</div>
</InputContainer>
<InputContainer label={'关卡数'}>
<InputContainer label={'关卡数'} required>
<input
type={'number'}
placeholder={'最低5关最高...理论上无限默认为50'}
placeholder={'最低5关最高...理论上无限'}
value={customTheme.maxLevel || ''}
onChange={(e) =>
updateCustomTheme('maxLevel', Number(e.target.value))
}
/>
</InputContainer>
<InputContainer label={'音效素材'} required>
<InputContainer label={'音效素材'}>
<div className={'flex-container flex-left-center'}>
{customTheme.sounds.map((sound, idx) => {
return (
@ -205,7 +484,10 @@ const ConfigDialog: FC<{
<audio src={sound.src} controls />
<div className={style.inner}>
<span>{sound.name}</span>
<CloseIcon fill={'#fff'} />
<CloseIcon
fill={'#fff'}
onClick={() => onDeleteSoundClick(idx)}
/>
</div>
</div>
);
@ -213,43 +495,177 @@ const ConfigDialog: FC<{
</div>
<input
placeholder={'输入音效名称'}
onChange={(event) =>
setEditSound({
name: event.target.value,
src: editSound.src,
value={newSound.name}
onChange={(e) => onNewSoundChange('name', e.target.value)}
/>
<div className={style.tip}>
10k以下
</div>
<input
type={'file'}
accept={'.mp3'}
onChange={(e) =>
onFileChange({
type: 'sound',
file: e.target.files?.[0],
})
}
/>
<input type={'file'} />
<input
placeholder={'或者输入https外链'}
onChange={(event) =>
setEditSound({
src: event.target.value,
name: editSound.name,
value={newSound.src}
onChange={(e) => onNewSoundChange('src', e.target.value)}
/>
{soundError && (
<div className={style.errorTip}>{soundError}</div>
)}
<button onClick={onAddNewSoundClick}></button>
</InputContainer>
<InputContainer label={'图标素材'} required>
<div className={style.tip}>
5k以下56*56
</div>
</InputContainer>
{customTheme.icons.map((icon, idx) => (
<div key={icon.name} className={style.iconInputGroup}>
<img
alt=""
className={style.iconPreview}
src={icon.content}
/>
<div className={style.iconInput}>
<input
type={'file'}
accept={'.jpg,.png,.gif'}
onChange={(e) =>
onFileChange({
type: 'icon',
file: e.target.files?.[0],
idx,
})
}
/>
<button
onClick={() =>
setCustomTheme({
...customTheme,
sounds: [...customTheme.sounds, editSound],
})
<div
className={
'flex-container flex-center flex-no-wrap'
}
style={{ wordBreak: 'keep-all' }}
>
<input
placeholder={'或者输入https外链'}
value={customTheme.icons[idx].content}
onBlur={(e) => {
if (!linkReg.test(e.target.value))
setIconErrors(
makeIconErrors(
idx,
'请输入https外链'
)
);
}}
onChange={(e) =>
updateIcons('content', e.target.value, idx)
}
/>
{iconErrors[idx] && (
<div className={style.errorTip}>
{iconErrors[idx]}
</div>
)}
</div>
<div className={'flex-container'}>
<select
className={'flex-grow'}
value={customTheme.icons[idx].clickSound}
onChange={(e) =>
updateIcons(
'clickSound',
e.target.value,
idx
)
}
>
</button>
</InputContainer>
<InputContainer label={'图标素材'} required>
<div className={'flex-container flex-left-center'}>
{customTheme.icons.map((icon, idx) => {
return <div key={icon.name}>{icon.name}</div>;
})}
<option value=""></option>
{customTheme.sounds.map((sound) => (
<option key={sound.name} value={sound.name}>
{sound.name}
</option>
))}
</select>
<select
className={'flex-grow'}
value={customTheme.icons[idx].tripleSound}
onChange={(e) =>
updateIcons(
'tripleSound',
e.target.value,
idx
)
}
>
<option value=""></option>
{customTheme.sounds.map((sound) => (
<option key={sound.name} value={sound.name}>
{sound.name}
</option>
))}
</select>
</div>
</InputContainer>
<InputContainer label={'操作音效'}></InputContainer>
</div>
</div>
))}
{/*<InputContainer label={'操作音效'}></InputContainer>*/}
{genLink && (
<div className={'flex-container flex-center flex-column'}>
<QRCodeCanvas id="qrCode" value={genLink} size={300} />
<button
onClick={() =>
captureElement('qrCode', customTheme.title)
}
className="primary"
>
</button>
<div style={{ fontSize: 12 }}>{genLink}</div>
<button onClick={() => copy(genLink)} className="primary">
</button>
</div>
)}
<div className={style.tip}>
使mp3外链
👉
<input
type={'checkbox'}
checked={!enableFileSizeValidate}
onChange={(e) =>
setEnableFileSizeValidate(!e.target.checked)
}
/>
(1M为宜)
</div>
{configError && <div className={style.errorTip}>{configError}</div>}
<WxQrCode />
<div className={'flex-container'}>
<button
className={'primary flex-grow'}
onClick={onPreviewClick}
>
</button>
<button
className={classNames(
'primary flex-grow',
style.uploadBtn,
uploading && style.uploading
)}
onClick={onGenQrLinkClick}
>
{genLink ? '更新数据' : '生成二维码&链接'}
{uploading ? '...' : ''}
</button>
</div>
</div>
);
};

View File

@ -10,6 +10,7 @@ import {
LAST_LEVEL_STORAGE_KEY,
LAST_SCORE_STORAGE_KEY,
LAST_TIME_STORAGE_KEY,
linkReg,
randomString,
waitTimeout,
} from '../utils';
@ -132,8 +133,7 @@ const Symbol: FC<SymbolProps> = ({ x, y, icon, isCover, status, onClick }) => {
style={{ opacity: isCover ? 0.4 : 1 }}
>
{typeof icon.content === 'string' ? (
icon.content.startsWith('http') ||
icon.content.startsWith('/') ? (
linkReg.test(icon.content) ? (
/*图片地址*/
<img src={icon.content} alt="" />
) : (

View File

@ -74,7 +74,7 @@
right: 8px;
bottom: 8px;
transition: 0.5s;
height: 250px;
height: 280px;
backdrop-filter: blur(10px);
border-radius: 8px;
background-color: rgb(0 0 0 / 30%);

View File

@ -11,6 +11,7 @@
&Title {
opacity: 0.8;
width: 100%;
font-size: 14px;
}
&Item {

View File

@ -24,6 +24,7 @@ button {
padding: 8px 16px;
border-radius: 8px;
background-color: #3338;
cursor: pointer;
&.primary {
background-color: #747bff;

View File

@ -4,12 +4,15 @@ import { getDefaultTheme } from './themes/default';
export const LAST_LEVEL_STORAGE_KEY = 'lastLevel';
export const LAST_SCORE_STORAGE_KEY = 'lastScore';
export const LAST_TIME_STORAGE_KEY = 'lastTime';
export const LAST_UPLOAD_TIME_STORAGE_KEY = 'lastUploadTime';
export const CUSTOM_THEME_ID_STORAGE_KEY = 'customThemeId';
export const CUSTOM_THEME_STORAGE_KEY = 'customTheme';
export const CUSTOM_THEME_FILE_VALIDATE_STORAGE_KEY = 'customThemeFileValidate';
export const DEFAULT_BGM_STORAGE_KEY = 'defaultBgm';
export const DEFAULT_TRIPLE_SOUND_STORAGE_KEY = 'defaultTripleSound';
export const DEFAULT_CLICK_SOUND_STORAGE_KEY = 'defaultClickSound';
export const linkReg = /^(https|data):+/;
export const randomString: (len: number) => string = (len) => {
const pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let res = '';
@ -106,7 +109,35 @@ export const wrapThemeDefaultSounds: (theme: Theme<any>) => void = (theme) => {
}
};
export const deleteThemeUnusedSounds = (theme: Theme<any>) => {
const usedSounds = new Set();
for (const icon of theme.icons) {
usedSounds.add(icon.clickSound);
usedSounds.add(icon.tripleSound);
}
theme.sounds = theme.sounds.filter((s) => usedSounds.has(s.name));
};
export const domRelatedOptForTheme = (theme: Theme<any>) => {
document.body.style.backgroundColor = theme.backgroundColor || 'white';
document.body.style.color = theme.dark ? 'white' : 'rgb(0 0 0 / 60%)';
};
export const getFileBase64String: (file: File) => Promise<string> = (
file: File
) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onloadend = (e) => {
if (e.target?.result) {
resolve(e.target.result.toString());
} else {
reject('读取文件内容为空');
}
};
reader.onerror = () => {
reject('读取文件失败');
};
});
};