From 1890cc5a6f0ee56f7c0a9a1bfe10eb891e375709 Mon Sep 17 00:00:00 2001 From: streakingman Date: Tue, 11 Oct 2022 04:59:08 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E8=A1=A8=E5=8D=95=E5=B8=83=E5=B1=80=E9=87=8D?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ConfigDialog.module.scss | 213 +++--- src/components/ConfigDialog.tsx | 683 ++++-------------- .../FixedAnimateScalePanel.module.scss | 10 + src/styles/global.scss | 15 +- src/styles/utils.scss | 5 + 5 files changed, 294 insertions(+), 632 deletions(-) diff --git a/src/components/ConfigDialog.module.scss b/src/components/ConfigDialog.module.scss index 6e614c7..4ecb3d0 100644 --- a/src/components/ConfigDialog.module.scss +++ b/src/components/ConfigDialog.module.scss @@ -15,109 +15,146 @@ overflow-y: auto; color: rgb(255 255 255 / 87%); background-color: #242424; + z-index: 20 !important; + position: fixed; + left: 50%; + top: 0; + width: calc(100% - 32px); + max-width: 500px; + bottom: 0; + animation: ease-in-out show 0.3s both; + transition: 0.3s; + padding: 16px; + display: flex; + flex-flow: column nowrap !important; + gap: 8px; + + h2 { + text-align: center; + margin-top: -36px; + } + + @media screen and (min-width: 1024px) { + margin: 36px 0; + border-radius: 16px; + box-shadow: 0 19px 38px rgb(0 0 0 / 30%), 0 15px 12px rgb(0 0 0 / 22%); + } @media (prefers-color-scheme: light) { color: #213547; background-color: #fff; } - &Wrapper { - z-index: 10; - position: fixed; - left: 50%; - top: 0; - width: calc(100% - 32px); - max-width: 500px; - bottom: 0; - animation: ease-in-out show 0.3s both; - transition: 0.3s; - padding: 16px; - display: flex; - flex-flow: column nowrap !important; - - @media screen and (min-width: 1024px) { - margin: 36px 0; - border-radius: 16px; - box-shadow: 0 19px 38px rgb(0 0 0 / 30%), - 0 15px 12px rgb(0 0 0 / 22%); - } - } - - .error { + .errorTip { color: crimson; } - h4 { - margin: 8px 0; - } -} - -.add { - &Btn { - border-radius: 8px; - width: 50px; - height: 50px; - border: 1px solid gray; - overflow: hidden; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - - &Empty::before { - content: '+'; - color: #999; - font-size: 2em; - } - - img { - width: 100%; - height: 100%; - object-fit: cover; - } - } - - &Dialog { - position: absolute; - left: 50%; - top: 50%; - width: 90%; - padding: 12px; - transform: translateX(-50%) translateY(-60vh); - opacity: 0; - transition: 0.3s; - border-radius: 8px; - border: 2px solid #535bf2; - color: rgb(255 255 255 / 87%); - background-color: #242424; - - // 写冗余了,待优化 - @media (prefers-color-scheme: light) { - color: #213547; - background-color: #fff; - } - - &Show { - transform: translateX(-50%) translateY(-50%); - opacity: 1; - } - } -} - -.delete { - &Btn { - flex-grow: 1; + .closeBtn { + position: sticky; + background-color: rgb(0 0 0 / 30%); border-radius: 4px; + left: calc(100% - 36px); + top: 0; + width: 36px; + height: 36px; + min-height: 36px; display: flex; align-items: center; justify-content: center; - background-color: #f9f9f9; - font-size: 1.5em; - color: #999; + backdrop-filter: blur(5px); cursor: pointer; - span { - transform: rotate(45deg); + svg { + transition: 0.3s; + } + + &:hover { + svg { + transform: rotate(180deg); + } } } } + +.inputContainer { + word-break: keep-all; + + input, + select { + flex-grow: 1; + } + + input[type='file'] { + padding: 4px 12px; + } + + input[type='checkbox'] { + flex-grow: 0; + width: 24px; + height: 24px; + } + + input[type='color'] { + padding: 0 2px; + flex-grow: 0; + width: 30px; + height: 30px; + } + + .label { + min-width: 74px; + opacity: 0.7; + font-weight: 600; + } + + .tip { + font-size: 14px; + opacity: 0.6; + word-break: break-all; + } + + &.required .label { + &::after { + content: '*'; + color: red; + } + } +} + +.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; + } + + audio { + height: 30px; + width: 100px; + position: absolute; + left: 0; + z-index: 1; + } +} diff --git a/src/components/ConfigDialog.tsx b/src/components/ConfigDialog.tsx index 8ddb1d2..760e339 100644 --- a/src/components/ConfigDialog.tsx +++ b/src/components/ConfigDialog.tsx @@ -1,240 +1,87 @@ -import React, { FC, useEffect, useRef, useState } from 'react'; +import React, { FC, ReactNode, useEffect, useRef, useState } from 'react'; import style from './ConfigDialog.module.scss'; import classNames from 'classnames'; import { Icon, Sound, Theme } from '../themes/interface'; import { QRCodeCanvas } from 'qrcode.react'; import Bmob from 'hydrogen-js-sdk'; -import { captureElement, LAST_UPLOAD_TIME_STORAGE_KEY } from '../utils'; +import { + captureElement, + CUSTOM_THEME_STORAGE_KEY, + LAST_UPLOAD_TIME_STORAGE_KEY, + randomString, + wrapThemeDefaultSounds, +} from '../utils'; import { copy } from 'clipboard'; +import { CloseIcon } from './CloseIcon'; +import WxQrCode from './WxQrCode'; -const STORAGEKEY = 'customTheme'; +const InputContainer: FC<{ + label: string; + required?: boolean; + children: ReactNode; +}> = ({ label, children, required }) => { + return ( + <> +
+
+ {label} +
+ {children} +
+
+ + ); +}; const ConfigDialog: FC<{ closeMethod: () => void; previewMethod: (theme: Theme) => void; }> = ({ closeMethod, previewMethod }) => { - const [sounds, setSounds] = useState([]); - const [icons, setIcons] = useState([]); - const inputRefMap = useRef< - Record< - 'name' | 'link' | 'clickSound' | 'tripleSound' | string, - HTMLInputElement | HTMLSelectElement - > - >({}); + // 错误提示 const [configError, setConfigError] = useState(''); - const [customThemeInfo, setCustomThemeInfo] = useState<{ - title: string; - desc?: string; - bgm?: string; - background?: string; - backgroundBlur?: boolean; - }>({ title: '', desc: '', bgm: '', background: '', backgroundBlur: false }); - const [addDialog, setAddDialog] = useState<{ - show: boolean; - type: 'sound' | 'icon'; - iconForm?: Icon; - soundForm?: Sound; - error: string; - idx?: number; - }>({ - show: false, - type: 'sound', - error: '', - }); + // 生成链接 const [genLink, setGenLink] = useState(''); - const [pureCount, setPureCount] = useState(0); + const [customTheme, setCustomTheme] = useState>({ + title: '', + sounds: [], + icons: new Array(10).fill(0).map(() => ({ + name: randomString(4), + content: '', + clickSound: '', + tripleSound: '', + })), + }); + + // 编辑中音效 + const [editSound, setEditSound] = useState({ name: '', src: '' }); // 初始化 useEffect(() => { - let storageTheme: Theme | undefined = undefined; try { - const configString = localStorage.getItem(STORAGEKEY); + const configString = localStorage.getItem(CUSTOM_THEME_STORAGE_KEY); if (configString) { const parseRes = JSON.parse(configString); - if (typeof parseRes === 'object') storageTheme = parseRes; + if (typeof parseRes === 'object') { + setCustomTheme(parseRes); + } } } catch (e) { - // + console.log(e); } - if (!storageTheme) return; - const { - title, - desc = '', - bgm = '', - sounds, - icons, - background = '', - backgroundBlur = false, - } = storageTheme; - setSounds( - sounds.filter((s) => !['triple', 'button-click'].includes(s.name)) - ); - setIcons( - icons.map((icon) => { - if (icon.clickSound === 'button-click') icon.clickSound = ''; - if (icon.tripleSound === 'triple') icon.tripleSound = ''; - return icon; - }) - ); - setCustomThemeInfo({ - title, - // @ts-ignore - desc, - bgm, - background, - backgroundBlur, - }); }, []); - // 音效保存 - const saveSound = (sound: Sound, idx?: number) => { - if (!sound.src.startsWith('https')) return '请输入https链接'; - const newSounds = sounds.slice(); - const newIcons = icons.slice(); - if (idx != null) { - // 编辑 - for (let i = 0; i < sounds.length; i++) { - if (sounds[i].name === sound.name && i !== idx) { - return '名称已存在'; - } - } - // 检查编辑的音效是否有引用并修改 - const oldSoundName = sounds[idx].name; - for (const icon of newIcons) { - if (icon.clickSound === oldSoundName) - icon.clickSound = sound.name; - if (icon.tripleSound === oldSoundName) - icon.tripleSound = sound.name; - } - newSounds[idx] = sound; - } else { - // 新增 - if (sounds.find((s) => s.name === sound.name)) return '名称已存在'; - newSounds.push(sound); - } - setIcons(newIcons); - setSounds(newSounds); - }; - const onSoundClick = (idx?: number) => { - if (addDialog.show) return; - setAddDialog({ - idx, - show: true, - type: 'sound', - soundForm: { - name: '', - src: '', - }, - error: '', - }); - }; - - // 图片保存 - const saveIcon = (icon: Icon, idx?: number) => { - if ( - typeof icon.content !== 'string' || - !icon.content?.startsWith('https') - ) - return '请输入https链接'; - const newIcons = icons.slice(); - if (idx != null) { - // 编辑 - for (let i = 0; i < icons.length; i++) { - if (icons[i].name === icon.name && i !== idx) { - return '名称已存在'; - } - } - newIcons[idx] = icon; - } else { - // 新增 - if (icons.find((i) => i.name === icon.name)) return '名称已存在'; - newIcons.push(icon); - } - setIcons(newIcons); - }; - const onIconClick = (idx?: number) => { - if (addDialog.show) return; - setAddDialog({ - idx, - show: true, - type: 'icon', - iconForm: - idx != null - ? { ...icons[idx] } - : { - name: '', - content: '', - tripleSound: '', - clickSound: '', - }, - error: '', - }); - }; - - // 回显 - useEffect(() => { - const { show, type, idx } = addDialog; - if (show) return; - if (!inputRefMap.current) return; - if (type === 'icon') { - inputRefMap.current.name.value = idx != null ? icons[idx].name : ''; - inputRefMap.current.link.value = - idx != null ? (icons[idx].content as string) : ''; - inputRefMap.current.clickSound.value = - idx != null ? icons[idx]?.clickSound || '' : ''; - inputRefMap.current.tripleSound.value = - idx != null ? icons[idx]?.tripleSound || '' : ''; - } else { - inputRefMap.current.name.value = - idx != null ? sounds[idx].name : ''; - inputRefMap.current.link.value = idx != null ? sounds[idx].src : ''; - } - }, [addDialog]); - - // 添加单项的点击 - const onAddDialogSaveClick = () => { - const error = (addDialog.type === 'sound' ? saveSound : saveIcon)( - addDialog[`${addDialog.type}Form`] as any, - addDialog.idx - ); - if (error) { - setAddDialog({ ...addDialog, error }); - } else { - closeAddDialog(); - } - }; - - // 关闭添加弹窗 - const closeAddDialog = () => { - setAddDialog({ ...addDialog, show: false }); - }; - // 生成主题 const generateTheme: () => Promise> = async () => { - const { title, desc, bgm, background, backgroundBlur } = - customThemeInfo; - if (bgm && !bgm.startsWith('https')) - return Promise.reject('背景音乐请输入https链接'); - if (background && !background.startsWith('https')) - return Promise.reject('背景图片请输入https链接'); - if (!title) return Promise.reject('请填写标题'); - if (icons.length !== 10) return Promise.reject('图片素材需要提供10张'); - - const customTheme: Theme = { - // 恭喜你发现纯净模式彩蛋🎉,点击文字十次可以开启纯净模式 - pure: pureCount !== 0 && pureCount % 10 === 0, - title, - desc, - bgm, - background, - backgroundBlur, - icons, - sounds, - }; - - console.log(customTheme); - - return Promise.resolve(JSON.parse(JSON.stringify(customTheme))); + // TODO 校验 + const cloneTheme = JSON.parse(JSON.stringify(customTheme)); + wrapThemeDefaultSounds(cloneTheme); + return Promise.resolve(cloneTheme); }; // 预览 @@ -243,7 +90,10 @@ const ConfigDialog: FC<{ generateTheme() .then((theme) => { previewMethod(theme); - localStorage.setItem(STORAGEKEY, JSON.stringify(theme)); + localStorage.setItem( + CUSTOM_THEME_STORAGE_KEY, + JSON.stringify(theme) + ); closeMethod(); }) .catch((e) => { @@ -276,7 +126,7 @@ const ConfigDialog: FC<{ } const stringify = JSON.stringify(theme); - localStorage.setItem(STORAGEKEY, stringify); + localStorage.setItem(CUSTOM_THEME_STORAGE_KEY, stringify); const query = Bmob.Query('config'); query.set('content', stringify); query @@ -305,338 +155,101 @@ const ConfigDialog: FC<{ }); }; - // 删除按钮 - const DeleteBtn: FC<{ idx: number; type: 'sound' | 'icon' }> = ({ - idx, - type, - }) => { - const deleteItem = () => { - if (type === 'sound') { - const newSounds = sounds.slice(); - newSounds.splice(idx, 1); - setSounds(newSounds); - } else { - const newIcons = icons.slice(); - newIcons.splice(idx, 1); - setIcons(newIcons); - } - }; - return ( -
- + -
- ); - }; - // TODO HTML有点臭长了,待优化 // @ts-ignore return ( -
-

