Compare commits

...

10 Commits

Author SHA1 Message Date
streakingman
09e43ae02a chore(release): 1.1.0 2023-05-05 00:44:31 +08:00
streakingman
c5bd3f01d9 chore: 停用分享说明 2023-05-05 00:44:02 +08:00
streakingman
be0faff9fd fix: 图片文件大小校验逻辑 2023-05-05 00:33:21 +08:00
streakingman
cebf64847f chore: 服务到期说明 2023-05-05 00:20:14 +08:00
streakingman
ffbb38495f feat: 本地图片文件压缩 2023-05-04 23:52:21 +08:00
streakingman
e553e5360f chore: update diy README.md 2023-01-17 22:02:14 +08:00
StreakingMan
d234bd06a8 1 binaries uploaded 2023-01-17 21:03:05 +08:00
streakingman
680811c50c fix: 音频文件切换为minio外链 2022-11-10 07:57:54 +08:00
streakingman
ffabf6805f fix: 自定义主题编辑回显 2022-11-04 10:11:12 +08:00
streakingman
58bff1ce98 revert: 默认主题关卡数 2022-10-19 23:56:13 +08:00
12 changed files with 232 additions and 127 deletions

0
.husky/commit-msg Executable file → Normal file
View File

0
.husky/pre-commit Executable file → Normal file
View File

View File

@ -2,6 +2,20 @@
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)

View File

@ -76,7 +76,9 @@ vite+react 实现,欢迎 star、issue、pr、fork尽量标注原仓库地
## 资助
由于各种白嫖的静态资源托管、后台服务的免费额度都已用完,目前自费升级了相关套餐。
如果您喜欢这个项目,觉得本项目对你有帮助的话,可以扫描下方付款码请我喝杯咖啡 ☕️/分摊后台服务费用~ 😘
~~由于各种白嫖的静态资源托管、后台服务的免费额度都已用完,目前自费升级了相关套餐。~~
如果您喜欢这个项目,觉得本项目对你有帮助的话,可以扫描下方付款码请我喝杯咖啡 ☕️/~~分摊后台服务费用~~ 😘
2023.5.5 更新Bmob 服务到期,后台服务已下线,相关功能暂时无法使用,如有需要请自行搭建后台服务
![wxQrCodes.png](wxQrCodes.png)

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

View File

