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:
Walid 2023-05-02 15:49:05 +01:00
parent 6ba06e09d2
commit d26b429ee8
Signed by: Walidoux
GPG Key ID: CCF21881FE8BEBAF
45 changed files with 385 additions and 342 deletions

View File

@ -1,13 +1,18 @@
import { Window } from './components/system' import type { Component } from 'solid-js'
import { Downloaders } from './components/layout'
const Main: React.FC = () => { import { Downloaders, TitleBar } from './components/layout'
const Main: Component = () => {
return ( return (
<Window> <>
<span className='mb-20 text-white'>I would like to:</span> <TitleBar />
<Downloaders /> <main class='relative flex h-full w-screen flex-col items-center justify-center bg-[#242424] py-20'>
</Window> <span class='mb-20 text-white'>I would like to:</span>
<Downloaders />
</main>
</>
) )
} }

View 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>
)
}

View 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>
)
}

View 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}
/>
)
}

View File

@ -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 active: boolean
} }
export const Loader: React.FC<LoaderProps> = ({ active, ...rest }) => { export const Loader: Component<LoaderProps> = (props) => {
return ( return (
<AnimateView <AnimateView
condition={active} class={props.class}
initial={{ opacity: 0, scale: 0 }} condition={props.active}
animate={{ opacity: 1, scale: 1 }} animation={Animation.fadeInOut({ scale: [0, 1, 0], y: [1, 4, 1] })}>
exit={{ opacity: 0, scale: 0 }}
{...rest}>
<svg width='44' height='44' viewBox='0 0 44 44' xmlns='http://www.w3.org/2000/svg' stroke='#fff'> <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'> <circle cx='22' cy='22' r='1'>
<animate <animate
attributeName='r' attributeName='r'

View File

@ -1 +1,4 @@
export * from './Loader' export * from './Loader'
export * from './AnimateView'
export * from './Image'
export * from './Button'

View File

@ -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>
)
}

View File

@ -1,61 +1,57 @@
import { Motion } from '@motionone/solid'
import classNames from 'classnames' import classNames from 'classnames'
import { AnimatePresence, motion } from 'framer-motion' import type { Component, JSXElement } from 'solid-js'
import { useState } from 'react' import { For, createSignal } from 'solid-js'
export interface IDownloaderContent { import type { GameDataDownloader } from '../../../../config'
title: string import { Animation } from '../../../../config'
features: string[] import { AnimateView } from '../../../design'
}
interface DownloaderProps { interface DownloaderProps {
className?: string className?: string
children: React.ReactNode children: JSXElement
content: IDownloaderContent content: typeof GameDataDownloader
} }
export const Downloader: React.FC<DownloaderProps> = ({ className, children, content }) => { export const Downloader: Component<DownloaderProps> = (props) => {
const [activeContent, setActiveContent] = useState(false) const [activeContent, setActiveContent] = createSignal(false)
const handleActiveContent = (): void => { const handleActiveContent = (): boolean => {
return setActiveContent(!activeContent) return setActiveContent(!activeContent())
} }
return ( return (
<li <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} onMouseEnter={handleActiveContent}
onMouseLeave={handleActiveContent}> onMouseLeave={handleActiveContent}>
{children} {props.children}
<AnimatePresence> <AnimateView
{activeContent && ( condition={activeContent()}
<motion.ul animation={Animation.fadeInOut()}
initial={{ opacity: 0 }} 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'>
animate={{ opacity: 1, transition: { staggerChildren: 0.5 } }} <Motion.h1
exit={{ opacity: 0 }} initial={{ opacity: 0, y: -20 }}
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'> animate={{ opacity: 1, y: 0 }}
<motion.h1 exit={{ opacity: 0, y: -20 }}
initial={{ opacity: 0, y: -20 }} class='mb-3 text-[16px]'>
animate={{ opacity: 1, y: 0 }} {props.content.title}
exit={{ opacity: 0, y: -20 }} </Motion.h1>
className='mb-3 text-[16px]'>
{content.title}
</motion.h1>
{content.features.map((feature) => { <For each={props.content.features}>
return ( {(feature) => {
<motion.span return (
initial={{ opacity: 0, y: 50, scale: 0.75 }} <Motion.span
animate={{ opacity: 1, y: 0, scale: 1 }} initial={{ opacity: 0, y: 50, scale: 0.75 }}
exit={{ opacity: 0, y: 50, scale: 0.75 }} animate={{ opacity: 1, y: 0, scale: 1 }}
key={feature}> exit={{ opacity: 0, y: 50, scale: 0.75 }}>
{feature} {feature}
</motion.span> </Motion.span>
) )
})} }}
</motion.ul> </For>
)} </AnimateView>
</AnimatePresence>
</li> </li>
) )
} }

View File

@ -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 classNames from 'classnames'
import { Image, Popup } from '../../system' import type { ConvertionHandler } from '../../../types'
import { Button } from '../Button'
import { GameAssetsDownloader, GameDataDownloader } from '../../../config/GameDownloader'
import { handleConvertion } from '../../../tools/handleConvertion' import { handleConvertion } from '../../../tools/handleConvertion'
import { Downloader } from './Downloader' import { Animation, GameAssetsDownloader, GameDataDownloader } from '../../../config'
import type { ConvertionHandler } from '../../../types/global' import { Button, Image, Loader } from '../../design'
import { Loader } from '../../design' import { Popup } from '../Popup'
import { Downloader } from './Downloader/Downloader'
export const Downloaders: React.FC = () => { export const Downloaders: Component = () => {
const [message, setMessage] = useState('') const [message, setMessage] = createSignal('')
const [error, setError] = useState(false) const [error, setError] = createSignal(false)
const [popup, setPopup] = useState(false) const [popup, setPopup] = createSignal(false)
const [loading, setLoading] = useState(true) const [loading, setLoading] = createSignal(false)
const callback: ConvertionHandler = (message, state = 'idle') => { const callback: ConvertionHandler = (message, state = 'idle') => {
switch (state) { switch (state) {
@ -43,38 +43,39 @@ export const Downloaders: React.FC = () => {
return callback(`Completed in: ${seconds} seconds`, 'success') return callback(`Completed in: ${seconds} seconds`, 'success')
} }
console.log(Animation.fadeInOut({ scale: [0, 1, 0], y: [1, 4, 1] }))
return ( return (
<Fragment> <>
<Popup condition={popup}> <Loader active={loading()} class='mt-10' />
<Popup condition={popup()}>
<span <span
className={classNames('', { class={classNames('', {
'text-red-600': error 'text-red-600': error
})}> })}>
{message} {message()}
</span> </span>
<Loader active={loading} className='mt-10' />
<Button <Button
value='Close' value='Close'
icon={<Image src='/icons/cross.png' />} 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', { class={classNames('invisible mt-6 bg-red-600 p-2 px-4 text-white opacity-0 active:opacity-40', {
'!opacity-100 !visible': !loading '!visible !opacity-100': !loading()
})} })}
handler={() => { handler={() => {
return setPopup(!popup) return setPopup(!popup())
}} }}
/> />
</Popup> </Popup>
<ul className='flex gap-x-8'> <ul class='flex gap-x-8'>
<Downloader content={GameDataDownloader}> <Downloader content={GameDataDownloader}>
<Image src='/images/Gamedata.png' /> <Image src='/images/Gamedata.png' />
<Button <Button
value='Download Gamedata' value='Download Gamedata'
icon={<Image src='/icons/game.png' size={22} />} 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} handler={downloadGameData}
/> />
</Downloader> </Downloader>
@ -84,11 +85,11 @@ export const Downloaders: React.FC = () => {
<Button <Button
value='Download GameAssets' value='Download GameAssets'
icon={<Image src='/icons/picture.png' icon />} icon={<Image src='/icons/picture.png' pixelated />}
className='download-button border-gameAssets-secondary bg-gameAssets-primary shadow-gameAssets-primary/40' class='download-button border-gameAssets-secondary bg-gameAssets-primary shadow-gameAssets-primary/40'
/> />
</Downloader> </Downloader>
</ul> </ul>
</Fragment> </>
) )
} }

