diff --git a/src/renderer/src/assets/css/components/tools/local-card.scss b/src/renderer/src/assets/css/components/tools/local-card.scss new file mode 100644 index 0000000..5170b49 --- /dev/null +++ b/src/renderer/src/assets/css/components/tools/local-card.scss @@ -0,0 +1,110 @@ +@use '@/assets/css/constants' as constants; + +[data-component=component-local-card] { + height: 100%; + cursor: pointer; + + .local-card { + width: 100%; + height: 100%; + text-align: center; + align-items: center; + + > * { + display: block; + flex: 0 0 auto; + } + + .header { + display: flex; + width: 100%; + padding: 10px; + justify-content: space-between; + + .version { + width: 0; + transition: all 0.2s; + } + + .operation { + display: flex; + font-size: 1.6em; + gap: 4px; + opacity: 0; + transition: all 0.2s; + + > *:hover { + color: constants.$font-secondary-color; + } + } + } + + .icon { + display: flex; + padding-top: 10px; + padding-bottom: 20px; + color: constants.$production-color; + font-size: constants.$SIZE_ICON_XL; + justify-content: center; + + img { + width: constants.$SIZE_ICON_XL; + } + } + + .info { + padding-top: 20px; + + .tool-name { + font-weight: bolder; + font-size: 1.6em; + } + + .tool-desc { + margin: { + top: 10px; + left: auto; + right: auto; + }; + color: constants.$font-secondary-color; + overflow: hidden; + text-overflow: ellipsis; + max-height: 40px; + width: 80%; + } + } + + .author { + display: flex; + margin-top: auto; + flex-direction: row; + justify-content: end; + padding-bottom: 10px; + gap: 10px; + + .avatar { + > * { + width: 24px; + height: 24px; + } + } + + .author-name { + display: flex; + align-items: center; + } + } + } + + :hover { + .header { + .version { + opacity: 0; + } + + .operation { + opacity: 1; + } + } + } +} diff --git a/src/renderer/src/assets/css/pages/tools/local.scss b/src/renderer/src/assets/css/pages/tools/local.scss new file mode 100644 index 0000000..c7b9cf7 --- /dev/null +++ b/src/renderer/src/assets/css/pages/tools/local.scss @@ -0,0 +1,49 @@ +@use '@/assets/css/constants' as constants; + +[data-component=tools-local] { + .search { + display: flex; + position: sticky; + width: 100%; + margin-top: 20px; + top: 20px; + z-index: 10; + justify-content: center; + transition: all 0.3s ease; + + > * { + width: 80%; + } + + &.hide { + transform: translateY(-60px); + } + } + + .root-content { + padding: 20px; + gap: 20px; + flex-wrap: wrap; + justify-content: center; + + > div { + width: 180px; + height: 290px; + flex: 0 0 auto; + } + + .no-tool { + display: flex; + justify-content: center; + font-size: 1.4em; + font-weight: bolder; + color: constants.$font-secondary-color; + } + } + + .android-qrcode { + align-items: center; + transform: translateX(-16px); + gap: 20px; + } +} diff --git a/src/renderer/src/assets/svg/download.svg b/src/renderer/src/assets/svg/download.svg new file mode 100644 index 0000000..4f47468 --- /dev/null +++ b/src/renderer/src/assets/svg/download.svg @@ -0,0 +1 @@ + diff --git a/src/renderer/src/assets/svg/installed.svg b/src/renderer/src/assets/svg/installed.svg new file mode 100644 index 0000000..fcf64c5 --- /dev/null +++ b/src/renderer/src/assets/svg/installed.svg @@ -0,0 +1 @@ + diff --git a/src/renderer/src/components/tools/LocalCard.tsx b/src/renderer/src/components/tools/LocalCard.tsx new file mode 100644 index 0000000..53642cf --- /dev/null +++ b/src/renderer/src/components/tools/LocalCard.tsx @@ -0,0 +1,206 @@ +import { DetailedHTMLProps, HTMLAttributes, MouseEvent } from 'react' +import VanillaTilt, { TiltOptions } from 'vanilla-tilt' +import Icon from '@ant-design/icons' +import '@/assets/css/components/tools/local-card.scss' +import { COLOR_BACKGROUND, COLOR_MAIN } from '@/constants/common.constants' +import { checkDesktop, omitText } from '@/util/common' +import { navigateToStore, navigateToView } from '@/util/navigation' +import Card from '@/components/common/Card' +import FlexBox from '@/components/common/FlexBox' +import DragHandle from '@/components/dnd/DragHandle' +import Draggable from '@/components/dnd/Draggable' + +interface StoreCardProps extends DetailedHTMLProps, HTMLDivElement> { + icon: string + toolName: string + toolId: string + toolDesc: string + options?: TiltOptions + author: UserWithInfoVo + showAuthor?: boolean + ver: string + platform: Platform + supportPlatform: Platform[] + favorite: boolean +} + +const StoreCard = ({ + style, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ref, + icon, + toolName, + toolId, + toolDesc, + options = { + reverse: true, + max: 8, + glare: true, + ['max-glare']: 0.3, + scale: 1.03 + }, + author, + showAuthor = true, + ver, + platform, + supportPlatform, + favorite, + ...props +}: StoreCardProps) => { + const navigate = useNavigate() + const [modal, contextHolder] = AntdModal.useModal() + const cardRef = useRef(null) + + useEffect(() => { + cardRef.current && VanillaTilt.init(cardRef.current, options) + }, [options]) + + const handleCardOnClick = () => { + if (!checkDesktop() && platform === 'DESKTOP') { + void message.warning('此应用需要桌面端环境,请在桌面端打开') + return + } + if (platform === 'ANDROID') { + void modal.confirm({ + centered: true, + icon: , + title: 'Android 端', + content: ( + + + 请使用手机端扫描上方二维码 + + ), + okText: '确定', + cancelText: '模拟器', + onCancel() { + navigateToView(navigate, author.username, toolId, platform, undefined, true) + } + }) + return + } + navigateToView(navigate, author.username, toolId, platform, undefined, true) + } + + const handleOnClickAuthor = (e: MouseEvent) => { + e.stopPropagation() + navigateToStore(navigate, author.username) + } + + const handleOnAndroidBtnClick = (e: MouseEvent) => { + e.stopPropagation() + void modal.confirm({ + centered: true, + icon: , + title: 'Android 端', + content: ( + + + 请使用手机端扫描上方二维码 + + ), + okText: '确定', + cancelText: '模拟器', + onCancel() { + navigateToView(navigate, author.username, toolId, 'ANDROID', undefined, true) + } + }) + } + + const handleOnWebBtnClick = (e: MouseEvent) => { + e.stopPropagation() + navigateToView(navigate, author.username, toolId, 'WEB', undefined, true) + } + + return ( + <> + + + +
+
+ + {platform.slice(0, 1)}-{ver} + +
+
+ {platform !== 'ANDROID' && supportPlatform.includes('ANDROID') && ( + + + + )} + {platform === 'DESKTOP' && supportPlatform.includes('WEB') && ( + + + + )} + +
+
+
+ {'Icon'} +
+
+
{toolName}
+
{`ID: ${toolId}`}
+ {toolDesc && ( +
{`简介:${omitText(toolDesc, 18)}`}
+ )} +
+ {showAuthor && ( +
+
+ + } + style={{ background: COLOR_BACKGROUND }} + /> +
+
{author.userInfo.nickname}
+
+ )} +
+
+
+ {contextHolder} + + ) +} + +export default StoreCard diff --git a/src/renderer/src/components/tools/StoreCard.tsx b/src/renderer/src/components/tools/StoreCard.tsx index e2123f4..1046600 100644 --- a/src/renderer/src/components/tools/StoreCard.tsx +++ b/src/renderer/src/components/tools/StoreCard.tsx @@ -3,7 +3,12 @@ import VanillaTilt, { TiltOptions } from 'vanilla-tilt' import protocolCheck from 'custom-protocol-check' import Icon from '@ant-design/icons' import '@/assets/css/components/tools/store-card.scss' -import { COLOR_BACKGROUND, COLOR_MAIN, COLOR_PRODUCTION } from '@/constants/common.constants' +import { + COLOR_BACKGROUND, + COLOR_MAIN, + COLOR_PRODUCTION, + DATABASE_SELECT_SUCCESS +} from '@/constants/common.constants' import { checkDesktop, omitText } from '@/util/common' import { getLoginStatus, getUserId } from '@/util/auth' import { @@ -12,7 +17,13 @@ import { navigateToStore, navigateToView } from '@/util/navigation' -import { r_tool_add_favorite, r_tool_remove_favorite } from '@/services/tool' +import { + l_tool_get, + l_tool_install, + r_tool_add_favorite, + r_tool_detail, + r_tool_remove_favorite +} from '@/services/tool' import Card from '@/components/common/Card' import FlexBox from '@/components/common/FlexBox' import DragHandle from '@/components/dnd/DragHandle' @@ -29,6 +40,7 @@ interface StoreCardProps extends DetailedHTMLProps favorite: boolean } @@ -52,6 +64,7 @@ const StoreCard = ({ ver, platform, supportPlatform, + vers, favorite, ...props }: StoreCardProps) => { @@ -60,6 +73,9 @@ const StoreCard = ({ const cardRef = useRef(null) const [favorite_, setFavorite_] = useState(favorite) const [userId, setUserId] = useState('') + const [isInstalling, setIsInstalling] = useState(false) + const [isInstalled, setIsInstalled] = useState(true) + const [isAvailableUpdate, setIsAvailableUpdate] = useState(false) useEffect(() => { cardRef.current && VanillaTilt.init(cardRef.current, options) @@ -143,6 +159,59 @@ const StoreCard = ({ } } + const handleOnInstallBtnClick = (e: MouseEvent) => { + e.stopPropagation() + if (isInstalling) { + return + } + setIsInstalling(true) + + void message.loading({ + content: isAvailableUpdate ? '更新中' : '安装中', + key: 'INSTALLING', + duration: 0 + }) + const newTools = {} as Record + const flags: boolean[] = [] + supportPlatform.forEach((platform) => { + void r_tool_detail(author.username, toolId, 'latest', platform) + .then((res) => { + const response = res.data + switch (response.code) { + case DATABASE_SELECT_SUCCESS: + newTools[platform] = response.data! + flags.push(true) + break + default: + flags.push(false) + } + }) + .catch(() => { + flags.push(false) + }) + .finally(() => { + message.destroy('INSTALLING') + setIsInstalling(false) + if (flags.length !== supportPlatform.length) { + return + } + if (flags.every((item) => item)) { + void l_tool_install({ [`${author.username}:${toolId}`]: newTools }).then( + () => { + void message.success(isAvailableUpdate ? '更新成功' : '安装成功') + setIsInstalled(true) + setIsAvailableUpdate(false) + } + ) + } else { + void message.error( + isAvailableUpdate ? '更新失败,请稍后重试' : '安装失败,请稍后重试' + ) + } + }) + }) + } + const handleOnAndroidBtnClick = (e: MouseEvent) => { e.stopPropagation() void modal.confirm({ @@ -195,6 +264,30 @@ const StoreCard = ({ navigateToView(navigate, author.username, toolId, 'WEB') } + useEffect(() => { + void l_tool_get().then((value) => { + const tools = value[`${author.username}:${toolId}`] + if (!tools) { + setIsInstalled(false) + return + } + setIsInstalled(true) + + if ( + Object.keys(tools).length !== supportPlatform.length || + !supportPlatform.every((platform) => Object.keys(tools).includes(platform)) + ) { + setIsAvailableUpdate(true) + return + } + + if (supportPlatform.some((platform) => vers[platform] !== tools[platform].ver)) { + setIsAvailableUpdate(true) + return + } + }) + }, []) + return ( <>
+ {(!isInstalled || isAvailableUpdate) && ( + + + + )} {platform !== 'ANDROID' && supportPlatform.includes('ANDROID') && ( > ) => Promise>> + + getInstalledTool: () => Promise>> } } diff --git a/src/renderer/src/pages/Tools/Local.tsx b/src/renderer/src/pages/Tools/Local.tsx new file mode 100644 index 0000000..3e73af7 --- /dev/null +++ b/src/renderer/src/pages/Tools/Local.tsx @@ -0,0 +1,138 @@ +import { UIEvent } from 'react' +import '@/assets/css/pages/tools/local.scss' +import { checkDesktop } from '@/util/common' +import { l_tool_get } from '@/services/tool' +import FlexBox from '@/components/common/FlexBox' +import FitFullscreen from '@/components/common/FitFullscreen' +import HideScrollbar from '@/components/common/HideScrollbar' +import LocalCard from '@/components/tools/LocalCard' + +const Local = () => { + const scrollTopRef = useRef(0) + const [isLoading, setIsLoading] = useState(false) + const [toolData, setToolData] = useState([]) + const [isHideSearch, setIsHideSearch] = useState(false) + + const handleOnSearch = (value: string) => { + getTool(value) + } + + const handleOnScroll = (event: UIEvent) => { + if (event.currentTarget.scrollTop < scrollTopRef.current) { + setIsHideSearch(false) + } else { + setIsHideSearch(true) + } + scrollTopRef.current = event.currentTarget.scrollTop + } + + const getTool = (searchValue: string) => { + if (isLoading) { + return + } + setIsLoading(true) + void message.loading({ content: '加载工具列表中', key: 'LOADING', duration: 0 }) + + void l_tool_get() + .then((data) => { + const list: ToolVo[] = [] + Object.values(data).forEach((value) => + Object.values(value).forEach((item) => list.push(item)) + ) + setToolData( + list.filter( + ({ name, keywords }) => + name.toLowerCase().includes(searchValue.toLowerCase()) || + keywords.some((value) => + value.toLowerCase().includes(searchValue.toLowerCase()) + ) + ) + ) + }) + .catch((e) => { + void message.error('加载失败,请稍后重试') + console.error('Get installed tools error: ', e) + }) + .finally(() => { + setIsLoading(false) + message.destroy('LOADING') + }) + } + + useEffect(() => { + getTool('') + }, []) + + return ( + <> + + +
+ +
+ + {!toolData.length &&
未找到任何工具
} + {toolData + ?.reduce((previousValue: ToolVo[], currentValue) => { + if ( + !previousValue.some( + (value) => + value.author.id === currentValue.author.id && + value.toolId === currentValue.toolId + ) + ) { + previousValue.push(currentValue) + } + return previousValue + }, []) + .map((item) => { + const tools = toolData.filter( + (value) => + value.author.id === item.author.id && + value.toolId === item.toolId + ) + const webTool = tools.find((value) => value.platform === 'WEB') + const desktopTool = tools.find( + (value) => value.platform === 'DESKTOP' + ) + const androidTool = tools.find( + (value) => value.platform === 'ANDROID' + ) + const firstTool = + (checkDesktop() + ? desktopTool || webTool + : webTool || desktopTool) || androidTool + + return ( + value.platform)} + favorite={firstTool!.favorite} + /> + ) + })} +
+
+
+ + ) +} + +export default Local diff --git a/src/renderer/src/pages/Tools/Store.tsx b/src/renderer/src/pages/Tools/Store.tsx index 27f449c..aee62d4 100644 --- a/src/renderer/src/pages/Tools/Store.tsx +++ b/src/renderer/src/pages/Tools/Store.tsx @@ -142,6 +142,13 @@ const Store = () => { ver={firstTool!.ver} platform={firstTool!.platform} supportPlatform={tools.map((value) => value.platform)} + vers={tools.reduce( + (previousValue, currentValue) => ({ + ...previousValue, + [currentValue.platform]: currentValue.ver + }), + {} as Record + )} favorite={firstTool!.favorite} /> ) diff --git a/src/renderer/src/pages/Tools/User.tsx b/src/renderer/src/pages/Tools/User.tsx index 4fb4cd1..5812528 100644 --- a/src/renderer/src/pages/Tools/User.tsx +++ b/src/renderer/src/pages/Tools/User.tsx @@ -206,6 +206,13 @@ const User = () => { ver={firstTool!.ver} platform={firstTool!.platform} supportPlatform={tools.map((value) => value.platform)} + vers={tools.reduce( + (previousValue, currentValue) => ({ + ...previousValue, + [currentValue.platform]: currentValue.ver + }), + {} as Record + )} favorite={firstTool!.favorite} /> ) diff --git a/src/renderer/src/pages/Tools/View.tsx b/src/renderer/src/pages/Tools/View.tsx index 327394f..0a805a1 100644 --- a/src/renderer/src/pages/Tools/View.tsx +++ b/src/renderer/src/pages/Tools/View.tsx @@ -1,8 +1,13 @@ import '@/assets/css/pages/tools/view.scss' import { DATABASE_NO_RECORD_FOUND, DATABASE_SELECT_SUCCESS } from '@/constants/common.constants' import { getLoginStatus } from '@/util/auth' -import { navigateToRepository, navigateToRoot, navigateToView } from '@/util/navigation' -import { r_tool_detail } from '@/services/tool' +import { + navigateToInstall, + navigateToRepository, + navigateToRoot, + navigateToView +} from '@/util/navigation' +import { l_tool_detail, r_tool_detail } from '@/services/tool' import compiler from '@/components/Playground/compiler' import { IImportMap } from '@/components/Playground/shared' import { base64ToFiles, base64ToStr, IMPORT_MAP_FILE_NAME } from '@/components/Playground/files' @@ -64,6 +69,24 @@ const View = () => { setIsLoading(true) void message.loading({ content: '加载中……', key: 'LOADING', duration: 0 }) + if (searchParams.get('source') === 'local') { + void l_tool_detail(username!, toolId!, searchParams.get('platform') as Platform) + .then((tool) => { + if (!tool) { + void message.error('未找到指定工具') + navigateToInstall(navigate) + return + } + render(tool) + }) + .finally(() => { + setIsLoading(false) + message.destroy('LOADING') + }) + + return + } + void r_tool_detail( username!, toolId!, diff --git a/src/renderer/src/pages/Tools/index.tsx b/src/renderer/src/pages/Tools/index.tsx index a7bfe6a..fe784d2 100644 --- a/src/renderer/src/pages/Tools/index.tsx +++ b/src/renderer/src/pages/Tools/index.tsx @@ -623,6 +623,14 @@ const Tools = () => { supportPlatform={tools.map( (value) => value.platform )} + vers={tools.reduce( + (previousValue, currentValue) => ({ + ...previousValue, + [currentValue.platform]: + currentValue.ver + }), + {} as Record + )} favorite={firstTool!.favorite} /> ) diff --git a/src/renderer/src/pages/ToolsFramework.tsx b/src/renderer/src/pages/ToolsFramework.tsx index 839caa5..66e783a 100644 --- a/src/renderer/src/pages/ToolsFramework.tsx +++ b/src/renderer/src/pages/ToolsFramework.tsx @@ -108,6 +108,12 @@ const ToolsFramework = () => { icon={tools[1].icon} text={tools[1].name} /> + @@ -145,7 +151,8 @@ const ToolsFramework = () => { authorUsername, toolId, platform, - ver + ver === 'local' ? '' : ver, + ver === 'local' )} icon={icon} text={toolName} diff --git a/src/renderer/src/router/tools.tsx b/src/renderer/src/router/tools.tsx index ca18ede..e67d7da 100644 --- a/src/renderer/src/router/tools.tsx +++ b/src/renderer/src/router/tools.tsx @@ -19,6 +19,16 @@ export const tools: RouteJsonObject[] = [ icon: lazy(() => import('~icons/oxygen/home')), menu: true }, + { + path: 'install', + absolutePath: '/install', + id: 'tools-install', + component: lazy(() => import('@/pages/Tools/Local')), + name: '本地安装', + titlePostfix: ' - 本地', + icon: lazy(() => import('~icons/oxygen/installed')), + menu: true + }, { path: '', absolutePath: '/', diff --git a/src/renderer/src/services/tool.ts b/src/renderer/src/services/tool.ts index 2a31952..a6ecad9 100644 --- a/src/renderer/src/services/tool.ts +++ b/src/renderer/src/services/tool.ts @@ -47,3 +47,14 @@ export const r_tool_remove_favorite = (param: ToolFavoriteAddRemoveParam) => export const r_tool_get_favorite = (param: PageParam) => request.get>(URL_TOOL_FAVORITE, param) + +export const l_tool_get = () => window.api.getInstalledTool() + +export const l_tool_install = (tools: Record>) => + window.api.installTool(tools) + +export const l_tool_detail = async ( + username: string, + toolId: string, + platform: Platform +): Promise => (await l_tool_get())?.[`${username}:${toolId}`]?.[platform] diff --git a/src/renderer/src/util/navigation.ts b/src/renderer/src/util/navigation.ts index fceb2fe..8b667e8 100644 --- a/src/renderer/src/util/navigation.ts +++ b/src/renderer/src/util/navigation.ts @@ -17,6 +17,10 @@ export const navigateToRepository = (navigate: NavigateFunction, options?: Navig navigate('/repository', options) } +export const navigateToInstall = (navigate: NavigateFunction, options?: NavigateOptions) => { + navigate('/install', options) +} + export const navigateToLogin = ( navigate: NavigateFunction, locationSearch?: string, @@ -35,12 +39,19 @@ export const navigateToView = ( toolId: string, platform: Platform, version?: string, + local?: boolean, options?: NavigateOptions ) => { - navigate( - `/view/${username}/${toolId}${version ? `/${version}` : ''}${platform !== import.meta.env.VITE_PLATFORM ? `?platform=${platform}` : ''}`, - options + const url = new URL( + `/view/${username}/${toolId}${version ? `/${version}` : ''}`, + window.location.href ) + if (platform !== import.meta.env.VITE_PLATFORM) { + url.searchParams.append('platform', platform) + } + local && url.searchParams.append('source', 'local') + + navigate(`${url.pathname}${url.search}`, options) } export const navigateToSource = ( @@ -51,10 +62,14 @@ export const navigateToSource = ( version?: string, options?: NavigateOptions ) => { - navigate( - `/source/${username}/${toolId}${version ? `/${version}` : ''}${platform !== import.meta.env.VITE_PLATFORM ? `?platform=${platform}` : ''}`, - options + const url = new URL( + `/source/${username}/${toolId}${version ? `/${version}` : ''}`, + window.location.href ) + if (platform !== import.meta.env.VITE_PLATFORM) { + url.searchParams.append('platform', platform) + } + navigate(`${url.pathname}${url.search}`, options) } export const navigateToRedirect = ( @@ -104,10 +119,12 @@ export const navigateToEdit = ( platform: Platform, options?: NavigateOptions ) => { - navigate( - `/edit/${toolId}${platform !== import.meta.env.VITE_PLATFORM ? `?platform=${platform}` : ''}`, - options - ) + const url = new URL(`/edit/${toolId}`, window.location.href) + if (platform !== import.meta.env.VITE_PLATFORM) { + url.searchParams.append('platform', platform) + } + + navigate(`${url.pathname}${url.search}`, options) } export const navigateToUser = (navigate: NavigateFunction, options?: NavigateOptions) => { @@ -122,6 +139,17 @@ export const getViewPath = ( username: string, toolId: string, platform: Platform, - version?: string -) => - `/view/${username}/${toolId}${version ? `/${version}` : ''}${platform !== import.meta.env.VITE_PLATFORM ? `?platform=${platform}` : ''}` + version?: string, + local?: boolean +) => { + const url = new URL( + `/view/${username}/${toolId}${version ? `/${version}` : ''}`, + window.location.href + ) + if (platform !== import.meta.env.VITE_PLATFORM) { + url.searchParams.append('platform', platform) + } + local && url.searchParams.append('source', 'local') + + return `${url.pathname}${url.search}` +}