@ -1,7 +1,7 @@
{
"name": "solvable-sheep-game",
"private": false,
"version": "1.0.1",
"version": "1.1.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@ -15,6 +15,8 @@ import {
randomString,
wrapThemeDefaultSounds,
LAST_UPLOAD_TIME_STORAGE_KEY,
canvasToFile,
createCanvasByImgSrc,
} from '../utils';
import { copy } from 'clipboard';
import { CloseIcon } from './CloseIcon';
@ -144,10 +146,14 @@ const ConfigDialog: FC<{
new Array(10).fill('')
);
// 文件体积校验开关
const initEnableFileSizeValidate = localStorage.getItem(
CUSTOM_THEME_FILE_VALIDATE_STORAGE_KEY
);
const [enableFileSizeValidate, setEnableFileSizeValidate] =
useState<boolean>(
localStorage.getItem(CUSTOM_THEME_FILE_VALIDATE_STORAGE_KEY) !==
'false'
initEnableFileSizeValidate === null
? true
: initEnableFileSizeValidate === 'true'
);
useEffect(() => {
localStorage.setItem(
@ -163,7 +169,7 @@ const ConfigDialog: FC<{
type: 'bgm' | 'background' | 'sound' | 'icon';
file?: File;
idx?: number;
}) => void = ({ type, file, idx }) => {
}) => void = async ({ type, file, idx }) => {
if (!file) return;
switch (type) {
case 'bgm':
@ -181,16 +187,20 @@ const ConfigDialog: FC<{
break;
case 'background':
setBackgroundError('');
if (enableFileSizeValidate && file.size > 80 * 1024) {
return setBackgroundError('请选择80k以内全损画质的图片');
try {
const _file = enableFileSizeValidate
? 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;
case 'sound':
setSoundError('');
@ -208,23 +218,27 @@ const ConfigDialog: FC<{
case 'icon':
if (idx == null) return;
setIconErrors(makeIconErrors(idx, ''));
if (enableFileSizeValidate && file.size > 5 * 1024) {
return setIconErrors(
makeIconErrors(idx, '请选择5k以内的图片文件')
try {
const _file = enableFileSizeValidate
? await canvasToFile({
canvas: await createCanvasByImgSrc({
imgSrc: await getFileBase64String(file),
}),
maxFileSize: 4 * 1024,
})
: file;
const fileBase64 = await getFileBase64String(_file);
updateCustomTheme(
'icons',
customTheme.icons.map((icon, _idx) =>
_idx === idx
? { ...icon, content: fileBase64 }
: icon
)
);
} catch (e: any) {
setIconErrors(makeIconErrors(idx, e));
}
getFileBase64String(file)
.then((res) => {
updateCustomTheme(
'icons',
customTheme.icons.map((icon, _idx) =>
_idx === idx ? { ...icon, content: res } : icon
)
);
})
.catch((e) => {
setIconErrors(makeIconErrors(idx, e));
});
break;
}
};
@ -251,7 +265,9 @@ const ConfigDialog: FC<{
if (configString) {
const parseRes = JSON.parse(configString);
if (typeof parseRes === 'object') {
setCustomTheme(parseRes);
setTimeout(() => {
setCustomTheme(parseRes);
}, 300);
}
}
} catch (e) {
@ -357,11 +373,7 @@ const ConfigDialog: FC<{
})
.catch(({ error, code }) => {
setTimeout(() => {
setConfigError(
code === 10007
? '上传数据长度已超过bmob的限制'
: error
);
setConfigError(error);
}, 3000);
})
.finally(() => {
@ -391,6 +403,17 @@ const ConfigDialog: FC<{
<CloseIcon fill={'#fff'} />
</div>
<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>
<input
@ -408,7 +431,7 @@ const ConfigDialog: FC<{
</InputContainer>
<InputContainer label={'BGM'}>
<div className={style.tip}>
80k以下
80k以下使
</div>
<input
type={'file'}
@ -430,7 +453,7 @@ const ConfigDialog: FC<{
</InputContainer>
<InputContainer label={'背景图'}>
<div className={style.tip}>
80k以下
使
</div>
<input
type={'file'}
@ -536,7 +559,7 @@ const ConfigDialog: FC<{
onChange={(e) => onNewSoundChange('name', e.target.value)}
/>
<div className={style.tip}>
10k以下
10k以下使
</div>
<input
type={'file'}
@ -560,7 +583,7 @@ const ConfigDialog: FC<{
</InputContainer>
<InputContainer label={'图标素材'} required>
<div className={style.tip}>
5k以下56*56
使
</div>
</InputContainer>
{customTheme.icons.map((icon, idx) => (
@ -685,7 +708,7 @@ const ConfigDialog: FC<{
</div>
)}
<div className={style.tip}>
使mp3外链
使mp3外链
👉
<input
type={'checkbox'}
@ -710,16 +733,17 @@ const ConfigDialog: FC<{
>
</button>
<button
className={classNames(
'primary flex-grow',
style.uploadBtn,
uploading && style.uploading
)}
onClick={onGenQrLinkClick}
>
&
</button>
{/*<button*/}
{/* className={classNames(*/}
{/* 'primary flex-grow',*/}
{/* style.uploadBtn,*/}
{/* uploading && style.uploading*/}
{/* )}*/}
{/* onClick={onGenQrLinkClick}*/}
{/* disabled*/}
{/*>*/}
{/* 生成二维码&链接*/}
{/*</button>*/}
</div>
</div>
);

View File

@ -2,7 +2,7 @@ import React, { FC, MouseEventHandler, useState } from 'react';
import style from './WxQrCode.module.scss';
import classNames from 'classnames';
const WxQrCode: FC<{ title?: string; onClick?: MouseEventHandler }> = ({
title = '如果您喜欢这个项目的话,可以点击扫描下方收款码分担后台相关费用(或请我喝杯咖啡,感谢~😘',
title = '如果您喜欢这个项目的话,点击扫描下方收款码请我喝杯咖啡,感谢~😘',
onClick,
}) => {
const [fullScreen, setFullScreen] = useState<Record<number, boolean>>({

View File

@ -5,11 +5,7 @@ import './styles/global.scss';
import './styles/utils.scss';
import Bmob from 'hydrogen-js-sdk';
import {
DEFAULT_BGM_STORAGE_KEY,
domRelatedOptForTheme,
LAST_LEVEL_STORAGE_KEY,
LAST_SCORE_STORAGE_KEY,
LAST_TIME_STORAGE_KEY,
parsePathCustomThemeId,
PLAYING_THEME_ID_STORAGE_KEY,
resetScoreStorage,
@ -77,58 +73,37 @@ Bmob.initialize(
import.meta.env.VITE_BMOB_SECCODE
);
const loadTheme = () => {
// 请求主题
if (customThemeIdFromPath) {
const storageTheme = localStorage.getItem(customThemeIdFromPath);
if (storageTheme) {
try {
const customTheme = JSON.parse(storageTheme);
successTrans(customTheme);
} catch (e) {
errorTip('主题配置解析失败');
}
} else {
Bmob.Query('config')
.get(customThemeIdFromPath)
.then((res) => {
const { content, increment } = res as any;
localStorage.setItem(customThemeIdFromPath, content);
try {
const customTheme = JSON.parse(content);
successTrans(customTheme);
} catch (e) {
errorTip('主题配置解析失败');
}
// 统计访问次数
increment('visitNum');
// @ts-ignore
res.save();
})
.catch(({ error }) => {
errorTip(error);
});
// 请求主题
if (customThemeIdFromPath) {
const storageTheme = localStorage.getItem(customThemeIdFromPath);
if (storageTheme) {
try {
const customTheme = JSON.parse(storageTheme);
successTrans(customTheme);
} catch (e) {
errorTip('主题配置解析失败');
}
} else {
successTrans(getDefaultTheme());
Bmob.Query('config')
.get(customThemeIdFromPath)
.then((res) => {
const { content, increment } = res as any;
localStorage.setItem(customThemeIdFromPath, content);
try {
const customTheme = JSON.parse(content);
successTrans(customTheme);
} catch (e) {
errorTip('主题配置解析失败');
}
// 统计访问次数
increment('visitNum');
// @ts-ignore
res.save();
})
.catch(({ error }) => {
errorTip(error);
});
}
};
// 音效资源请求
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();
successTrans(getDefaultTheme());
}

View File

@ -1,9 +1,4 @@
import { Theme } from '../interface';
import {
DEFAULT_BGM_STORAGE_KEY,
DEFAULT_CLICK_SOUND_STORAGE_KEY,
DEFAULT_TRIPLE_SOUND_STORAGE_KEY,
} from '../../utils';
const icons = <const>[
`🎨`,
@ -25,7 +20,7 @@ export const getDefaultTheme: () => Theme<DefaultSoundNames> = () => {
title: '有解的羊了个羊',
desc: '真的可以通关~',
dark: true,
maxLevel: 5,
maxLevel: 20,
backgroundColor: '#8dac85',
icons: icons.map((icon) => ({
name: icon,
@ -36,16 +31,13 @@ export const getDefaultTheme: () => Theme<DefaultSoundNames> = () => {
sounds: [
{
name: 'button-click',
src:
localStorage.getItem(DEFAULT_CLICK_SOUND_STORAGE_KEY) || '',
src: 'https://minio.streakingman.com/solvable-sheep-game/sound-button-click.mp3',
},
{
name: 'triple',
src:
localStorage.getItem(DEFAULT_TRIPLE_SOUND_STORAGE_KEY) ||
'',
src: 'https://minio.streakingman.com/solvable-sheep-game/sound-triple.mp3',
},
],
bgm: localStorage.getItem(DEFAULT_BGM_STORAGE_KEY) || '',
bgm: 'https://minio.streakingman.com/solvable-sheep-game/sound-disco.mp3',
};
};

View File

@ -9,9 +9,6 @@ export const LAST_CUSTOM_THEME_ID_STORAGE_KEY = 'lastCustomThemeId';
export const LAST_UPLOAD_TIME_STORAGE_KEY = 'lastUploadTime';
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 USER_NAME_STORAGE_KEY = 'username';
export const USER_ID_STORAGE_KEY = 'userId';
export const PLAYING_THEME_ID_STORAGE_KEY = 'playingThemeId';
@ -174,3 +171,104 @@ export const timestampToUsedTimeString: (time: number) => string = (time) => {
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;
}
};