refactor(components)
BREAKING CHANGES - Refactored `App.tsx` to import `Component` from `solid-js`, and use a new component `TitleBar`. - Added new component `AnimateView` under `src/components/design`, and renamed old `AnimateView` to `Loader`. - Added new component `Button` under `src/components/design`. - Added new component `Image` under `src/components/design`. - Moved old `Image`, `Loader`, and `Button` components under `src/components/design`. - Refactored `Downloader` component to use `Motion` from `@motionone/solid`, moved it under `Downloader` folder, and used it to create a slick hover effect. - Removed `Downloaders/Button`. Notes: - Used `createSignal` instead of `useState` for SolidJS in `Downloaders.tsx`. - Used `type` keyword to explicitly define the type of the component props or objects where it makes the code clearer.
This commit is contained in:
parent
6ba06e09d2
commit
d26b429ee8
19
src/App.tsx
19
src/App.tsx
@ -1,13 +1,18 @@
|
||||
import { Window } from './components/system'
|
||||
import { Downloaders } from './components/layout'
|
||||
import type { Component } from 'solid-js'
|
||||
|
||||
const Main: React.FC = () => {
|
||||
import { Downloaders, TitleBar } from './components/layout'
|
||||
|
||||
const Main: Component = () => {
|
||||
return (
|
||||
<Window>
|
||||
<span className='mb-20 text-white'>I would like to:</span>
|
||||
<>
|
||||
<TitleBar />
|
||||
|
||||
<Downloaders />
|
||||
</Window>
|
||||
<main class='relative flex h-full w-screen flex-col items-center justify-center bg-[#242424] py-20'>
|
||||
<span class='mb-20 text-white'>I would like to:</span>
|
||||
|
||||
<Downloaders />
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
22
src/components/design/AnimateView/AnimateView.tsx
Normal file
22
src/components/design/AnimateView/AnimateView.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { Show } from 'solid-js'
|
||||
import type { Component } from 'solid-js'
|
||||
import type { MotionComponentProps, Options as MotionProps } from '@motionone/solid'
|
||||
import { Motion, Presence } from '@motionone/solid'
|
||||
|
||||
export interface AnimateViewProps extends MotionComponentProps {
|
||||
condition: boolean
|
||||
animation: MotionProps
|
||||
class?: string
|
||||
}
|
||||
|
||||
export const AnimateView: Component<AnimateViewProps> = (props) => {
|
||||
return (
|
||||
<Presence>
|
||||
<Show when={props.condition}>
|
||||
<Motion.div {...props} initial={props.initial} animate={props.animate} exit={props.exit}>
|
||||
{props.children}
|
||||
</Motion.div>
|
||||
</Show>
|
||||
</Presence>
|
||||
)
|
||||
}
|
18
src/components/design/Button/Button.tsx
Normal file
18
src/components/design/Button/Button.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import classNames from 'classnames'
|
||||
import type { Component, ComponentProps, JSXElement } from 'solid-js'
|
||||
|
||||
interface ButtonProps extends ComponentProps<'button'> {
|
||||
icon: JSXElement
|
||||
value: string
|
||||
class?: string
|
||||
handler?: () => void
|
||||
}
|
||||
|
||||
export const Button: Component<ButtonProps> = (props) => {
|
||||
return (
|
||||
<button onClick={props.handler} class={classNames(props.class, 'flex items-center justify-center gap-x-5')}>
|
||||
{props.icon}
|
||||
<span class='uppercase'>{props.value}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
25
src/components/design/Image/Image.tsx
Normal file
25
src/components/design/Image/Image.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import classNames from 'classnames'
|
||||
import type { Component } from 'solid-js'
|
||||
|
||||
interface ImageProps {
|
||||
size?: number
|
||||
pixelated?: boolean
|
||||
src: string
|
||||
class?: string
|
||||
height?: number
|
||||
width?: number
|
||||
}
|
||||
|
||||
export const Image: Component<ImageProps> = (props) => {
|
||||
return (
|
||||
<img
|
||||
{...props}
|
||||
elementtiming=''
|
||||
fetchpriority='auto'
|
||||
style={{ 'image-rendering': Boolean(props.pixelated) ? 'pixelated' : 'unset' }}
|
||||
class={classNames(props.class, 'select-none')}
|
||||
height={props.size ?? props.height}
|
||||
width={props.size ?? props.width}
|
||||
/>
|
||||
)
|
||||
}
|
@ -1,21 +1,21 @@
|
||||
import type { HTMLMotionProps } from 'framer-motion'
|
||||
import type { Component } from 'solid-js'
|
||||
|
||||
import { AnimateView } from '../../system'
|
||||
import { Animation } from '../../../config'
|
||||
import { AnimateView } from '../AnimateView'
|
||||
|
||||
interface LoaderProps extends HTMLMotionProps<'main'> {
|
||||
interface LoaderProps {
|
||||
class?: string
|
||||
active: boolean
|
||||
}
|
||||
|
||||
export const Loader: React.FC<LoaderProps> = ({ active, ...rest }) => {
|
||||
export const Loader: Component<LoaderProps> = (props) => {
|
||||
return (
|
||||
<AnimateView
|
||||
condition={active}
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0 }}
|
||||
{...rest}>
|
||||
class={props.class}
|
||||
condition={props.active}
|
||||
animation={Animation.fadeInOut({ scale: [0, 1, 0], y: [1, 4, 1] })}>
|
||||
<svg width='44' height='44' viewBox='0 0 44 44' xmlns='http://www.w3.org/2000/svg' stroke='#fff'>
|
||||
<g fill='none' fillRule='evenodd' strokeWidth={2}>
|
||||
<g fill='none' fill-rule='evenodd' stroke-width={2}>
|
||||
<circle cx='22' cy='22' r='1'>
|
||||
<animate
|
||||
attributeName='r'
|
||||
|
@ -1 +1,4 @@
|
||||
export * from './Loader'
|
||||
export * from './AnimateView'
|
||||
export * from './Image'
|
||||
export * from './Button'
|
||||
|
@ -1,17 +0,0 @@
|
||||
import classNames from 'classnames'
|
||||
|
||||
interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {
|
||||
icon: JSX.Element
|
||||
value: string
|
||||
className?: string
|
||||
handler?: () => void
|
||||
}
|
||||
|
||||
export const Button: React.FC<ButtonProps> = ({ icon, value, className, handler }) => {
|
||||
return (
|
||||
<button onClick={handler} className={classNames(className, 'flex items-center justify-center gap-x-5')}>
|
||||
{icon}
|
||||
<span className='uppercase'>{value}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
@ -1,61 +1,57 @@
|
||||
import { Motion } from '@motionone/solid'
|
||||
import classNames from 'classnames'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { useState } from 'react'
|
||||
import type { Component, JSXElement } from 'solid-js'
|
||||
import { For, createSignal } from 'solid-js'
|
||||
|
||||
export interface IDownloaderContent {
|
||||
title: string
|
||||
features: string[]
|
||||
}
|
||||
import type { GameDataDownloader } from '../../../../config'
|
||||
import { Animation } from '../../../../config'
|
||||
import { AnimateView } from '../../../design'
|
||||
|
||||
interface DownloaderProps {
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
content: IDownloaderContent
|
||||
children: JSXElement
|
||||
content: typeof GameDataDownloader
|
||||
}
|
||||
|
||||
export const Downloader: React.FC<DownloaderProps> = ({ className, children, content }) => {
|
||||
const [activeContent, setActiveContent] = useState(false)
|
||||
export const Downloader: Component<DownloaderProps> = (props) => {
|
||||
const [activeContent, setActiveContent] = createSignal(false)
|
||||
|
||||
const handleActiveContent = (): void => {
|
||||
return setActiveContent(!activeContent)
|
||||
const handleActiveContent = (): boolean => {
|
||||
return setActiveContent(!activeContent())
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
className={classNames(className, 'relative rounded-xl shadow-red-500 hover:shadow-2xl')}
|
||||
class={classNames(props.className, 'relative rounded-xl shadow-red-500 hover:shadow-2xl')}
|
||||
onMouseEnter={handleActiveContent}
|
||||
onMouseLeave={handleActiveContent}>
|
||||
{children}
|
||||
{props.children}
|
||||
|
||||
<AnimatePresence>
|
||||
{activeContent && (
|
||||
<motion.ul
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1, transition: { staggerChildren: 0.5 } }}
|
||||
exit={{ opacity: 0 }}
|
||||
className='pointer-events-none text-center absolute left-0 top-0 flex h-full w-full flex-col items-center justify-center rounded-xl bg-black/70 pb-5 text-[12px] text-white'>
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
className='mb-3 text-[16px]'>
|
||||
{content.title}
|
||||
</motion.h1>
|
||||
<AnimateView
|
||||
condition={activeContent()}
|
||||
animation={Animation.fadeInOut()}
|
||||
class='pointer-events-none absolute left-0 top-0 flex h-full w-full flex-col items-center justify-center rounded-xl bg-black/70 pb-5 text-center text-[12px] text-white'>
|
||||
<Motion.h1
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
class='mb-3 text-[16px]'>
|
||||
{props.content.title}
|
||||
</Motion.h1>
|
||||
|
||||
{content.features.map((feature) => {
|
||||
return (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 50, scale: 0.75 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 50, scale: 0.75 }}
|
||||
key={feature}>
|
||||
{feature}
|
||||
</motion.span>
|
||||
)
|
||||
})}
|
||||
</motion.ul>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<For each={props.content.features}>
|
||||
{(feature) => {
|
||||
return (
|
||||
<Motion.span
|
||||
initial={{ opacity: 0, y: 50, scale: 0.75 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 50, scale: 0.75 }}>
|
||||
{feature}
|
||||
</Motion.span>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</AnimateView>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
@ -1,19 +1,19 @@
|
||||
import { Fragment, useState } from 'react'
|
||||
import type { Component } from 'solid-js'
|
||||
import { createSignal } from 'solid-js'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { Image, Popup } from '../../system'
|
||||
import { Button } from '../Button'
|
||||
import { GameAssetsDownloader, GameDataDownloader } from '../../../config/GameDownloader'
|
||||
import type { ConvertionHandler } from '../../../types'
|
||||
import { handleConvertion } from '../../../tools/handleConvertion'
|
||||
import { Downloader } from './Downloader'
|
||||
import type { ConvertionHandler } from '../../../types/global'
|
||||
import { Loader } from '../../design'
|
||||
import { Animation, GameAssetsDownloader, GameDataDownloader } from '../../../config'
|
||||
import { Button, Image, Loader } from '../../design'
|
||||
import { Popup } from '../Popup'
|
||||
import { Downloader } from './Downloader/Downloader'
|
||||
|
||||
export const Downloaders: React.FC = () => {
|
||||
const [message, setMessage] = useState('')
|
||||
const [error, setError] = useState(false)
|
||||
const [popup, setPopup] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
export const Downloaders: Component = () => {
|
||||
const [message, setMessage] = createSignal('')
|
||||
const [error, setError] = createSignal(false)
|
||||
const [popup, setPopup] = createSignal(false)
|
||||
const [loading, setLoading] = createSignal(false)
|
||||
|
||||
const callback: ConvertionHandler = (message, state = 'idle') => {
|
||||
switch (state) {
|
||||
@ -43,38 +43,39 @@ export const Downloaders: React.FC = () => {
|
||||
return callback(`Completed in: ${seconds} seconds`, 'success')
|
||||
}
|
||||
|
||||
console.log(Animation.fadeInOut({ scale: [0, 1, 0], y: [1, 4, 1] }))
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Popup condition={popup}>
|
||||
<>
|
||||
<Loader active={loading()} class='mt-10' />
|
||||
<Popup condition={popup()}>
|
||||
<span
|
||||
className={classNames('', {
|
||||
class={classNames('', {
|
||||
'text-red-600': error
|
||||
})}>
|
||||
{message}
|
||||
{message()}
|
||||
</span>
|
||||
|
||||
<Loader active={loading} className='mt-10' />
|
||||
|
||||
<Button
|
||||
value='Close'
|
||||
icon={<Image src='/icons/cross.png' />}
|
||||
className={classNames('bg-red-600 mt-6 opacity-0 p-2 px-4 active:opacity-40 invisible text-white', {
|
||||
'!opacity-100 !visible': !loading
|
||||
class={classNames('invisible mt-6 bg-red-600 p-2 px-4 text-white opacity-0 active:opacity-40', {
|
||||
'!visible !opacity-100': !loading()
|
||||
})}
|
||||
handler={() => {
|
||||
return setPopup(!popup)
|
||||
return setPopup(!popup())
|
||||
}}
|
||||
/>
|
||||
</Popup>
|
||||
|
||||
<ul className='flex gap-x-8'>
|
||||
<ul class='flex gap-x-8'>
|
||||
<Downloader content={GameDataDownloader}>
|
||||
<Image src='/images/Gamedata.png' />
|
||||
|
||||
<Button
|
||||
value='Download Gamedata'
|
||||
icon={<Image src='/icons/game.png' size={22} />}
|
||||
className='download-button border-gamedata-secondary bg-gamedata-primary shadow-gamedata-primary/20'
|
||||
class='download-button border-gamedata-secondary bg-gamedata-primary shadow-gamedata-primary/20'
|
||||
handler={downloadGameData}
|
||||
/>
|
||||
</Downloader>
|
||||
@ -84,11 +85,11 @@ export const Downloaders: React.FC = () => {
|
||||
|
||||
<Button
|
||||
value='Download GameAssets'
|
||||
icon={<Image src='/icons/picture.png' icon />}
|
||||
className='download-button border-gameAssets-secondary bg-gameAssets-primary shadow-gameAssets-primary/40'
|
||||
icon={<Image src='/icons/picture.png' pixelated />}
|
||||
class='download-button border-gameAssets-secondary bg-gameAssets-primary shadow-gameAssets-primary/40'
|
||||
/>
|
||||
</Downloader>
|
||||
</ul>
|
||||
</Fragment>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
23
src/components/layout/Popup/Popup.tsx
Normal file
23
src/components/layout/Popup/Popup.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import type { Component, JSXElement } from 'solid-js'
|
||||
|
||||
import { AnimateView } from '../../design'
|
||||
import { Animation } from '../../../config'
|
||||
|
||||
interface PopupProps {
|
||||
condition: boolean
|
||||
children: JSXElement
|
||||
}
|
||||
|
||||
export const Popup: Component<PopupProps> = (props) => {
|
||||
return (
|
||||
<AnimateView
|
||||
animation={Animation.fadeInOut()}
|
||||
class='absolute left-0 top-0 z-20 flex h-screen w-screen flex-col items-center justify-center bg-black/40 text-white backdrop-blur-xl transition-all'
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
condition={props.condition}>
|
||||
{props.children}
|
||||
</AnimateView>
|
||||
)
|
||||
}
|
41
src/components/layout/TitleBar/TitleBar.tsx
Normal file
41
src/components/layout/TitleBar/TitleBar.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { FiMaximize, FiMinus, FiX } from 'solid-icons/fi'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import type { Component } from 'solid-js'
|
||||
|
||||
import { Image } from '../../design'
|
||||
|
||||
export const TitleBar: Component = () => {
|
||||
return (
|
||||
<nav class='!z-50 flex h-[40px] w-full items-center justify-between bg-[#1f1f1f] text-white'>
|
||||
<div
|
||||
class='w-full select-none'
|
||||
onMouseDown={async () => {
|
||||
return await appWindow.startDragging()
|
||||
}}>
|
||||
<Image src='/Logo.svg' size={60} class='ml-3 p-1' />
|
||||
</div>
|
||||
|
||||
<ul class='flex h-full'>
|
||||
<li
|
||||
class='grid h-full w-14 cursor-pointer place-items-center transition-colors duration-[10ms] hover:bg-[#2a2a2a]'
|
||||
onClick={async () => {
|
||||
return await appWindow.minimize()
|
||||
}}>
|
||||
<FiMinus />
|
||||
</li>
|
||||
|
||||
<li class='grid h-full w-14 cursor-not-allowed place-items-center opacity-40'>
|
||||
<FiMaximize size={20} />
|
||||
</li>
|
||||
|
||||
<li
|
||||
class='grid h-full w-14 cursor-pointer place-items-center transition-colors duration-[10ms] hover:bg-red-500'
|
||||
onClick={async () => {
|
||||
return await appWindow.close()
|
||||
}}>
|
||||
<FiX />
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
export * from './Button'
|
||||
export * from './Downloaders'
|
||||
export * from './Popup'
|
||||
export * from './TitleBar'
|
||||
|
@ -1,19 +0,0 @@
|
||||
import type { HTMLMotionProps } from 'framer-motion'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
|
||||
export interface AnimateViewProps extends HTMLMotionProps<'main'> {
|
||||
condition: boolean
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const AnimateView: React.FC<AnimateViewProps> = ({ condition, children, ...rest }) => {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{condition && (
|
||||
<motion.main initial={rest.initial} animate={rest.animate} exit={rest.exit} {...rest}>
|
||||
{children}
|
||||
</motion.main>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import classNames from 'classnames'
|
||||
|
||||
interface ImageProps extends React.ComponentPropsWithoutRef<'img'> {
|
||||
size?: number
|
||||
icon?: boolean
|
||||
}
|
||||
|
||||
export const Image: React.FC<ImageProps> = ({ size, icon, ...rest }) => {
|
||||
return (
|
||||
<img
|
||||
{...rest}
|
||||
draggable={false}
|
||||
style={{ imageRendering: Boolean(icon) ? 'pixelated' : 'unset' }}
|
||||
className={classNames(rest.className, 'select-none')}
|
||||
height={size ?? rest.height}
|
||||
width={size ?? rest.width}
|
||||
/>
|
||||
)
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import { AnimateView } from '../AnimateView'
|
||||
|
||||
interface PopupProps {
|
||||
condition: boolean
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const Popup: React.FC<PopupProps> = ({ condition, children }) => {
|
||||
return (
|
||||
<AnimateView
|
||||
className='absolute flex text-white items-center flex-col transition-all justify-center z-20 top-0 left-0 w-screen h-screen bg-black/40 backdrop-blur-xl'
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
condition={condition}>
|
||||
{children}
|
||||
</AnimateView>
|
||||
)
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import { Maximize, Minus, X } from 'react-feather'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
|
||||
import { Image } from '..'
|
||||
|
||||
export const TitleBar: React.FC = () => {
|
||||
return (
|
||||
<nav className='flex w-full h-[40px] !z-50 items-center justify-between bg-[#1f1f1f] text-white'>
|
||||
<div
|
||||
className='w-full select-none'
|
||||
onMouseDown={async () => {
|
||||
return await appWindow.startDragging()
|
||||
}}>
|
||||
<Image src='/Logo.svg' size={60} className='ml-3 p-1' />
|
||||
</div>
|
||||
|
||||
<ul className='flex h-full'>
|
||||
<li
|
||||
className='grid h-full w-14 cursor-pointer place-items-center transition-colors duration-[10ms] hover:bg-[#2a2a2a]'
|
||||
onClick={async () => {
|
||||
return await appWindow.minimize()
|
||||
}}>
|
||||
<Minus />
|
||||
</li>
|
||||
|
||||
<li className='grid h-full w-14 place-items-center cursor-not-allowed opacity-40'>
|
||||
<Maximize size={20} />
|
||||
</li>
|
||||
|
||||
<li
|
||||
className='grid h-full w-14 cursor-pointer place-items-center transition-colors duration-[10ms] hover:bg-red-500'
|
||||
onClick={async () => {
|
||||
return await appWindow.close()
|
||||
}}>
|
||||
<X />
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import { Fragment } from 'react'
|
||||
|
||||
import { TitleBar } from '../TitleBar'
|
||||
|
||||
interface WindowProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const Window: React.FC<WindowProps> = ({ children }) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<TitleBar />
|
||||
|
||||
<main className='relative flex items-center justify-center py-20 h-full w-screen flex-col bg-[#242424]'>
|
||||
{children}
|
||||
</main>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from './Window'
|
@ -1,5 +0,0 @@
|
||||
export * from './Image'
|
||||
export * from './Window'
|
||||
export * from './AnimateView'
|
||||
export * from './TitleBar'
|
||||
export * from './Popup'
|
23
src/config/Animation.ts
Normal file
23
src/config/Animation.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import type { Options, Variant } from '@motionone/solid'
|
||||
|
||||
const fadeInOut = (variant?: Variant): Options => {
|
||||
const getVariantAtPosition = (pos: 0 | 1 | 2): Record<string, number> | null => {
|
||||
if (variant == null) return null
|
||||
|
||||
const variantAtPos: Record<string, number> = {}
|
||||
|
||||
for (const [key, value] of Object.entries(variant)) {
|
||||
variantAtPos[key] = (value as number[])[pos as keyof typeof value]
|
||||
}
|
||||
|
||||
return variantAtPos
|
||||
}
|
||||
|
||||
return {
|
||||
initial: { opacity: 0, ...getVariantAtPosition(0) },
|
||||
animate: { opacity: 1, ...getVariantAtPosition(1) },
|
||||
exit: { opacity: 1, ...getVariantAtPosition(2) }
|
||||
}
|
||||
}
|
||||
|
||||
export const Animation = { fadeInOut }
|
13
src/config/Domain.ts
Normal file
13
src/config/Domain.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export enum DomainTypes {
|
||||
Portuguese = 'com.br',
|
||||
Turkish = 'com.tr',
|
||||
English = 'com',
|
||||
German = 'de',
|
||||
Spanish = 'es',
|
||||
Finnish = 'fi',
|
||||
French = 'fr',
|
||||
Italian = 'it',
|
||||
Dutch = 'nl'
|
||||
}
|
||||
|
||||
export const SUPPORTED_LANGS = Object.keys(DomainTypes)
|
@ -1,6 +1,7 @@
|
||||
import { getClient, ResponseType } from '@tauri-apps/api/http'
|
||||
|
||||
import type { DomainTypes, GameEndPointsTypes } from '../types'
|
||||
import { DomainTypes } from './Domain'
|
||||
import type { GamedataEndpoints } from '../tools/rusty'
|
||||
|
||||
const PROD_VERSION_REGEX = /(production-[^/]+)/im
|
||||
const STABLE_PROD_VERSION = 'PRODUCTION-202304181630-471782382'
|
||||
@ -15,33 +16,19 @@ export const HABBO_GORDON_URL = `https://images.habbo.com/gordon/${PROD_VERSION
|
||||
|
||||
export const client = await getClient()
|
||||
await client
|
||||
.get(`${HABBO_URL('com')}/gamedata/external_variables/0`, {
|
||||
.get(`${HABBO_URL(DomainTypes.English)}/gamedata/external_variables/0`, {
|
||||
responseType: ResponseType.Text
|
||||
})
|
||||
.then(({ data }) => {
|
||||
return (PROD_VERSION = (data as string).match(PROD_VERSION_REGEX)?.[0])
|
||||
})
|
||||
|
||||
export const GAME_ENDPOINTS = (domain: DomainTypes): GameEndPointsTypes => {
|
||||
export const GAMEDATA_ENDPOINTS = async (domain: DomainTypes): Promise<GamedataEndpoints[]> => {
|
||||
return [
|
||||
{
|
||||
src: `${HABBO_URL(domain)}/gamedata/figuredata/0`,
|
||||
convert: 'XML',
|
||||
fileName: 'FigureData'
|
||||
},
|
||||
{
|
||||
src: `${HABBO_GORDON_URL}/figuremap.xml`,
|
||||
convert: 'XML',
|
||||
fileName: 'FigureMap'
|
||||
},
|
||||
{
|
||||
src: `${HABBO_GORDON_URL}/effectmap.xml`,
|
||||
convert: 'XML',
|
||||
fileName: 'EffectMap'
|
||||
},
|
||||
{
|
||||
src: `${HABBO_URL(domain)}/gamedata/furnidata_json/0`,
|
||||
fileName: 'FurniData'
|
||||
convert: 'JSON',
|
||||
file_name: 'FurniData'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,11 +1,9 @@
|
||||
import type { IDownloaderContent } from '../components/layout/Downloaders/Downloader'
|
||||
|
||||
export const GameDataDownloader: IDownloaderContent = {
|
||||
export const GameDataDownloader = {
|
||||
title: 'Converts and bundles:',
|
||||
features: ['XML/TXT to minified JSON files', 'Converts SWF files to Parquet']
|
||||
features: ['XML/TXT to minified JSON files', 'Converts SWF files to Sprite']
|
||||
}
|
||||
|
||||
export const GameAssetsDownloader: IDownloaderContent = {
|
||||
export const GameAssetsDownloader = {
|
||||
title: 'Fetches PNG/JPEG:',
|
||||
features: [
|
||||
'Badges + Badgeparts',
|
||||
|
5
src/config/index.ts
Normal file
5
src/config/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './Animation'
|
||||
export * from './Convertion'
|
||||
export * from './Domain'
|
||||
export * from './Endpoints'
|
||||
export * from './GameDownloader'
|
@ -1,7 +1,7 @@
|
||||
import type { IFurni, IFurniData, IXML, KeyValuePairs } from '../types'
|
||||
|
||||
export class FurniData {
|
||||
public data: IFurniData = { roomItemTypes: [], wallItemTypes: [] }
|
||||
public data: IFurniData = { floorItems: [], wallItems: [] }
|
||||
public fileName: string
|
||||
|
||||
constructor(data: IXML, fileName: string) {
|
||||
@ -13,13 +13,13 @@ export class FurniData {
|
||||
|
||||
private parseRoomItemTypes(roomItems: IFurni[]): void {
|
||||
for (const roomItem of roomItems) {
|
||||
this.data.roomItemTypes.push(roomItem)
|
||||
this.data.floorItems.push(roomItem)
|
||||
}
|
||||
}
|
||||
|
||||
private parseWallItemTypes(wallItems: IFurni[]): void {
|
||||
for (const wallItem of wallItems) {
|
||||
this.data.wallItemTypes.push(wallItem)
|
||||
this.data.wallItems.push(wallItem)
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,12 +33,12 @@ export class FurniData {
|
||||
public get classNamesAndRevisions(): KeyValuePairs<string, string> {
|
||||
const entries: KeyValuePairs<string, string> = {}
|
||||
|
||||
for (const roomItem of this.data.roomItemTypes) {
|
||||
for (const roomItem of this.data.floorItems) {
|
||||
const { className, revision } = this.getClassNameRevision(roomItem)
|
||||
entries[className] = String(revision)
|
||||
}
|
||||
|
||||
for (const wallItem of this.data.wallItemTypes) {
|
||||
for (const wallItem of this.data.wallItems) {
|
||||
const { className, revision } = this.getClassNameRevision(wallItem)
|
||||
entries[className] = String(revision)
|
||||
}
|
||||
|
@ -5,22 +5,33 @@ export const useOutSideClickEventHandler = (callback: () => void): RefObject<HTM
|
||||
const wrapper = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: KeyboardEvent): void => {
|
||||
if (Boolean(wrapper.current?.contains(event.target as Node))) return
|
||||
if (event.key !== 'Escape') return
|
||||
const handleClickOutside = (event: KeyboardEvent | MouseEvent): void => {
|
||||
const currentEvent = event as KeyboardEvent
|
||||
|
||||
if (
|
||||
wrapper.current == null ||
|
||||
Boolean(wrapper.current.contains(currentEvent.target as Node)) ||
|
||||
currentEvent.key !== 'Escape'
|
||||
)
|
||||
return
|
||||
|
||||
return callback()
|
||||
}
|
||||
|
||||
window.addEventListener('click', (event: any) => {
|
||||
window.addEventListener('click', (event) => {
|
||||
return handleClickOutside(event)
|
||||
})
|
||||
|
||||
window.addEventListener('keydown', (event: any) => {
|
||||
window.addEventListener('keydown', (event) => {
|
||||
return handleClickOutside(event)
|
||||
})
|
||||
|
||||
return () => {
|
||||
return window.removeEventListener('click', (event: any) => {
|
||||
window.removeEventListener('click', (event) => {
|
||||
return handleClickOutside(event)
|
||||
})
|
||||
|
||||
window.removeEventListener('keydown', (event) => {
|
||||
return handleClickOutside(event)
|
||||
})
|
||||
}
|
||||
|
9
src/index.tsx
Normal file
9
src/index.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { render } from 'solid-js/web'
|
||||
|
||||
import './styles.css'
|
||||
import '@fontsource/press-start-2p'
|
||||
import App from './App'
|
||||
|
||||
render(() => {
|
||||
return <App />
|
||||
}, document.getElementById('root') as HTMLElement)
|
12
src/main.tsx
12
src/main.tsx
@ -1,12 +0,0 @@
|
||||
import React from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
|
||||
import './styles.css'
|
||||
import '@fontsource/press-start-2p'
|
||||
import App from './App'
|
||||
|
||||
createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
@ -1,6 +1,3 @@
|
||||
import { XMLParser } from 'fast-xml-parser'
|
||||
|
||||
import type { GameEndPointsTypes } from '../types'
|
||||
import { FigureMap } from '../controllers/FigureMap'
|
||||
import { EffectMap } from '../controllers/EffectMap'
|
||||
import { convertTXT } from './convertTXT'
|
||||
@ -9,18 +6,18 @@ import { FurniData } from '../controllers/FurniData'
|
||||
import { Convertion } from '../config/Convertion'
|
||||
import { parseData } from './parseData'
|
||||
|
||||
export const fetchGamedataConfig = async (data: string, endpoint: GameEndPointsTypes[number]): Promise<unknown> => {
|
||||
export const fetchGamedataConfig = async (data: string, endpoint: GamedataEndpoints): Promise<unknown> => {
|
||||
switch (endpoint.convert) {
|
||||
case 'XML':
|
||||
const convertedData = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '' }).parse(data)
|
||||
let parsedData: FigureData | FigureMap | EffectMap | undefined
|
||||
|
||||
if (endpoint.fileName === 'FigureData') {
|
||||
parsedData = new FigureData(convertedData, endpoint.fileName)
|
||||
} else if (endpoint.fileName === 'FigureMap') {
|
||||
parsedData = new FigureMap(convertedData, endpoint.fileName)
|
||||
} else if (endpoint.fileName === 'EffectMap') {
|
||||
parsedData = new EffectMap(convertedData, endpoint.fileName)
|
||||
if (endpoint.file_name === 'FigureData') {
|
||||
parsedData = new FigureData(convertedData, endpoint.file_name)
|
||||
} else if (endpoint.file_name === 'FigureMap') {
|
||||
parsedData = new FigureMap(convertedData, endpoint.file_name)
|
||||
} else if (endpoint.file_name === 'EffectMap') {
|
||||
parsedData = new EffectMap(convertedData, endpoint.file_name)
|
||||
}
|
||||
|
||||
return await parseData(Convertion.gamedataDir, parsedData?.fileName, parsedData?.data).catch((error) => {
|
||||
@ -32,8 +29,8 @@ export const fetchGamedataConfig = async (data: string, endpoint: GameEndPointsT
|
||||
default: {
|
||||
let parsedData: FurniData | undefined
|
||||
|
||||
if (endpoint.fileName === 'FurniData') {
|
||||
parsedData = new FurniData(JSON.parse(data), endpoint.fileName)
|
||||
if (endpoint.file_name === 'FurniData') {
|
||||
parsedData = new FurniData(JSON.parse(data), endpoint.file_name)
|
||||
}
|
||||
|
||||
return await parseData(Convertion.gamedataDir, parsedData?.fileName, parsedData?.data).catch((error) => {
|
@ -1,9 +1,11 @@
|
||||
import { ResponseType } from '@tauri-apps/api/http'
|
||||
|
||||
import { GAME_ENDPOINTS, client } from '../config/Endpoints'
|
||||
import { GAMEDATA_ENDPOINTS, client } from '../config/Endpoints'
|
||||
import type { ConvertionHandler } from '../types'
|
||||
import type { DomainTypes } from '../types/Domain'
|
||||
import { fetchGamedataConfig } from './fetchGamedataConfig'
|
||||
import type { DomainTypes } from '../config/Domain'
|
||||
import { parseData } from './parseData'
|
||||
import { Convertion } from '../config/Convertion'
|
||||
import { downloadGamedata } from './rusty'
|
||||
|
||||
export const handleConvertion = async (
|
||||
domain: DomainTypes,
|
||||
@ -13,17 +15,24 @@ export const handleConvertion = async (
|
||||
if (!assetsOption) {
|
||||
callback('Initializing Gamedata configuration...', 'loading')
|
||||
|
||||
const gameData = await GAMEDATA_ENDPOINTS(domain)
|
||||
|
||||
await Promise.all(
|
||||
GAME_ENDPOINTS(domain).map(async (endpoint) => {
|
||||
await client
|
||||
.get(endpoint.src, { responseType: ResponseType.Text })
|
||||
.then(async ({ data }) => {
|
||||
return await fetchGamedataConfig(data as string, endpoint)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
return callback(error, 'error')
|
||||
})
|
||||
gameData.map(async (endpoint) => {
|
||||
if (endpoint.src.startsWith('http')) {
|
||||
return await client
|
||||
.get(endpoint.src, { responseType: ResponseType.Text })
|
||||
.then(async ({ data }) => {
|
||||
return await downloadGamedata(data as string, endpoint).catch((error) => {
|
||||
return console.log(error)
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
return callback(error, 'error')
|
||||
})
|
||||
} else {
|
||||
return await parseData(Convertion.gamedataDir, endpoint.file_name, endpoint.src)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
export * from './convertTXT'
|
@ -11,11 +11,10 @@ export const parseData = async (
|
||||
|
||||
const fileDir = path.concat(`/${fileName}.json`)
|
||||
|
||||
// By default, output files will be overwritten, and I cannot recursively remove the entire output folder
|
||||
// By default, output files will be overwritten. I cannot recursively remove the entire output folder
|
||||
// and create it again because it just won't parse files' contents for some reason
|
||||
|
||||
if (!(await exists(Convertion.outputDir))) await createDir(Convertion.outputDir)
|
||||
if (!(await exists(Convertion.gamedataDir))) await createDir(Convertion.gamedataDir)
|
||||
if (!(await exists(Convertion.gamedataDir))) await createDir(Convertion.gamedataDir, { recursive: true })
|
||||
|
||||
return await writeFile(fileDir, typeof fileContent === 'object' ? JSON.stringify(fileContent) : fileContent)
|
||||
}
|
||||
|
17
src/tools/rusty.ts
Normal file
17
src/tools/rusty.ts
Normal file
@ -0,0 +1,17 @@
|
||||
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__TAURI_INVOKE__<T>(cmd: string, args?: Record<string, unknown>): Promise<T>;
|
||||
}
|
||||
}
|
||||
|
||||
const invoke = window.__TAURI_INVOKE__;
|
||||
|
||||
export function downloadGamedata(data: string, endpoint: GamedataEndpoints) {
|
||||
return invoke<null>("download_gamedata", { data,endpoint })
|
||||
}
|
||||
|
||||
export type Converters = "FigureData" | "FigureMap" | "EffectMap" | "FurniData"
|
||||
export type ConvertTypes = "TXT" | "XML" | "JSON"
|
||||
export type GamedataEndpoints = { src: string; convert: ConvertTypes; file_name: Converters }
|
13
src/types/Converters.d.ts
vendored
13
src/types/Converters.d.ts
vendored
@ -1,4 +1,11 @@
|
||||
import type { IFigureDataPalette, IFigureDataSetType, IFigureMapLibrary, IFurni, IProduct } from './SubConverters'
|
||||
import type {
|
||||
IFigureDataPalette,
|
||||
IFigureDataSetType,
|
||||
IFigureMapLibrary,
|
||||
IFloorItem,
|
||||
IFurni,
|
||||
IProduct
|
||||
} from './SubConverters'
|
||||
import type { KeyValuePairs } from './global'
|
||||
|
||||
export interface IFigureData {
|
||||
@ -12,8 +19,8 @@ export interface IFigureMap {
|
||||
}
|
||||
|
||||
export interface IFurniData {
|
||||
roomItemTypes: IFurni[]
|
||||
wallItemTypes: IFurni[]
|
||||
floorItems: IFloorItem[]
|
||||
wallItems: IFurni[]
|
||||
}
|
||||
|
||||
export type IEffectMap = KeyValuePairs<string, KeyValuePairs<string, string>>
|
||||
|
1
src/types/Domain.d.ts
vendored
1
src/types/Domain.d.ts
vendored
@ -1 +0,0 @@
|
||||
export type DomainTypes = 'com.br' | 'com.tr' | 'com' | 'de' | 'es' | 'fi' | 'fr' | 'it' | 'nl'
|
9
src/types/Endpoint.d.ts
vendored
9
src/types/Endpoint.d.ts
vendored
@ -1,9 +0,0 @@
|
||||
import type { XMLParser } from 'fast-xml-parser'
|
||||
|
||||
export type GameEndPointsTypes = Array<{
|
||||
src: string
|
||||
convert?: 'TXT' | 'XML'
|
||||
fileName: string
|
||||
}>
|
||||
|
||||
export type IXML = ReturnType<XMLParser['parse']>
|
44
src/types/SubConverters.d.ts
vendored
44
src/types/SubConverters.d.ts
vendored
@ -1,31 +1,33 @@
|
||||
import type { KeyValuePairs } from './global'
|
||||
|
||||
export interface IFloorItemDimensions {
|
||||
x: number
|
||||
y: number
|
||||
defaultDirection: number
|
||||
}
|
||||
|
||||
export interface IFloorItemPermissions {
|
||||
canSitOn: boolean
|
||||
canLayOn: boolean
|
||||
canStandOn: boolean
|
||||
}
|
||||
|
||||
export interface IFloorItem extends IFurni {
|
||||
dimensions: IFloorItemDimensions
|
||||
permissions: IFloorItemPermissions
|
||||
}
|
||||
|
||||
export interface IFurni {
|
||||
id: number
|
||||
classname: string
|
||||
revision: number
|
||||
category?: string
|
||||
defaultdir: number
|
||||
xdim: number
|
||||
ydim: number
|
||||
partcolors?: { color: string[] }
|
||||
name?: string
|
||||
description?: string
|
||||
name?: string
|
||||
furniLine?: string
|
||||
customParams?: string
|
||||
adurl?: string
|
||||
offerid?: number
|
||||
buyout: boolean
|
||||
rentofferid: number
|
||||
rentbuyout: boolean
|
||||
bc: boolean
|
||||
excludeddynamic: boolean
|
||||
customparams?: string
|
||||
specialtype: number
|
||||
canstandon: boolean
|
||||
cansiton: boolean
|
||||
canlayon: boolean
|
||||
furniline?: string
|
||||
environment?: string
|
||||
rare: boolean
|
||||
offerID?: number
|
||||
excludeDynamic: boolean
|
||||
specialType: number
|
||||
}
|
||||
|
||||
export type Club = 'idle' | 'HC' | 'VIP'
|
||||
|
4
src/types/global.d.ts
vendored
4
src/types/global.d.ts
vendored
@ -1,6 +1,2 @@
|
||||
export type StateTypes = 'idle' | 'loading' | 'error' | 'success'
|
||||
export type ConvertionHandler = (message: string, state: StateTypes) => void
|
||||
|
||||
export type KeyValuePairs<KeyType extends number | string, ValueType> = {
|
||||
[key in KeyType]: ValueType
|
||||
}
|
||||
|
2
src/types/index.d.ts
vendored
2
src/types/index.d.ts
vendored
@ -1,5 +1,3 @@
|
||||
export * from './Converters'
|
||||
export * from './SubConverters'
|
||||
export * from './Endpoint'
|
||||
export * from './Domain'
|
||||
export * from './global'
|
||||
|
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
Loading…
Reference in New Issue
Block a user