View 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>
)
}

View 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>
)
}

View File

@ -1,2 +1,3 @@
export * from './Button'
export * from './Downloaders' export * from './Downloaders'
export * from './Popup'
export * from './TitleBar'

View File

@ -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>
)
}

View File

@ -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}
/>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -1 +0,0 @@
export * from './Window'

View File

@ -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
View 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
View 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)

View File

@ -1,6 +1,7 @@
import { getClient, ResponseType } from '@tauri-apps/api/http' 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 PROD_VERSION_REGEX = /(production-[^/]+)/im
const STABLE_PROD_VERSION = 'PRODUCTION-202304181630-471782382' 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() export const client = await getClient()
await client await client
.get(`${HABBO_URL('com')}/gamedata/external_variables/0`, { .get(`${HABBO_URL(DomainTypes.English)}/gamedata/external_variables/0`, {
responseType: ResponseType.Text responseType: ResponseType.Text
}) })
.then(({ data }) => { .then(({ data }) => {
return (PROD_VERSION = (data as string).match(PROD_VERSION_REGEX)?.[0]) 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 [ 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`, src: `${HABBO_URL(domain)}/gamedata/furnidata_json/0`,
fileName: 'FurniData' convert: 'JSON',
file_name: 'FurniData'
} }
] ]
} }

View File

@ -1,11 +1,9 @@
import type { IDownloaderContent } from '../components/layout/Downloaders/Downloader' export const GameDataDownloader = {
export const GameDataDownloader: IDownloaderContent = {
title: 'Converts and bundles:', 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:', title: 'Fetches PNG/JPEG:',
features: [ features: [
'Badges + Badgeparts', 'Badges + Badgeparts',

5
src/config/index.ts Normal file
View File

@ -0,0 +1,5 @@
export * from './Animation'
export * from './Convertion'
export * from './Domain'
export * from './Endpoints'
export * from './GameDownloader'

View File

@ -1,7 +1,7 @@
import type { IFurni, IFurniData, IXML, KeyValuePairs } from '../types' import type { IFurni, IFurniData, IXML, KeyValuePairs } from '../types'
export class FurniData { export class FurniData {
public data: IFurniData = { roomItemTypes: [], wallItemTypes: [] } public data: IFurniData = { floorItems: [], wallItems: [] }
public fileName: string public fileName: string
constructor(data: IXML, fileName: string) { constructor(data: IXML, fileName: string) {
@ -13,13 +13,13 @@ export class FurniData {
private parseRoomItemTypes(roomItems: IFurni[]): void { private parseRoomItemTypes(roomItems: IFurni[]): void {
for (const roomItem of roomItems) { for (const roomItem of roomItems) {
this.data.roomItemTypes.push(roomItem) this.data.floorItems.push(roomItem)
} }
} }
private parseWallItemTypes(wallItems: IFurni[]): void { private parseWallItemTypes(wallItems: IFurni[]): void {
for (const wallItem of wallItems) { 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> { public get classNamesAndRevisions(): KeyValuePairs<string, string> {
const entries: 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) const { className, revision } = this.getClassNameRevision(roomItem)
entries[className] = String(revision) entries[className] = String(revision)
} }
for (const wallItem of this.data.wallItemTypes) { for (const wallItem of this.data.wallItems) {
const { className, revision } = this.getClassNameRevision(wallItem) const { className, revision } = this.getClassNameRevision(wallItem)
entries[className] = String(revision) entries[className] = String(revision)
} }

View File

@ -5,22 +5,33 @@ export const useOutSideClickEventHandler = (callback: () => void): RefObject<HTM
const wrapper = useRef<HTMLDivElement>(null) const wrapper = useRef<HTMLDivElement>(null)
useEffect(() => { useEffect(() => {
const handleClickOutside = (event: KeyboardEvent): void => { const handleClickOutside = (event: KeyboardEvent | MouseEvent): void => {
if (Boolean(wrapper.current?.contains(event.target as Node))) return const currentEvent = event as KeyboardEvent
if (event.key !== 'Escape') return
if (
wrapper.current == null ||
Boolean(wrapper.current.contains(currentEvent.target as Node)) ||
currentEvent.key !== 'Escape'
)
return
return callback() return callback()
} }
window.addEventListener('click', (event: any) => { window.addEventListener('click', (event) => {
return handleClickOutside(event) return handleClickOutside(event)
}) })
window.addEventListener('keydown', (event: any) => { window.addEventListener('keydown', (event) => {
return handleClickOutside(event) return handleClickOutside(event)
}) })
return () => { return () => {
return window.removeEventListener('click', (event: any) => { window.removeEventListener('click', (event) => {
return handleClickOutside(event)
})
window.removeEventListener('keydown', (event) => {
return handleClickOutside(event) return handleClickOutside(event)
}) })
} }

9
src/index.tsx Normal file
View 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)

View File

@ -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>
)

View File

@ -1,6 +1,3 @@
import { XMLParser } from 'fast-xml-parser'
import type { GameEndPointsTypes } from '../types'
import { FigureMap } from '../controllers/FigureMap' import { FigureMap } from '../controllers/FigureMap'
import { EffectMap } from '../controllers/EffectMap' import { EffectMap } from '../controllers/EffectMap'
import { convertTXT } from './convertTXT' import { convertTXT } from './convertTXT'
@ -9,18 +6,18 @@ import { FurniData } from '../controllers/FurniData'
import { Convertion } from '../config/Convertion' import { Convertion } from '../config/Convertion'
import { parseData } from './parseData' 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) { switch (endpoint.convert) {
case 'XML': case 'XML':
const convertedData = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '' }).parse(data) const convertedData = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '' }).parse(data)
let parsedData: FigureData | FigureMap | EffectMap | undefined let parsedData: FigureData | FigureMap | EffectMap | undefined
if (endpoint.fileName === 'FigureData') { if (endpoint.file_name === 'FigureData') {
parsedData = new FigureData(convertedData, endpoint.fileName) parsedData = new FigureData(convertedData, endpoint.file_name)
} else if (endpoint.fileName === 'FigureMap') { } else if (endpoint.file_name === 'FigureMap') {
parsedData = new FigureMap(convertedData, endpoint.fileName) parsedData = new FigureMap(convertedData, endpoint.file_name)
} else if (endpoint.fileName === 'EffectMap') { } else if (endpoint.file_name === 'EffectMap') {
parsedData = new EffectMap(convertedData, endpoint.fileName) parsedData = new EffectMap(convertedData, endpoint.file_name)
} }
return await parseData(Convertion.gamedataDir, parsedData?.fileName, parsedData?.data).catch((error) => { return await parseData(Convertion.gamedataDir, parsedData?.fileName, parsedData?.data).catch((error) => {
@ -32,8 +29,8 @@ export const fetchGamedataConfig = async (data: string, endpoint: GameEndPointsT
default: { default: {
let parsedData: FurniData | undefined let parsedData: FurniData | undefined
if (endpoint.fileName === 'FurniData') { if (endpoint.file_name === 'FurniData') {
parsedData = new FurniData(JSON.parse(data), endpoint.fileName) parsedData = new FurniData(JSON.parse(data), endpoint.file_name)
} }
return await parseData(Convertion.gamedataDir, parsedData?.fileName, parsedData?.data).catch((error) => { return await parseData(Convertion.gamedataDir, parsedData?.fileName, parsedData?.data).catch((error) => {

View File

@ -1,9 +1,11 @@
import { ResponseType } from '@tauri-apps/api/http' 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 { ConvertionHandler } from '../types'
import type { DomainTypes } from '../types/Domain' import type { DomainTypes } from '../config/Domain'
import { fetchGamedataConfig } from './fetchGamedataConfig' import { parseData } from './parseData'
import { Convertion } from '../config/Convertion'
import { downloadGamedata } from './rusty'
export const handleConvertion = async ( export const handleConvertion = async (
domain: DomainTypes, domain: DomainTypes,
@ -13,17 +15,24 @@ export const handleConvertion = async (
if (!assetsOption) { if (!assetsOption) {
callback('Initializing Gamedata configuration...', 'loading') callback('Initializing Gamedata configuration...', 'loading')
const gameData = await GAMEDATA_ENDPOINTS(domain)
await Promise.all( await Promise.all(
GAME_ENDPOINTS(domain).map(async (endpoint) => { gameData.map(async (endpoint) => {
await client if (endpoint.src.startsWith('http')) {
.get(endpoint.src, { responseType: ResponseType.Text }) return await client
.then(async ({ data }) => { .get(endpoint.src, { responseType: ResponseType.Text })
return await fetchGamedataConfig(data as string, endpoint) .then(async ({ data }) => {
}) return await downloadGamedata(data as string, endpoint).catch((error) => {
.catch((error) => { return console.log(error)
console.error(error) })
return callback(error, 'error') })
}) .catch((error) => {
return callback(error, 'error')
})
} else {
return await parseData(Convertion.gamedataDir, endpoint.file_name, endpoint.src)
}
}) })
) )

View File

@ -1 +0,0 @@
export * from './convertTXT'

View File

@ -11,11 +11,10 @@ export const parseData = async (
const fileDir = path.concat(`/${fileName}.json`) 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 // 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, { recursive: true })
if (!(await exists(Convertion.gamedataDir))) await createDir(Convertion.gamedataDir)
return await writeFile(fileDir, typeof fileContent === 'object' ? JSON.stringify(fileContent) : fileContent) return await writeFile(fileDir, typeof fileContent === 'object' ? JSON.stringify(fileContent) : fileContent)
} }

17
src/tools/rusty.ts Normal file
View 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 }

View File

@ -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' import type { KeyValuePairs } from './global'
export interface IFigureData { export interface IFigureData {
@ -12,8 +19,8 @@ export interface IFigureMap {
} }
export interface IFurniData { export interface IFurniData {
roomItemTypes: IFurni[] floorItems: IFloorItem[]
wallItemTypes: IFurni[] wallItems: IFurni[]
} }
export type IEffectMap = KeyValuePairs<string, KeyValuePairs<string, string>> export type IEffectMap = KeyValuePairs<string, KeyValuePairs<string, string>>

View File

@ -1 +0,0 @@
export type DomainTypes = 'com.br' | 'com.tr' | 'com' | 'de' | 'es' | 'fi' | 'fr' | 'it' | 'nl'

View File

@ -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']>

View File

@ -1,31 +1,33 @@
import type { KeyValuePairs } from './global' 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 { export interface IFurni {
id: number id: number
classname: string classname: string
revision: number
category?: string
defaultdir: number
xdim: number
ydim: number
partcolors?: { color: string[] }
name?: string
description?: string description?: string
name?: string
furniLine?: string
customParams?: string
adurl?: string adurl?: string
offerid?: number offerID?: number
buyout: boolean excludeDynamic: boolean
rentofferid: number specialType: number
rentbuyout: boolean
bc: boolean
excludeddynamic: boolean
customparams?: string
specialtype: number
canstandon: boolean
cansiton: boolean
canlayon: boolean
furniline?: string
environment?: string
rare: boolean
} }
export type Club = 'idle' | 'HC' | 'VIP' export type Club = 'idle' | 'HC' | 'VIP'

View File

@ -1,6 +1,2 @@
export type StateTypes = 'idle' | 'loading' | 'error' | 'success' export type StateTypes = 'idle' | 'loading' | 'error' | 'success'
export type ConvertionHandler = (message: string, state: StateTypes) => void export type ConvertionHandler = (message: string, state: StateTypes) => void
export type KeyValuePairs<KeyType extends number | string, ValueType> = {
[key in KeyType]: ValueType
}

View File

@ -1,5 +1,3 @@
export * from './Converters' export * from './Converters'
export * from './SubConverters' export * from './SubConverters'
export * from './Endpoint'
export * from './Domain'
export * from './global' export * from './global'

1
src/vite-env.d.ts vendored
View File

@ -1 +0,0 @@
/// <reference types="vite/client" />