mirror of
https://ghproxy.com/https://github.com/StreakingMan/solvable-sheep-game
synced 2025-05-23 21:56:07 +08:00
feat: 本地文件配置存储
This commit is contained in:
parent
1890cc5a6f
commit
2d7de338fa
|
@ -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 (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width="13"
|
width="13"
|
||||||
|
@ -8,6 +11,7 @@ export const CloseIcon: FC<{ fill: string }> = ({ fill }) => {
|
||||||
viewBox="0 0 13 14"
|
viewBox="0 0 13 14"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
|
|
|
@ -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 {
|
.dialog {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
@ -28,13 +87,14 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column nowrap !important;
|
flex-flow: column nowrap !important;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: -36px;
|
margin-top: -36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1024px) {
|
@media screen and (min-width: 768px) {
|
||||||
margin: 36px 0;
|
margin: 36px 0;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 19px 38px rgb(0 0 0 / 30%), 0 15px 12px rgb(0 0 0 / 22%);
|
box-shadow: 0 19px 38px rgb(0 0 0 / 30%), 0 15px 12px rgb(0 0 0 / 22%);
|
||||||
|
@ -47,6 +107,7 @@
|
||||||
|
|
||||||
.errorTip {
|
.errorTip {
|
||||||
color: crimson;
|
color: crimson;
|
||||||
|
animation: shake 0.2s 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.closeBtn {
|
.closeBtn {
|
||||||
|
@ -79,6 +140,10 @@
|
||||||
.inputContainer {
|
.inputContainer {
|
||||||
word-break: keep-all;
|
word-break: keep-all;
|
||||||
|
|
||||||
|
audio {
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
select {
|
select {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
@ -107,10 +172,11 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tip {
|
.imgPreview {
|
||||||
font-size: 14px;
|
height: 30px;
|
||||||
opacity: 0.6;
|
border: 1px dashed rgb(0 0 0 / 30%);
|
||||||
word-break: break-all;
|
min-width: 30px;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.required .label {
|
&.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 {
|
.divider {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 0;
|
height: 0;
|
||||||
border-bottom: 1px solid rgb(0 0 0 / 8%);
|
border-bottom: 1px solid rgb(0 0 0 / 8%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.soundItem {
|
@keyframes gradient {
|
||||||
height: 30px;
|
0% {
|
||||||
position: relative;
|
background-position: 100% 50%;
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
audio {
|
50% {
|
||||||
height: 30px;
|
background-position: 0 50%;
|
||||||
width: 100px;
|
}
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
100% {
|
||||||
z-index: 1;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,12 @@ import { QRCodeCanvas } from 'qrcode.react';
|
||||||
import Bmob from 'hydrogen-js-sdk';
|
import Bmob from 'hydrogen-js-sdk';
|
||||||
import {
|
import {
|
||||||
captureElement,
|
captureElement,
|
||||||
|
CUSTOM_THEME_FILE_VALIDATE_STORAGE_KEY,
|
||||||
|
CUSTOM_THEME_ID_STORAGE_KEY,
|
||||||
CUSTOM_THEME_STORAGE_KEY,
|
CUSTOM_THEME_STORAGE_KEY,
|
||||||
LAST_UPLOAD_TIME_STORAGE_KEY,
|
deleteThemeUnusedSounds,
|
||||||
|
getFileBase64String,
|
||||||
|
linkReg,
|
||||||
randomString,
|
randomString,
|
||||||
wrapThemeDefaultSounds,
|
wrapThemeDefaultSounds,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
|
@ -18,7 +22,7 @@ import WxQrCode from './WxQrCode';
|
||||||
const InputContainer: FC<{
|
const InputContainer: FC<{
|
||||||
label: string;
|
label: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
children: ReactNode;
|
children?: ReactNode;
|
||||||
}> = ({ label, children, required }) => {
|
}> = ({ label, children, required }) => {
|
||||||
return (
|
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<{
|
const ConfigDialog: FC<{
|
||||||
closeMethod: () => void;
|
closeMethod: () => void;
|
||||||
previewMethod: (theme: Theme<string>) => void;
|
previewMethod: (theme: Theme<string>) => void;
|
||||||
|
@ -46,8 +60,12 @@ const ConfigDialog: FC<{
|
||||||
// 错误提示
|
// 错误提示
|
||||||
const [configError, setConfigError] = useState<string>('');
|
const [configError, setConfigError] = useState<string>('');
|
||||||
// 生成链接
|
// 生成链接
|
||||||
const [genLink, setGenLink] = useState<string>('');
|
const [genLink, setGenLink] = useState<string>(
|
||||||
const [customTheme, setCustomTheme] = useState<Theme<any>>({
|
id ? `${location.origin}?customTheme=${id}` : ''
|
||||||
|
);
|
||||||
|
|
||||||
|
// 主题大对象
|
||||||
|
const [customTheme, setCustomTheme] = useState<CustomTheme>({
|
||||||
title: '',
|
title: '',
|
||||||
sounds: [],
|
sounds: [],
|
||||||
icons: new Array(10).fill(0).map(() => ({
|
icons: new Array(10).fill(0).map(() => ({
|
||||||
|
@ -57,9 +75,174 @@ const ConfigDialog: FC<{
|
||||||
tripleSound: '',
|
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(() => {
|
useEffect(() => {
|
||||||
|
@ -76,23 +259,34 @@ const ConfigDialog: FC<{
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 生成主题
|
// 校验主题
|
||||||
const generateTheme: () => Promise<Theme<any>> = async () => {
|
const validateTheme: () => Promise<string> = async () => {
|
||||||
// TODO 校验
|
// 校验
|
||||||
const cloneTheme = JSON.parse(JSON.stringify(customTheme));
|
if (!customTheme.title) return Promise.reject('请输入标题');
|
||||||
wrapThemeDefaultSounds(cloneTheme);
|
if (customTheme.bgm && !linkReg.test(customTheme.bgm))
|
||||||
return Promise.resolve(cloneTheme);
|
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 = () => {
|
const onPreviewClick = () => {
|
||||||
setConfigError('');
|
setConfigError('');
|
||||||
generateTheme()
|
validateTheme()
|
||||||
.then((theme) => {
|
.then(() => {
|
||||||
previewMethod(theme);
|
const cloneTheme = JSON.parse(JSON.stringify(customTheme));
|
||||||
|
wrapThemeDefaultSounds(cloneTheme);
|
||||||
|
previewMethod(cloneTheme);
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
CUSTOM_THEME_STORAGE_KEY,
|
CUSTOM_THEME_STORAGE_KEY,
|
||||||
JSON.stringify(theme)
|
JSON.stringify(customTheme)
|
||||||
);
|
);
|
||||||
closeMethod();
|
closeMethod();
|
||||||
})
|
})
|
||||||
|
@ -101,62 +295,66 @@ const ConfigDialog: FC<{
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const [uploading, setUploading] = useState<boolean>(false);
|
|
||||||
// 生成二维码和链接
|
// 生成二维码和链接
|
||||||
|
const [uploading, setUploading] = useState<boolean>(false);
|
||||||
const onGenQrLinkClick = () => {
|
const onGenQrLinkClick = () => {
|
||||||
if (uploading) return;
|
if (uploading) return;
|
||||||
|
if (!enableFileSizeValidate)
|
||||||
|
return setConfigError('请先开启文件大小校验');
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
setConfigError('');
|
setConfigError('');
|
||||||
generateTheme()
|
validateTheme()
|
||||||
.then((theme) => {
|
.then(() => {
|
||||||
// 五分钟能只能上传一次
|
const cloneTheme = JSON.parse(JSON.stringify(customTheme));
|
||||||
const lastUploadTime = localStorage.getItem(
|
deleteThemeUnusedSounds(cloneTheme);
|
||||||
LAST_UPLOAD_TIME_STORAGE_KEY
|
const stringify = JSON.stringify(cloneTheme);
|
||||||
);
|
|
||||||
if (
|
|
||||||
lastUploadTime &&
|
|
||||||
new Date().getTime() - Number(lastUploadTime) <
|
|
||||||
1000 * 60 * 5
|
|
||||||
) {
|
|
||||||
setConfigError(
|
|
||||||
'五分钟内只能上传一次(用的人有点多十分抱歉😭),先保存预览看看效果把~'
|
|
||||||
);
|
|
||||||
setUploading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stringify = JSON.stringify(theme);
|
|
||||||
localStorage.setItem(CUSTOM_THEME_STORAGE_KEY, stringify);
|
localStorage.setItem(CUSTOM_THEME_STORAGE_KEY, stringify);
|
||||||
const query = Bmob.Query('config');
|
const query = Bmob.Query('config');
|
||||||
|
if (id) query.set('id', id);
|
||||||
|
// Bmob上限 384563
|
||||||
query.set('content', stringify);
|
query.set('content', stringify);
|
||||||
query
|
query
|
||||||
.save()
|
.save()
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
//@ts-ignore
|
if (!id) {
|
||||||
const link = `${location.origin}?customTheme=${res.objectId}`;
|
localStorage.setItem(
|
||||||
setGenLink(link);
|
CUSTOM_THEME_ID_STORAGE_KEY,
|
||||||
localStorage.setItem(
|
//@ts-ignore
|
||||||
LAST_UPLOAD_TIME_STORAGE_KEY,
|
res.objectId
|
||||||
new Date().getTime().toString()
|
);
|
||||||
);
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
setGenLink(
|
||||||
|
`${location.origin}?customTheme=${
|
||||||
|
/*@ts-ignore*/
|
||||||
|
res.objectId || id
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}, 3000);
|
||||||
})
|
})
|
||||||
.catch(({ error }) => {
|
.catch(({ error, code }) => {
|
||||||
setConfigError(error);
|
setTimeout(() => {
|
||||||
setGenLink('');
|
setConfigError(
|
||||||
|
code === 10007
|
||||||
|
? '上传数据长度已超过bmob的限制'
|
||||||
|
: error
|
||||||
|
);
|
||||||
|
}, 3000);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setUploading(false);
|
setTimeout(() => {
|
||||||
|
setUploading(false);
|
||||||
|
}, 3000);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
setConfigError(e);
|
setTimeout(() => {
|
||||||
setGenLink('');
|
setConfigError(e);
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
|
}, 3000);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO HTML有点臭长了,待优化
|
|
||||||
// @ts-ignore
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(style.dialog)}>
|
<div className={classNames(style.dialog)}>
|
||||||
<div className={style.closeBtn} onClick={closeMethod}>
|
<div className={style.closeBtn} onClick={closeMethod}>
|
||||||
|
@ -165,39 +363,120 @@ const ConfigDialog: FC<{
|
||||||
<h2>自定义主题</h2>
|
<h2>自定义主题</h2>
|
||||||
|
|
||||||
<InputContainer label={'标题'} required>
|
<InputContainer label={'标题'} required>
|
||||||
<input placeholder={'请输入标题'} />
|
<input
|
||||||
|
placeholder={'请输入标题'}
|
||||||
|
value={customTheme.title}
|
||||||
|
onChange={(e) => updateCustomTheme('title', e.target.value)}
|
||||||
|
/>
|
||||||
</InputContainer>
|
</InputContainer>
|
||||||
<InputContainer label={'描述'}>
|
<InputContainer label={'描述'}>
|
||||||
<input placeholder={'请输入描述'} />
|
<input
|
||||||
|
placeholder={'请输入描述'}
|
||||||
|
value={customTheme.desc || ''}
|
||||||
|
onChange={(e) => updateCustomTheme('desc', e.target.value)}
|
||||||
|
/>
|
||||||
</InputContainer>
|
</InputContainer>
|
||||||
<InputContainer label={'BGM'}>
|
<InputContainer label={'BGM'}>
|
||||||
<input type={'file'} />
|
<div className={style.tip}>
|
||||||
<input placeholder={'或者输入https外链'} />
|
接口上传体积有限制,上传文件请全力压缩到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>
|
||||||
<InputContainer label={'背景图'}>
|
<InputContainer label={'背景图'}>
|
||||||
<input type={'file'} />
|
<div className={style.tip}>
|
||||||
<input placeholder={'或者输入https外链'} />
|
接口上传体积有限制,上传文件请全力压缩到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'}>
|
<div className={'flex-container flex-center flex-no-wrap'}>
|
||||||
<span>毛玻璃</span>
|
<span>毛玻璃</span>
|
||||||
<input type={'checkbox'} />
|
<input
|
||||||
|
type={'checkbox'}
|
||||||
|
checked={!!customTheme.backgroundBlur}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateCustomTheme(
|
||||||
|
'backgroundBlur',
|
||||||
|
e.target.checked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
<div className={'flex-spacer'} />
|
<div className={'flex-spacer'} />
|
||||||
<span>深色</span>
|
<span>深色</span>
|
||||||
<input type={'checkbox'} />
|
<input
|
||||||
|
type={'checkbox'}
|
||||||
|
checked={!!customTheme.dark}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateCustomTheme('dark', e.target.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
<div className={'flex-spacer'} />
|
<div className={'flex-spacer'} />
|
||||||
<span>纯色</span>
|
<span>纯色</span>
|
||||||
<input type={'color'} value="#fff" />
|
<input
|
||||||
|
type={'color'}
|
||||||
|
value={customTheme.backgroundColor || '#ffffff'}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateCustomTheme('backgroundColor', e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.tip}>
|
<div className={style.tip}>
|
||||||
使用图片或者纯色作为背景,图片可开启毛玻璃效果。如果你使用了深色的图片和颜色,请开启深色模式,此时标题等文字将变为亮色
|
使用图片或者纯色作为背景,图片可开启毛玻璃效果。如果你使用了深色的图片和颜色,请开启深色模式,此时标题等文字将变为亮色
|
||||||
</div>
|
</div>
|
||||||
</InputContainer>
|
</InputContainer>
|
||||||
<InputContainer label={'关卡数'}>
|
<InputContainer label={'关卡数'} required>
|
||||||
<input
|
<input
|
||||||
type={'number'}
|
type={'number'}
|
||||||
placeholder={'最低5关,最高...理论上无限,默认为50'}
|
placeholder={'最低5关,最高...理论上无限'}
|
||||||
|
value={customTheme.maxLevel || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateCustomTheme('maxLevel', Number(e.target.value))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</InputContainer>
|
</InputContainer>
|
||||||
<InputContainer label={'音效素材'} required>
|
<InputContainer label={'音效素材'}>
|
||||||
<div className={'flex-container flex-left-center'}>
|
<div className={'flex-container flex-left-center'}>
|
||||||
{customTheme.sounds.map((sound, idx) => {
|
{customTheme.sounds.map((sound, idx) => {
|
||||||
return (
|
return (
|
||||||
|
@ -205,7 +484,10 @@ const ConfigDialog: FC<{
|
||||||
<audio src={sound.src} controls />
|
<audio src={sound.src} controls />
|
||||||
<div className={style.inner}>
|
<div className={style.inner}>
|
||||||
<span>{sound.name}</span>
|
<span>{sound.name}</span>
|
||||||
<CloseIcon fill={'#fff'} />
|
<CloseIcon
|
||||||
|
fill={'#fff'}
|
||||||
|
onClick={() => onDeleteSoundClick(idx)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -213,43 +495,177 @@ const ConfigDialog: FC<{
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
placeholder={'输入音效名称'}
|
placeholder={'输入音效名称'}
|
||||||
onChange={(event) =>
|
value={newSound.name}
|
||||||
setEditSound({
|
onChange={(e) => onNewSoundChange('name', e.target.value)}
|
||||||
name: event.target.value,
|
/>
|
||||||
src: editSound.src,
|
<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
|
<input
|
||||||
placeholder={'或者输入https外链'}
|
placeholder={'或者输入https外链'}
|
||||||
onChange={(event) =>
|
value={newSound.src}
|
||||||
setEditSound({
|
onChange={(e) => onNewSoundChange('src', e.target.value)}
|
||||||
src: event.target.value,
|
|
||||||
name: editSound.name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<button
|
{soundError && (
|
||||||
onClick={() =>
|
<div className={style.errorTip}>{soundError}</div>
|
||||||
setCustomTheme({
|
)}
|
||||||
...customTheme,
|
<button onClick={onAddNewSoundClick}>添加音效</button>
|
||||||
sounds: [...customTheme.sounds, editSound],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
添加音效
|
|
||||||
</button>
|
|
||||||
</InputContainer>
|
</InputContainer>
|
||||||
<InputContainer label={'图标素材'} required>
|
<InputContainer label={'图标素材'} required>
|
||||||
<div className={'flex-container flex-left-center'}>
|
<div className={style.tip}>
|
||||||
{customTheme.icons.map((icon, idx) => {
|
接口上传体积有限制,上传文件请全力压缩到5k以下,推荐尺寸56*56
|
||||||
return <div key={icon.name}>{icon.name}</div>;
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</InputContainer>
|
</InputContainer>
|
||||||
<InputContainer label={'操作音效'}>??</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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</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 />
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
LAST_LEVEL_STORAGE_KEY,
|
LAST_LEVEL_STORAGE_KEY,
|
||||||
LAST_SCORE_STORAGE_KEY,
|
LAST_SCORE_STORAGE_KEY,
|
||||||
LAST_TIME_STORAGE_KEY,
|
LAST_TIME_STORAGE_KEY,
|
||||||
|
linkReg,
|
||||||
randomString,
|
randomString,
|
||||||
waitTimeout,
|
waitTimeout,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
|
@ -132,8 +133,7 @@ const Symbol: FC<SymbolProps> = ({ x, y, icon, isCover, status, onClick }) => {
|
||||||
style={{ opacity: isCover ? 0.4 : 1 }}
|
style={{ opacity: isCover ? 0.4 : 1 }}
|
||||||
>
|
>
|
||||||
{typeof icon.content === 'string' ? (
|
{typeof icon.content === 'string' ? (
|
||||||
icon.content.startsWith('http') ||
|
linkReg.test(icon.content) ? (
|
||||||
icon.content.startsWith('/') ? (
|
|
||||||
/*图片地址*/
|
/*图片地址*/
|
||||||
<img src={icon.content} alt="" />
|
<img src={icon.content} alt="" />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -74,7 +74,7 @@
|
||||||
right: 8px;
|
right: 8px;
|
||||||
bottom: 8px;
|
bottom: 8px;
|
||||||
transition: 0.5s;
|
transition: 0.5s;
|
||||||
height: 250px;
|
height: 280px;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: rgb(0 0 0 / 30%);
|
background-color: rgb(0 0 0 / 30%);
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
&Title {
|
&Title {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&Item {
|
&Item {
|
||||||
|
|
|
@ -24,6 +24,7 @@ button {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: #3338;
|
background-color: #3338;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
&.primary {
|
&.primary {
|
||||||
background-color: #747bff;
|
background-color: #747bff;
|
||||||
|
|
33
src/utils.ts
33
src/utils.ts
|
@ -4,12 +4,15 @@ import { getDefaultTheme } from './themes/default';
|
||||||
export const LAST_LEVEL_STORAGE_KEY = 'lastLevel';
|
export const LAST_LEVEL_STORAGE_KEY = 'lastLevel';
|
||||||
export const LAST_SCORE_STORAGE_KEY = 'lastScore';
|
export const LAST_SCORE_STORAGE_KEY = 'lastScore';
|
||||||
export const LAST_TIME_STORAGE_KEY = 'lastTime';
|
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_STORAGE_KEY = 'customTheme';
|
||||||
|
export const CUSTOM_THEME_FILE_VALIDATE_STORAGE_KEY = 'customThemeFileValidate';
|
||||||
export const DEFAULT_BGM_STORAGE_KEY = 'defaultBgm';
|
export const DEFAULT_BGM_STORAGE_KEY = 'defaultBgm';
|
||||||
export const DEFAULT_TRIPLE_SOUND_STORAGE_KEY = 'defaultTripleSound';
|
export const DEFAULT_TRIPLE_SOUND_STORAGE_KEY = 'defaultTripleSound';
|
||||||
export const DEFAULT_CLICK_SOUND_STORAGE_KEY = 'defaultClickSound';
|
export const DEFAULT_CLICK_SOUND_STORAGE_KEY = 'defaultClickSound';
|
||||||
|
|
||||||
|
export const linkReg = /^(https|data):+/;
|
||||||
|
|
||||||
export const randomString: (len: number) => string = (len) => {
|
export const randomString: (len: number) => string = (len) => {
|
||||||
const pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
const pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
let res = '';
|
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>) => {
|
export const domRelatedOptForTheme = (theme: Theme<any>) => {
|
||||||
document.body.style.backgroundColor = theme.backgroundColor || 'white';
|
document.body.style.backgroundColor = theme.backgroundColor || 'white';
|
||||||
document.body.style.color = theme.dark ? 'white' : 'rgb(0 0 0 / 60%)';
|
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('读取文件失败');
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue
Block a user