setPureCount(pureCount + 1)}> - 目前自定义仅支持配置https链接,可网上自行搜索素材复制链接,或者将自己处理好的素材上传第三方存储服务/图床上再复制外链 - (想白嫖的话自行搜索【免费图床】【免费对象存储】【免费mp3外链】等)。 - {pureCount != 0 && - pureCount % 10 === 0 && - '🎉🎉🎉恭喜发现彩蛋!主题分享后将开启纯净模式~'} -

-
- -

- - 开发不易,如果您喜欢这个项目的话可酌情扫左侧二维码 - 请我喝杯咖啡(后台相关费用用爱发电中,感谢支持) - -

+
+
+
+

自定义主题

- {/*基本配置*/} -

- 标题: - - setCustomThemeInfo({ - ...customThemeInfo, - title: e.target.value, - }) - } - /> -

-

- 描述: - - setCustomThemeInfo({ - ...customThemeInfo, - desc: e.target.value, - }) - } - /> -

-

- 背景音乐: - - setCustomThemeInfo({ - ...customThemeInfo, - bgm: e.target.value, - }) - } - /> -

-

- 背景图片: - - setCustomThemeInfo({ - ...customThemeInfo, - background: e.target.value, - }) - } - /> - {customThemeInfo?.background?.startsWith('https') && ( - <> - 毛玻璃: - - setCustomThemeInfo({ - ...customThemeInfo, - backgroundBlur: e.target.checked, - }) - } - type="checkbox" - /> - - )} -

