mirror of
https://ghproxy.com/https://github.com/StreakingMan/solvable-sheep-game
synced 2025-05-25 03:08:15 +08:00
Compare commits
No commits in common. "master" and "v1.0.1" have entirely different histories.
0
.husky/commit-msg
Normal file → Executable file
0
.husky/commit-msg
Normal file → Executable file
0
.husky/pre-commit
Normal file → Executable file
0
.husky/pre-commit
Normal file → Executable file
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -2,20 +2,6 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
## [1.1.0](https://github.com/StreakingMan/solvable-sheep-game/compare/v1.0.1...v1.1.0) (2023-05-04)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 本地图片文件压缩 ([ffbb384](https://github.com/StreakingMan/solvable-sheep-game/commit/ffbb38495f2c8c124f6039496f613e6c416d9db4))
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 图片文件大小校验逻辑 ([be0faff](https://github.com/StreakingMan/solvable-sheep-game/commit/be0faff9fdf4aea6a2659325520d68b2f348c656))
|
|
||||||
* 自定义主题编辑回显 ([ffabf68](https://github.com/StreakingMan/solvable-sheep-game/commit/ffabf6805f81dd03bd070dd09a32e51a51d1138f))
|
|
||||||
* 音频文件切换为minio外链 ([680811c](https://github.com/StreakingMan/solvable-sheep-game/commit/680811c50cbef3fec80b73407daddcf48847f3bd))
|
|
||||||
|
|
||||||
### [1.0.1](https://github.com/StreakingMan/solvable-sheep-game/compare/v1.0.0...v1.0.1) (2022-10-19)
|
### [1.0.1](https://github.com/StreakingMan/solvable-sheep-game/compare/v1.0.0...v1.0.1) (2022-10-19)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -76,9 +76,7 @@ vite+react 实现,欢迎 star、issue、pr、fork(尽量标注原仓库地
|
||||||
|
|
||||||
## 资助
|
## 资助
|
||||||
|
|
||||||
~~由于各种白嫖的静态资源托管、后台服务的免费额度都已用完,目前自费升级了相关套餐。~~
|
由于各种白嫖的静态资源托管、后台服务的免费额度都已用完,目前自费升级了相关套餐。
|
||||||
如果您喜欢这个项目,觉得本项目对你有帮助的话,可以扫描下方付款码请我喝杯咖啡 ☕️/~~分摊后台服务费用~~~ 😘
|
如果您喜欢这个项目,觉得本项目对你有帮助的话,可以扫描下方付款码请我喝杯咖啡 ☕️/分摊后台服务费用~ 😘
|
||||||
|
|
||||||
2023.5.5 更新:Bmob 服务到期,后台服务已下线,相关功能暂时无法使用,如有需要请自行搭建后台服务
|
|
||||||
|
|
||||||

|

|
||||||
|
|
|
@ -2,9 +2,6 @@
|
||||||
|
|
||||||
游戏的核心逻辑已经封装到了 `src/components/Game.tsx` ,方便大家魔改, 主题配置的类型声明见 `src/themes/interface.ts`
|
游戏的核心逻辑已经封装到了 `src/components/Game.tsx` ,方便大家魔改, 主题配置的类型声明见 `src/themes/interface.ts`
|
||||||
|
|
||||||
你可以先通过超酷的[stackblitz](https://stackblitz.com/edit/solvable-sheep-game?file=diy%2Fdiy.theme.json&terminal=dev:diy)
|
|
||||||
在线体验一番(等待依赖安装完成后,编辑配置将实时更新)再回来看这里的指南。
|
|
||||||
|
|
||||||
## 准备工作
|
## 准备工作
|
||||||
|
|
||||||
### 环境准备
|
### 环境准备
|
||||||
|
@ -97,6 +94,9 @@ ps: 如果您的项目托管在公共仓库中,请注意保护密钥,本地
|
||||||
|
|
||||||
`config` 表用来存储自定义配置的 json 字符串,需要新增 `content` 列
|
`config` 表用来存储自定义配置的 json 字符串,需要新增 `content` 列
|
||||||
|
|
||||||
|
`file` 表则是为了节省 vercel 流量,将一些默认文件转为 base64 字符串存到了数据库中,需要添加三列
|
||||||
|

|
||||||
|
|
||||||
`rank` 表,储存排名信息
|
`rank` 表,储存排名信息
|
||||||

|

|
||||||
|
|
||||||
|
|
BIN
diy/database-file.png
Normal file
BIN
diy/database-file.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "solvable-sheep-game",
|
"name": "solvable-sheep-game",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.1.0",
|
"version": "1.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|
|
@ -15,8 +15,6 @@ import {
|
||||||
randomString,
|
randomString,
|
||||||
wrapThemeDefaultSounds,
|
wrapThemeDefaultSounds,
|
||||||
LAST_UPLOAD_TIME_STORAGE_KEY,
|
LAST_UPLOAD_TIME_STORAGE_KEY,
|
||||||
canvasToFile,
|
|
||||||
createCanvasByImgSrc,
|
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { copy } from 'clipboard';
|
import { copy } from 'clipboard';
|
||||||
import { CloseIcon } from './CloseIcon';
|
import { CloseIcon } from './CloseIcon';
|
||||||
|
@ -146,14 +144,10 @@ const ConfigDialog: FC<{
|
||||||
new Array(10).fill('')
|
new Array(10).fill('')
|
||||||
);
|
);
|
||||||
// 文件体积校验开关
|
// 文件体积校验开关
|
||||||
const initEnableFileSizeValidate = localStorage.getItem(
|
|
||||||
CUSTOM_THEME_FILE_VALIDATE_STORAGE_KEY
|
|
||||||
);
|
|
||||||
const [enableFileSizeValidate, setEnableFileSizeValidate] =
|
const [enableFileSizeValidate, setEnableFileSizeValidate] =
|
||||||
useState<boolean>(
|
useState<boolean>(
|
||||||
initEnableFileSizeValidate === null
|
localStorage.getItem(CUSTOM_THEME_FILE_VALIDATE_STORAGE_KEY) !==
|
||||||
? true
|
'false'
|
||||||
: initEnableFileSizeValidate === 'true'
|
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
|
@ -169,7 +163,7 @@ const ConfigDialog: FC<{
|
||||||
type: 'bgm' | 'background' | 'sound' | 'icon';
|
type: 'bgm' | 'background' | 'sound' | 'icon';
|
||||||
file?: File;
|
file?: File;
|
||||||
idx?: number;
|
idx?: number;
|
||||||
}) => void = async ({ type, file, idx }) => {
|
}) => void = ({ type, file, idx }) => {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'bgm':
|
case 'bgm':
|
||||||
|
@ -187,20 +181,16 @@ const ConfigDialog: FC<{
|
||||||
break;
|
break;
|
||||||
case 'background':
|
case 'background':
|
||||||
setBackgroundError('');
|
setBackgroundError('');
|
||||||
try {
|
if (enableFileSizeValidate && file.size > 80 * 1024) {
|
||||||
const _file = enableFileSizeValidate
|
return setBackgroundError('请选择80k以内全损画质的图片');
|
||||||
? await canvasToFile({
|
|
||||||
canvas: await createCanvasByImgSrc({
|
|
||||||
imgSrc: await getFileBase64String(file),
|
|
||||||
}),
|
|
||||||
maxFileSize: 20 * 1024,
|
|
||||||
})
|
|
||||||
: file;
|
|
||||||
const fileBase64 = await getFileBase64String(_file);
|
|
||||||
updateCustomTheme('background', fileBase64);
|
|
||||||
} catch (e: any) {
|
|
||||||
setBackgroundError(e);
|
|
||||||
}
|
}
|
||||||
|
getFileBase64String(file)
|
||||||
|
.then((res) => {
|
||||||
|
updateCustomTheme('background', res);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
setBackgroundError(e);
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case 'sound':
|
case 'sound':
|
||||||
setSoundError('');
|
setSoundError('');
|
||||||
|
@ -218,27 +208,23 @@ const ConfigDialog: FC<{
|
||||||
case 'icon':
|
case 'icon':
|
||||||
if (idx == null) return;
|
if (idx == null) return;
|
||||||
setIconErrors(makeIconErrors(idx, ''));
|
setIconErrors(makeIconErrors(idx, ''));
|
||||||
try {
|
if (enableFileSizeValidate && file.size > 5 * 1024) {
|
||||||
const _file = enableFileSizeValidate
|
return setIconErrors(
|
||||||
? await canvasToFile({
|
makeIconErrors(idx, '请选择5k以内的图片文件')
|
||||||
canvas: await createCanvasByImgSrc({
|
);
|
||||||
imgSrc: await getFileBase64String(file),
|
}
|
||||||
}),
|
getFileBase64String(file)
|
||||||
maxFileSize: 4 * 1024,
|
.then((res) => {
|
||||||
})
|
|
||||||
: file;
|
|
||||||
const fileBase64 = await getFileBase64String(_file);
|
|
||||||
updateCustomTheme(
|
updateCustomTheme(
|
||||||
'icons',
|
'icons',
|
||||||
customTheme.icons.map((icon, _idx) =>
|
customTheme.icons.map((icon, _idx) =>
|
||||||
_idx === idx
|
_idx === idx ? { ...icon, content: res } : icon
|
||||||
? { ...icon, content: fileBase64 }
|
|
||||||
: icon
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} catch (e: any) {
|
})
|
||||||
|
.catch((e) => {
|
||||||
setIconErrors(makeIconErrors(idx, e));
|
setIconErrors(makeIconErrors(idx, e));
|
||||||
}
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -265,9 +251,7 @@ const ConfigDialog: FC<{
|
||||||
if (configString) {
|
if (configString) {
|
||||||
const parseRes = JSON.parse(configString);
|
const parseRes = JSON.parse(configString);
|
||||||
if (typeof parseRes === 'object') {
|
if (typeof parseRes === 'object') {
|
||||||
setTimeout(() => {
|
|
||||||
setCustomTheme(parseRes);
|
setCustomTheme(parseRes);
|
||||||
}, 300);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -373,7 +357,11 @@ const ConfigDialog: FC<{
|
||||||
})
|
})
|
||||||
.catch(({ error, code }) => {
|
.catch(({ error, code }) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setConfigError(error);
|
setConfigError(
|
||||||
|
code === 10007
|
||||||
|
? '上传数据长度已超过bmob的限制'
|
||||||
|
: error
|
||||||
|
);
|
||||||
}, 3000);
|
}, 3000);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
@ -403,17 +391,6 @@ const ConfigDialog: FC<{
|
||||||
<CloseIcon fill={'#fff'} />
|
<CloseIcon fill={'#fff'} />
|
||||||
</div>
|
</div>
|
||||||
<h2>自定义主题</h2>
|
<h2>自定义主题</h2>
|
||||||
<p style={{ color: 'red' }}>
|
|
||||||
后台服务到期,分享链接功能已停用,有需要的同学参考
|
|
||||||
<a
|
|
||||||
href="https://github.com/StreakingMan/solvable-sheep-game/blob/master/diy/README.md"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
👉这里👈
|
|
||||||
</a>
|
|
||||||
自行部署,感谢支持!🙏
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<InputContainer label={'标题'} required>
|
<InputContainer label={'标题'} required>
|
||||||
<input
|
<input
|
||||||
|
@ -431,7 +408,7 @@ const ConfigDialog: FC<{
|
||||||
</InputContainer>
|
</InputContainer>
|
||||||
<InputContainer label={'BGM'}>
|
<InputContainer label={'BGM'}>
|
||||||
<div className={style.tip}>
|
<div className={style.tip}>
|
||||||
接口上传体积有限制,上传文件请全力压缩到80k以下,推荐使用外链
|
接口上传体积有限制,上传文件请全力压缩到80k以下
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type={'file'}
|
type={'file'}
|
||||||
|
@ -453,7 +430,7 @@ const ConfigDialog: FC<{
|
||||||
</InputContainer>
|
</InputContainer>
|
||||||
<InputContainer label={'背景图'}>
|
<InputContainer label={'背景图'}>
|
||||||
<div className={style.tip}>
|
<div className={style.tip}>
|
||||||
接口上传体积有限制,上传的图片将会被严重压缩,推荐使用外链
|
接口上传体积有限制,上传文件请全力压缩到80k以下
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type={'file'}
|
type={'file'}
|
||||||
|
@ -559,7 +536,7 @@ const ConfigDialog: FC<{
|
||||||
onChange={(e) => onNewSoundChange('name', e.target.value)}
|
onChange={(e) => onNewSoundChange('name', e.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className={style.tip}>
|
<div className={style.tip}>
|
||||||
接口上传体积有限制,上传文件请全力压缩到10k以下,推荐使用外链
|
接口上传体积有限制,上传文件请全力压缩到10k以下
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type={'file'}
|
type={'file'}
|
||||||
|
@ -583,7 +560,7 @@ const ConfigDialog: FC<{
|
||||||
</InputContainer>
|
</InputContainer>
|
||||||
<InputContainer label={'图标素材'} required>
|
<InputContainer label={'图标素材'} required>
|
||||||
<div className={style.tip}>
|
<div className={style.tip}>
|
||||||
接口上传体积有限制,上传的图片将会被严重压缩,推荐使用外链
|
接口上传体积有限制,上传文件请全力压缩到5k以下,推荐尺寸56*56
|
||||||
</div>
|
</div>
|
||||||
</InputContainer>
|
</InputContainer>
|
||||||
{customTheme.icons.map((icon, idx) => (
|
{customTheme.icons.map((icon, idx) => (
|
||||||
|
@ -708,7 +685,7 @@ const ConfigDialog: FC<{
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={style.tip}>
|
<div className={style.tip}>
|
||||||
接口上传内容总体积有限制,上传文件失败请尝试进一步压缩文件,推荐使用外链(自行搜索【免费图床】【免费mp3外链】【对象存储服务】等关键词)。
|
接口上传内容总体积有限制,上传文件失败请进一步压缩文件,或者使用外链(自行搜索【免费图床】【免费mp3外链】【对象存储服务】等关键词)。
|
||||||
本地整活,勾选右侧关闭文件大小校验👉
|
本地整活,勾选右侧关闭文件大小校验👉
|
||||||
<input
|
<input
|
||||||
type={'checkbox'}
|
type={'checkbox'}
|
||||||
|
@ -733,17 +710,16 @@ const ConfigDialog: FC<{
|
||||||
>
|
>
|
||||||
保存并预览
|
保存并预览
|
||||||
</button>
|
</button>
|
||||||
{/*<button*/}
|
<button
|
||||||
{/* className={classNames(*/}
|
className={classNames(
|
||||||
{/* 'primary flex-grow',*/}
|
'primary flex-grow',
|
||||||
{/* style.uploadBtn,*/}
|
style.uploadBtn,
|
||||||
{/* uploading && style.uploading*/}
|
uploading && style.uploading
|
||||||
{/* )}*/}
|
)}
|
||||||
{/* onClick={onGenQrLinkClick}*/}
|
onClick={onGenQrLinkClick}
|
||||||
{/* disabled*/}
|
>
|
||||||
{/*>*/}
|
生成二维码&链接
|
||||||
{/* 生成二维码&链接*/}
|
</button>
|
||||||
{/*</button>*/}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React, { FC, MouseEventHandler, useState } from 'react';
|
||||||
import style from './WxQrCode.module.scss';
|
import style from './WxQrCode.module.scss';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
const WxQrCode: FC<{ title?: string; onClick?: MouseEventHandler }> = ({
|
const WxQrCode: FC<{ title?: string; onClick?: MouseEventHandler }> = ({
|
||||||
title = '如果您喜欢这个项目的话,点击扫描下方收款码请我喝杯咖啡,感谢~😘',
|
title = '如果您喜欢这个项目的话,可以点击扫描下方收款码分担后台相关费用(或请我喝杯咖啡),感谢~😘',
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
const [fullScreen, setFullScreen] = useState<Record<number, boolean>>({
|
const [fullScreen, setFullScreen] = useState<Record<number, boolean>>({
|
||||||
|
|
25
src/main.tsx
25
src/main.tsx
|
@ -5,7 +5,11 @@ import './styles/global.scss';
|
||||||
import './styles/utils.scss';
|
import './styles/utils.scss';
|
||||||
import Bmob from 'hydrogen-js-sdk';
|
import Bmob from 'hydrogen-js-sdk';
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_BGM_STORAGE_KEY,
|
||||||
domRelatedOptForTheme,
|
domRelatedOptForTheme,
|
||||||
|
LAST_LEVEL_STORAGE_KEY,
|
||||||
|
LAST_SCORE_STORAGE_KEY,
|
||||||
|
LAST_TIME_STORAGE_KEY,
|
||||||
parsePathCustomThemeId,
|
parsePathCustomThemeId,
|
||||||
PLAYING_THEME_ID_STORAGE_KEY,
|
PLAYING_THEME_ID_STORAGE_KEY,
|
||||||
resetScoreStorage,
|
resetScoreStorage,
|
||||||
|
@ -73,6 +77,7 @@ Bmob.initialize(
|
||||||
import.meta.env.VITE_BMOB_SECCODE
|
import.meta.env.VITE_BMOB_SECCODE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const loadTheme = () => {
|
||||||
// 请求主题
|
// 请求主题
|
||||||
if (customThemeIdFromPath) {
|
if (customThemeIdFromPath) {
|
||||||
const storageTheme = localStorage.getItem(customThemeIdFromPath);
|
const storageTheme = localStorage.getItem(customThemeIdFromPath);
|
||||||
|
@ -107,3 +112,23 @@ if (customThemeIdFromPath) {
|
||||||
} else {
|
} else {
|
||||||
successTrans(getDefaultTheme());
|
successTrans(getDefaultTheme());
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 音效资源请求
|
||||||
|
if (!localStorage.getItem(DEFAULT_BGM_STORAGE_KEY)) {
|
||||||
|
const query = Bmob.Query('file');
|
||||||
|
query.equalTo('type', '==', 'default');
|
||||||
|
query
|
||||||
|
.find()
|
||||||
|
.then((results) => {
|
||||||
|
for (const file of results as any) {
|
||||||
|
localStorage.setItem(file.name, file.base64);
|
||||||
|
}
|
||||||
|
loadTheme();
|
||||||
|
})
|
||||||
|
.catch(({ error }) => {
|
||||||
|
errorTip(error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
loadTheme();
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
import { Theme } from '../interface';
|
import { Theme } from '../interface';
|
||||||
|
import {
|
||||||
|
DEFAULT_BGM_STORAGE_KEY,
|
||||||
|
DEFAULT_CLICK_SOUND_STORAGE_KEY,
|
||||||
|
DEFAULT_TRIPLE_SOUND_STORAGE_KEY,
|
||||||
|
} from '../../utils';
|
||||||
|
|
||||||
const icons = <const>[
|
const icons = <const>[
|
||||||
`🎨`,
|
`🎨`,
|
||||||
|
@ -20,7 +25,7 @@ export const getDefaultTheme: () => Theme<DefaultSoundNames> = () => {
|
||||||
title: '有解的羊了个羊',
|
title: '有解的羊了个羊',
|
||||||
desc: '真的可以通关~',
|
desc: '真的可以通关~',
|
||||||
dark: true,
|
dark: true,
|
||||||
maxLevel: 20,
|
maxLevel: 5,
|
||||||
backgroundColor: '#8dac85',
|
backgroundColor: '#8dac85',
|
||||||
icons: icons.map((icon) => ({
|
icons: icons.map((icon) => ({
|
||||||
name: icon,
|
name: icon,
|
||||||
|
@ -31,13 +36,16 @@ export const getDefaultTheme: () => Theme<DefaultSoundNames> = () => {
|
||||||
sounds: [
|
sounds: [
|
||||||
{
|
{
|
||||||
name: 'button-click',
|
name: 'button-click',
|
||||||
src: 'https://minio.streakingman.com/solvable-sheep-game/sound-button-click.mp3',
|
src:
|
||||||
|
localStorage.getItem(DEFAULT_CLICK_SOUND_STORAGE_KEY) || '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'triple',
|
name: 'triple',
|
||||||
src: 'https://minio.streakingman.com/solvable-sheep-game/sound-triple.mp3',
|
src:
|
||||||
|
localStorage.getItem(DEFAULT_TRIPLE_SOUND_STORAGE_KEY) ||
|
||||||
|
'',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
bgm: 'https://minio.streakingman.com/solvable-sheep-game/sound-disco.mp3',
|
bgm: localStorage.getItem(DEFAULT_BGM_STORAGE_KEY) || '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
104
src/utils.ts
104
src/utils.ts
|
@ -9,6 +9,9 @@ export const LAST_CUSTOM_THEME_ID_STORAGE_KEY = 'lastCustomThemeId';
|
||||||
export const LAST_UPLOAD_TIME_STORAGE_KEY = 'lastUploadTime';
|
export const LAST_UPLOAD_TIME_STORAGE_KEY = 'lastUploadTime';
|
||||||
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 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 USER_NAME_STORAGE_KEY = 'username';
|
export const USER_NAME_STORAGE_KEY = 'username';
|
||||||
export const USER_ID_STORAGE_KEY = 'userId';
|
export const USER_ID_STORAGE_KEY = 'userId';
|
||||||
export const PLAYING_THEME_ID_STORAGE_KEY = 'playingThemeId';
|
export const PLAYING_THEME_ID_STORAGE_KEY = 'playingThemeId';
|
||||||
|
@ -171,104 +174,3 @@ export const timestampToUsedTimeString: (time: number) => string = (time) => {
|
||||||
return '时间转换出错';
|
return '时间转换出错';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dataURLToFile: (dataURL: string, filename?: string) => File = (
|
|
||||||
// #endregion dataURLToFile
|
|
||||||
dataURL,
|
|
||||||
filename
|
|
||||||
) => {
|
|
||||||
const isDataURL = dataURL.startsWith('data:');
|
|
||||||
if (!isDataURL) throw new Error('Data URL 错误');
|
|
||||||
const _fileName = filename || new Date().getTime().toString();
|
|
||||||
const mimeType = dataURL.match(/^data:([^;]+);/)?.[1] || '';
|
|
||||||
// base64转二进制
|
|
||||||
const binaryString = atob(dataURL.split(',')[1]);
|
|
||||||
let binaryStringLength = binaryString.length;
|
|
||||||
const bytes = new Uint8Array(binaryStringLength);
|
|
||||||
while (binaryStringLength--) {
|
|
||||||
bytes[binaryStringLength] = binaryString.charCodeAt(binaryStringLength);
|
|
||||||
}
|
|
||||||
return new File([bytes], _fileName, { type: mimeType });
|
|
||||||
};
|
|
||||||
|
|
||||||
interface drawImgSrcInCanvasParams {
|
|
||||||
imgSrc: string;
|
|
||||||
canvas: HTMLCanvasElement;
|
|
||||||
scale?: number;
|
|
||||||
}
|
|
||||||
export const drawImgSrcInCanvas: (
|
|
||||||
params: drawImgSrcInCanvasParams
|
|
||||||
) => Promise<void> = async ({ imgSrc, canvas, scale = 1 }) => {
|
|
||||||
if (scale < 0) throw new Error('scale不能小于0');
|
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (!ctx || !(ctx instanceof CanvasRenderingContext2D)) {
|
|
||||||
throw new Error('Failed to get 2D context');
|
|
||||||
}
|
|
||||||
const img = document.createElement('img');
|
|
||||||
img.setAttribute('src', imgSrc);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
img.onload = () => {
|
|
||||||
const width = img.width * scale;
|
|
||||||
const height = img.height * scale;
|
|
||||||
canvas.width = width;
|
|
||||||
canvas.height = height;
|
|
||||||
ctx.drawImage(img, 0, 0, width, height);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
img.onerror = () => {
|
|
||||||
reject(new Error('图片加载失败'));
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
interface createCanvasByImgSrcParams {
|
|
||||||
imgSrc: HTMLImageElement['src'];
|
|
||||||
scale?: number;
|
|
||||||
}
|
|
||||||
export const createCanvasByImgSrc: (
|
|
||||||
params: createCanvasByImgSrcParams
|
|
||||||
) => Promise<HTMLCanvasElement> = async ({ imgSrc, scale = 1 }) => {
|
|
||||||
if (scale < 0) throw new Error('scale不能小于0');
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
await drawImgSrcInCanvas({
|
|
||||||
imgSrc,
|
|
||||||
canvas,
|
|
||||||
scale,
|
|
||||||
});
|
|
||||||
return canvas;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface CanvasToFileParams {
|
|
||||||
canvas: HTMLCanvasElement;
|
|
||||||
fileName?: string;
|
|
||||||
maxFileSize?: number;
|
|
||||||
}
|
|
||||||
export const canvasToFile: (
|
|
||||||
params: CanvasToFileParams
|
|
||||||
) => Promise<File> = async ({ canvas, fileName, maxFileSize }) => {
|
|
||||||
// #endregion canvasToFile
|
|
||||||
const MIME_TYPE = 'image/png';
|
|
||||||
const dataURL = canvas.toDataURL(MIME_TYPE);
|
|
||||||
const _fileName = fileName || new Date().getTime().toString();
|
|
||||||
const genFile = dataURLToFile(dataURL, _fileName);
|
|
||||||
// 判断是否需要压缩
|
|
||||||
if (maxFileSize && genFile.size > maxFileSize) {
|
|
||||||
let scale = Math.sqrt(maxFileSize / genFile.size);
|
|
||||||
if (scale > 0.9) scale = 0.9;
|
|
||||||
// TODO 暂时通过canvas绘制缩放图像进行递归压缩,后续考虑其他方式
|
|
||||||
// TODO 不断生成canvas, 调研内存是否会泄漏
|
|
||||||
const _canvas = await createCanvasByImgSrc({
|
|
||||||
imgSrc: dataURL,
|
|
||||||
scale,
|
|
||||||
});
|
|
||||||
return canvasToFile({
|
|
||||||
canvas: _canvas,
|
|
||||||
fileName: _fileName,
|
|
||||||
maxFileSize,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return genFile;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user