- -

音效素材

-
- {sounds.map((sound, idx) => ( -
-
onSoundClick(idx)} - className={classNames(style.addBtn)} - > - {sound.name} -
- -
- ))} - {sounds.length < 20 && ( -
onSoundClick()} - className={classNames(style.addBtn, style.addBtnEmpty)} - /> - )} -
-

图片素材 {icons.length}/10

-
- {icons.map((icon, idx) => ( -
-
onIconClick(idx)} - className={classNames(style.addBtn)} - > - {/* @ts-ignore*/} - -
- -
- ))} - {icons.length < 10 && ( -
onIconClick()} - className={classNames(style.addBtn, style.addBtnEmpty)} - /> - )} -
- -
- {genLink && ( -
- - -
{genLink}
- + + + + + + + + + + + + + +
+ 毛玻璃 + +
+ 深色 + +
+ 纯色 +
- )} - {configError &&
{configError}
} -
- 由于访问量过大,生成二维码码和链接功能暂时关闭(个人项目经费有限且本项目没有接任何广告,感谢理解😭,但仍然可以本地整活,之前生成的二维码和链接依然有效), - 本项目开源,有需求可以自行fork进行部署: - + 使用图片或者纯色作为背景,图片可开启毛玻璃效果。如果你使用了深色的图片和颜色,请开启深色模式,此时标题等文字将变为亮色 +
+ + + + + +
+ {customTheme.sounds.map((sound, idx) => { + return ( +
+
+ ); + })} +
+ + setEditSound({ + name: event.target.value, + src: editSound.src, + }) + } + /> + + + setEditSound({ + src: event.target.value, + name: editSound.name, + }) + } + /> +
-
- - {/**/} - -
- - {/*添加弹窗*/} -
-
- 名称: - ref && (inputRefMap.current.name = ref)} - className="flex-grow" - placeholder="唯一名称" - onChange={(e) => - setAddDialog({ - ...addDialog, - [`${addDialog.type}Form`]: { - ...addDialog[`${addDialog.type}Form`], - name: e.target.value, - }, - }) - } - /> + + +
+ {customTheme.icons.map((icon, idx) => { + return
{icon.name}
; + })}
-
- 链接: - ref && (inputRefMap.current.link = ref)} - className="flex-grow" - placeholder="https://example.com/src.audioOrImage" - onChange={(e) => - setAddDialog({ - ...addDialog, - [`${addDialog.type}Form`]: { - ...addDialog[`${addDialog.type}Form`], - [addDialog.type === 'sound' - ? 'src' - : 'content']: e.target.value, - }, - }) - } - /> -
- {addDialog.type === 'icon' && ( - <> -
- 点击音效: - -
-
- 三连音效: - -
- - )} - {addDialog.error && ( -
{addDialog.error}
- )} -
- - -
-
+ + ?? +
); }; diff --git a/src/components/FixedAnimateScalePanel.module.scss b/src/components/FixedAnimateScalePanel.module.scss index 3163363..0f65a0d 100644 --- a/src/components/FixedAnimateScalePanel.module.scss +++ b/src/components/FixedAnimateScalePanel.module.scss @@ -37,6 +37,16 @@ color: white; cursor: pointer; user-select: none; + + svg { + transition: 0.3s; + } + + &:hover { + svg { + transform: rotate(180deg); + } + } } &.open { diff --git a/src/styles/global.scss b/src/styles/global.scss index 8f5ad22..ffa9e44 100644 --- a/src/styles/global.scss +++ b/src/styles/global.scss @@ -30,16 +30,13 @@ button { } } +input, select { - border: 1px solid gray; - border-radius: 4px; - padding: 4px 8px; -} - -input { - border: 1px solid gray; + border: 1px dashed rgb(0 0 0 / 30%); border-radius: 4px; padding: 8px 12px; + + &::placeholder { + color: rgb(0 0 0 / 30%); + } } - - diff --git a/src/styles/utils.scss b/src/styles/utils.scss index f204e4b..54cdc75 100644 --- a/src/styles/utils.scss +++ b/src/styles/utils.scss @@ -13,6 +13,11 @@ align-items: center; } +.flex-left-center { + justify-content: flex-start; + align-items: center; +} + .flex-row { flex-direction: row; }