Compare commits

..

No commits in common. "feat/game(data/assets)-downloader" and "develop" have entirely different histories.

92 changed files with 1566 additions and 3324 deletions

2
.gitignore vendored
View File

@ -13,6 +13,8 @@ dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo

View File

@ -1,9 +1,3 @@
{
"recommendations": [
"tauri-apps.tauri-vscode",
"rust-lang.rust-analyzer",
"bradlc.vscode-tailwindcss",
"DavidAnson.vscode-markdownlint",
"solidjs-community.solid-snippets"
]
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
}

View File

@ -1,4 +0,0 @@
{
"typescript.tsdk": "./node_modules/typescript/lib",
"tailwindCSS.classAttributes": ["class", "className"]
}

21
LICENSE
View File

@ -1,21 +0,0 @@
# MIT License
Copyright (c) 2023 Rypî Dev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,94 +0,0 @@
# 🧿 Getting started
## 🎯 Todos
- [ ] Two options for downloading Habbo resources:
- [ ] Default data extraction. (Without conversion)
- [ ] Using [Scuti](https://scuti.netlify.app/) dataset for its [renderer.](https://github.com/kozennnn/scuti-renderer)
- [x] Languages feature. (Default: EN)
- [ ] Download Habbo assets. (>800 MB of disk memory, make sure the IPC handles it correctly)
- [x] Fix rendering animation issues.
- [ ] Add "abort" button with data suppression feature.
- [x] Convert front-end with SolidJS
- [x] Fix issue of prettier-plugin-tailwindcss
- [ ] Export helpers/utils methods into Rust:
- [ ] Implementing typesafety with types' collections using Tauri Specta
- [ ] Handling extracted data in:
- [ ] JSON (Default + Adjustements for Scuti)
- [ ] XML (Using quickxml_to_serde)
- [ ] TXT (no idea...)
- [ ] Parsing data using `std::{fs::File, io::Write}`
- [ ] Learning Rust:
- [ ] Getting to know about vectors in depth.
- [ ] Avoid conflicts in open-source.
- [ ] Undertsand: Memory cache and disk cache.
## Overview
- [How it works](#🏗️-how-it-works)
- [Techs stack](#📦-techs-stack)
- [Interesting topics](#📤-interesting-topics)
- [Credits](#🤝-credits)
## 📦 Techs stack
![Rust lang](https://img.shields.io/badge/Rust-black?style=for-the-badge&logo=rust&logoColor=#E57324)
![Typescript lang](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)
![Tauri](https://img.shields.io/badge/tauri-%2324C8DB.svg?style=for-the-badge&logo=tauri&logoColor=%23FFFFFF)
![Vite](https://img.shields.io/badge/Vite-B73BFE?style=for-the-badge&logo=vite&logoColor=FFD62E)
![SolidJS](https://img.shields.io/badge/SolidJS-2c4f7c?style=for-the-badge&logo=solid&logoColor=c8c9cb)
![Tailwind framework](https://img.shields.io/badge/Tailwind_CSS-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white)
![Eslint linter](https://img.shields.io/badge/eslint-3A33D1?style=for-the-badge&logo=eslint&logoColor=white)
![Prettier formatter](https://img.shields.io/badge/prettier-1A2C34?style=for-the-badge&logo=prettier&logoColor=F7BA3E)
## 🏗️ How it works
### Gamedata/Generic
It comes first with fetching data using the `@tauri-apps/api/http` module.
Converting uncoming data into minified JSON file formats.
(No need for avro, parquet, protobuf nor CSV for efficient data compression
and encoding schemas for fast data storing/retrieval)
## 📤 Interesting topics
### 🦀 Rust-related
- [Why serde_json crate is the best choice?](https://blog.logrocket.com/json-and-rust-why-serde_json-is-the-top-choice/)
### Misc
```shell
npx cloc . \
--exclude-dir=target,_site,node_modules \
--not-match-f=pnpm-lock.yaml
```
```shell
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
TypeScript 43 158 19 855
JSON 6 0 0 138
Rust 5 39 81 83
Markdown 1 19 0 45
CSS 2 4 0 30
TOML 1 5 2 27
SVG 1 0 0 24
JavaScript 3 0 1 23
HTML 1 2 0 11
-------------------------------------------------------------------------------
SUM: 63 227 103 1236
-------------------------------------------------------------------------------
```
## 🤝 Credits
This wouldn't be possible without [KOZEN](https://github.com/kozennnn)'s help and bare bones the

View File

@ -8,6 +8,6 @@
<body>
<div id="root"></div>
<script src="/src/index.tsx" type="module"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -2,48 +2,49 @@
"name": "@rypidev/rypi-scrapper",
"private": true,
"license": "MIT",
"type": "module",
"version": "0.0.0",
"description": "Scrapping Habbo gamedata and assets",
"description": "",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"dev:tauri": "vite --config vite.config.tauri.ts",
"build:tauri": "vite build --config vite.config.tauri.ts",
"preview": "vite preview"
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@motionone/solid": "10.16.0",
"@fontsource/press-start-2p": "^4.5.11",
"@tauri-apps/api": "^1.2.0",
"@tauri-apps/cli": "^1.2.3",
"classnames": "^2.3.2",
"flag-icons": "^6.6.6",
"solid-icons": "1.0.4",
"solid-js": "^1.7.4"
"fast-xml-parser": "4.1.3",
"framer-motion": "^10.10.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-feather": "^2.0.10"
},
"devDependencies": {
"@types/node": "^18.16.3",
"@tauri-apps/cli": "^1.2.2",
"@types/node": "^18.7.10",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^3.0.0",
"@walidoux/eslint-config": "1.0.3",
"@walidoux/prettier-config": "1.0.3",
"@walidoux/prettier-config": "1.0.2",
"autoprefixer": "10.4.14",
"cssnano": "6.0.1",
"cssnano-preset-advanced": "6.0.1",
"eslint": "8.39.0",
"postcss": "8.4.23",
"prettier": "2.8.8",
"prettier-plugin-tailwindcss": "0.2.8",
"tailwindcss": "3.3.2",
"typescript": "^5.0.4",
"vite": "^4.3.4",
"vite-plugin-solid": "^2.7.0",
"vite-plugin-tauri": "3.1.0"
"eslint": "8.36.0",
"postcss": "8.4.21",
"prettier": "2.8.7",
"prettier-plugin-tailwindcss": "0.2.6",
"tailwindcss": "3.3.1",
"typescript": "^4.7.4",
"vite": "^4.0.0"
},
"prettier": "@walidoux/prettier-config",
"eslintConfig": {
"extends": [
"@walidoux/eslint-config"
],
"ignorePatterns": [
"**/*.config.cjs"
"**/*.config.js",
"**/*.config.ts"
]
}
}

2143
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
...(!process.env.TAURI_DEBUG ? { cssnano: { preset: 'advanced' } } : {})
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}

View File

@ -1,4 +0,0 @@
module.exports = {
...require("@walidoux/prettier-config"),
plugins: ["prettier-plugin-tailwindcss"],
};

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 373 B

525
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,11 @@
[package]
name = "app"
name = "rypi-scrapper"
version = "0.0.0"
description = "Scrapping Habbo gamedata and assets"
authors = ["Walidoux"]
license = "MIT"
repository = "https://github.com/RypiDev/rypi-scrapper"
default-run = "app"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
edition = "2021"
build = "src/build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -15,20 +13,11 @@ build = "src/build.rs"
tauri-build = { version = "1.2", features = [] }
[dependencies]
tauri = { version = "1.2", features = ["fs-create-dir", "fs-exists", "fs-read-dir", "fs-remove-dir", "fs-write-file", "http-all", "path-all", "window-close", "window-minimize", "window-start-dragging"] }
tauri-specta = { version = "1.0.0", features = ["typescript"] }
tauri = { version = "1.2", features = ["fs-create-dir", "fs-exists", "fs-read-dir", "fs-write-file", "http-all", "shell-all", "window-close", "window-maximize", "window-minimize", "window-unmaximize"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
specta = "1.0.3"
quickxml_to_serde = "0.5.0"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem
# DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]
[profile.release]
strip = true # Automatically strip symbols from the binary
panic = "abort" # Strip expensive panic clean-up logic
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
lto = true # Enables link to optimizations
opt-level = "s" # Optimize for binary size

3
src-tauri/build.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@ -1 +0,0 @@
{"std":[["li","lh","ls","lc","ri","bd","sh","lg","ch","cc","cp","wa","ca","rh","rs","rc","hd","fc","ey","hr","hrb","fa","ea","ha","he"],["li","lh","ls","lc","bd","sh","lg","ch","cc","cp","wa","ca","ri","rh","rs","rc","hd","fc","ey","hr","hrb","fa","ea","ha","he"],["li","lh","ls","lc","bd","sh","lg","ch","cc","cp","wa","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","ri","rh","rs","rc"],["bd","sh","lg","ch","cc","cp","wa","ca","li","lh","ls","lc","hd","fc","ey","hr","hrb","fa","ea","ha","he","ri","rh","rs","rc"],["bd","sh","lg","ch","cc","cp","wa","ca","lh","ls","lc","li","hd","fc","ey","hr","hrb","fa","ea","ha","he","rh","rs","rc","ri"],["rh","rs","rc","ri","bd","sh","lg","ch","cc","cp","wa","ca","li","lh","ls","lc","hd","fc","ey","hr","hrb","fa","ea","ha","he"],["bd","sh","lg","ch","cc","cp","wa","ca","li","lh","ls","lc","hd","fc","ey","hr","hrb","fa","ea","ha","he","rh","rs","rc","ri"],["li","lh","ls","lc","ri","rh","rs","rc","bd","sh","lg","ch","cc","cp","wa","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he"]],"lh-up":{"4":["rh","rs","rc","bd","sh","lg","ch","cc","cp","wa","ri","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","lh","ls","lc","li"],"5":["rh","rs","rc","ri","bd","sh","lg","ch","cc","cp","wa","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","li","lh","ls","lc"],"6":["rh","rs","rc","ri","bd","sh","lg","ch","cc","cp","wa","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","li","lh","ls","lc"]},"rh-up":[["li","lh","ls","lc","ri","bd","sh","lg","ch","cc","cp","wa","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","rh","rs","rc"],["li","lh","ls","lc","bd","sh","lg","ch","cc","cp","wa","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","ri","rh","rs","rc"],["li","lh","ls","lc","bd","sh","lg","ch","cc","cp","wa","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","ri","rh","rs","rc"],["bd","sh","lg","ch","cc","cp","wa","li","lh","ls","lc","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","ri","rh","rs","rc"]],"sit":{"2":["li","lh","ls","lc","bd","lg","ch","cc","cp","wa","ca","rh","rs","rc","hd","fc","ey","hr","hrb","fa","ea","ha","he","ri","sh"],"3":["bd","lg","ch","cc","cp","wa","ca","li","lh","ls","lc","rh","rs","rc","hd","fc","ey","hr","hrb","fa","ea","ha","he","ri","sh"],"4":["rh","rs","rc","bd","lg","ch","cc","cp","wa","ca","lh","ls","lc","li","hd","fc","ey","hr","hrb","fa","ea","ha","he","ri","sh"]},"sit.lh-up":{"4":["rh","rs","rc","bd","lg","ch","cc","cp","wa","ri","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","lh","ls","lc","li","sh"]},"sit.rh-up":{"2":["li","lh","ls","lc","bd","lg","ch","cc","cp","wa","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","ri","rh","rs","rc","sh"],"3":["bd","lg","ch","cc","cp","wa","li","lh","ls","lc","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","ri","rh","rs","rc","sh"]},"lay":{"2":["lh","ls","lc","li","bd","lg","sh","ch","cc","cp","hd","fc","ey","wa","ri","rh","rs","rc","ca","hr","hrb","fa","ea","ha","he"],"4":["rh","rs","rc","ri","bd","lg","sh","ch","cc","cp","hd","fc","ey","wa","li","lh","ls","lc","ca","hr","hrb","fa","ea","ha","he"]}}

View File

@ -1,5 +0,0 @@
use tauri_build::build;
fn main() {
build()
}

View File

@ -1,112 +0,0 @@
use std::path::PathBuf;
use quickxml_to_serde::{xml_string_to_json, Config, NullValue};
use specta::specta;
use crate::{
structs::{ConvertTypes, GamedataEndpoints},
utils::convert_json,
};
#[specta]
#[tauri::command]
pub fn download_gamedata(data: &str, endpoint: GamedataEndpoints) {
match endpoint.convert {
ConvertTypes::XML => {
let conf = Config::new_with_custom_values(true, "", "txt", NullValue::Null);
let json = xml_string_to_json(data.to_owned(), &conf);
println!("{}", json.expect("Malformed XML").to_string());
}
ConvertTypes::TXT => println!("TXT CONVERTION"),
ConvertTypes::JSON => convert_json(data, endpoint.file_name),
}
}
#[specta]
#[tauri::command]
pub fn parse_data(
path: &str,
file_name: &str,
file_content: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let file_dir = PathBuf::from(&path).join(format!("{}.json", file_name));
println!("{:?}", file_dir);
// 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 !exists(&Convertion::gamedata_dir()) {
create_dir_all(&Convertion::gamedata_dir())?;
}
write_file(&file_dir, file_content.as_bytes()); */
Ok(())
}
/* pub async fn convert_txt(path: &str, data: &str) -> Result<(), Box<dyn std::error::Error>> {
let mut badges: Vec<Badge> = Vec::new();
let lines: Vec<&str> = data.split('\n').collect();
let badge_pattern = Regex::new(r"^badge_(?:name|desc)_([^=]+)").unwrap();
let description_pattern = Regex::new(r"^badge_desc_(\s*\w+)").unwrap();
let name_pattern = Regex::new(r"^badge_name_(\s*\w+)").unwrap();
for (index, line) in lines.iter().enumerate() {
let parts: Vec<&str> = line.split('=').collect();
if parts.len() < 2 {
continue;
}
let key = parts[0];
let value = parts[1];
if badge_pattern.is_match(key) {
if name_pattern.is_match(key) {
let badge_code = name_pattern.captures(key).unwrap()[1].to_owned();
let existing_badge = badges.iter_mut().find(|badge| badge.code == badge_code);
if let Some(badge) = existing_badge {
badge.name = Some(value.to_owned());
} else {
badges.push(Badge {
code: badge_code,
name: Some(value.to_owned()),
description: None,
});
}
} else if description_pattern.is_match(key) {
let badge_code = description_pattern.captures(key).unwrap()[1].to_owned();
let existing_badge = badges.iter_mut().find(|badge| badge.code == badge_code);
if let Some(badge) = existing_badge {
badge.description = Some(value.to_owned());
} else {
badges.push(Badge {
code: badge_code,
name: None,
description: Some(value.to_owned()),
});
}
}
// Remove the line from the vector
lines[index] = "";
}
}
// Remove empty lines
let lines: Vec<&str> = lines
.iter()
.filter(|line| !line.is_empty())
.cloned()
.collect();
parse_habbo_data(path, "Badges", &badges);
Ok(())
} */

View File

@ -1,28 +1,8 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
pub mod commands;
pub mod structs;
pub mod utils;
use commands::download_gamedata;
use specta::collect_types;
use tauri_specta::ts;
fn main() {
ts::export(collect_types![download_gamedata], "../src/tools/rusty.ts").unwrap();
// This is useful for custom eslint, prettier overrides at the top of the file.
/* ts::export_with_cfg_with_header(
collect_types![download_gamedata].unwrap(),
Default::default(),
"../src/tools/rusty.ts",
"/* eslint-disable */
\n".into(),
)
.unwrap(); */
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![download_gamedata])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@ -1,70 +0,0 @@
use serde::{Deserialize, Serialize};
use specta::Type;
/* pub struct Badge {
pub code: String,
pub name: Option<String>,
pub description: Option<String>,
} */
#[derive(Serialize, Deserialize, Type, Debug)]
pub enum ConvertTypes {
TXT,
XML,
JSON,
}
#[derive(Serialize, Deserialize, Type, Debug)]
pub enum Converters {
FigureData,
FigureMap,
EffectMap,
FurniData,
}
#[derive(Serialize, Deserialize, Type, Debug)]
pub struct GamedataEndpoints {
pub src: String,
pub convert: ConvertTypes,
pub file_name: Converters,
}
#[derive(Serialize, Deserialize, Type, Debug)]
pub struct FurniData {
pub floor_items: Vec<IFloorItem>,
pub wall_items: Vec<IFurni>,
}
#[derive(Serialize, Deserialize, Type, Debug)]
pub struct IFloorItem {
dimensions: IFloorItemDimensions,
permissions: IFloorItemPermissions,
}
#[derive(Serialize, Deserialize, Type, Debug)]
pub struct IFloorItemDimensions {
x: i32,
y: i32,
default_direction: i32,
}
#[derive(Serialize, Deserialize, Type, Debug)]
pub struct IFloorItemPermissions {
can_sit_on: bool,
can_lay_on: bool,
can_stand_on: bool,
}
#[derive(Serialize, Deserialize, Type, Debug)]
pub struct IFurni {
id: i32,
classname: String,
description: Option<String>,
name: Option<String>,
furni_line: Option<String>,
custom_params: Option<String>,
adurl: Option<String>,
offer_id: Option<i32>,
exclude_dynamic: bool,
special_type: i32,
}

View File

@ -1,54 +0,0 @@
use serde_json::Value;
use crate::structs::{Converters, FurniData, IFloorItem, IFurni};
pub fn debug_typeof<T>(_: &T) {
println!("{}", std::any::type_name::<T>())
}
pub fn convert_json(data: &str, file_name: Converters) {
let object: Value = serde_json::from_str(data).unwrap();
match file_name {
Converters::FurniData => {
let mut data = FurniData {
floor_items: Vec::new(),
wall_items: Vec::new(),
};
let floor_items_result: Result<_, Vec<IFloorItem>> =
Ok(serde_json::from_value::<Vec<IFloorItem>>(
object["wallitemtypes"]["furnitype"].clone(),
));
let wall_items_result: Result<_, Vec<IFurni>> = Ok(
serde_json::from_value::<Vec<IFurni>>(object["roomitemtypes"]["furnitype"].clone()),
);
// Handle the floor_items_result and wall_items_result appropriately
match (floor_items_result, wall_items_result) {
(Ok(floor_items), Ok(wall_items)) => {
while let Ok(ref value) = wall_items {
println!("{:?}", value);
}
while let Ok(ref value) = floor_items {
println!("{:?}", value);
}
}
(Err(e), _) => {
println!("Error deserializing floor items: {:?}", e);
}
(_, Err(e)) => {
println!("Error deserializing wall items: {:?}", e);
}
}
// loop through each key value pairs (make sure your handle null exceptions)
// parse the data to the global variable above
//
}
Converters::FigureData => {}
Converters::FigureMap => {}
Converters::EffectMap => {}
}
}

View File

@ -1,5 +1,11 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"build": {
"beforeDevCommand": "yarn dev",
"beforeBuildCommand": "yarn build",
"devPath": "http://localhost:1420",
"distDir": "../dist",
"withGlobalTauri": false
},
"package": {
"productName": "rypi-scrapper",
"version": "0.0.0"
@ -9,7 +15,8 @@
"window": {
"close": true,
"minimize": true,
"startDragging": true
"maximize": true,
"unmaximize": true
},
"os": {
"all": false
@ -18,12 +25,21 @@
"createDir": true,
"exists": true,
"readDir": true,
"removeDir": true,
"writeFile": true,
"scope": ["$DOWNLOAD/**/*"]
},
"path": {
"all": true
"shell": {
"all": true,
"execute": true,
"sidecar": true,
"open": true,
"scope": [
{
"name": "download-habbo-downloader",
"cmd": "npm",
"args": ["install", "-g", "habbo-downloader"]
}
]
},
"http": {
"all": true,
@ -33,12 +49,18 @@
},
"bundle": {
"active": true,
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
"identifier": "org.rypi.dev",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "com.tauri.dev",
"targets": "all"
},
"security": {
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
"csp": null
},
"updater": {
"active": false
@ -46,10 +68,10 @@
"windows": [
{
"decorations": false,
"resizable": false,
"center": true,
"width": 1000,
"height": 575
"resizable": true,
"width": 800,
"height": 500
}
]
}

View File

@ -1,20 +1,52 @@
import type { Component } from 'solid-js'
import { useState } from 'react'
import { Downloaders, TitleBar, LangDropdown } from './components/layout'
import { Image, Window, Button, Downloader } from './components'
import { fetchGameData } from './utils/fetchGameData'
import {
GameAssetsDownloader,
GameDataDownloader
} from './config/GameDownloader'
const Main: React.FC = () => {
const [error, setError] = useState('')
const [response, setResponse] = useState('')
const [loading, setLoading] = useState(false)
const callback = (message: string, error = false): void => {
if (error) return setError(message)
if (message === 'COMPLETED') setLoading(false)
else setResponse(message)
}
const Main: Component = () => {
return (
<>
<TitleBar />
<Window>
<span className='mb-20 text-white'>I would like to:</span>
<main class='relative flex h-full w-screen flex-col items-center justify-center bg-[#242424] py-20 text-white'>
<span class='mb-20'>I would like to:</span>
<ul className='flex gap-x-8'>
<Downloader content={GameDataDownloader}>
<Image src='/images/Gamedata.png' className='w-[400px]' />
<LangDropdown />
<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'
handler={async () => {
return await fetchGameData('com', callback)
}}
/>
</Downloader>
<Downloaders />
</main>
</>
<Downloader content={GameAssetsDownloader}>
<Image src='/images/GameAssets.png' className='w-[400px]' />
<Button
value='Download GameAssets'
icon={<Image src='/icons/picture.png' icon />}
className='download-button border-gameAssets-secondary bg-gameAssets-primary shadow-gameAssets-primary/40'
/>
</Downloader>
</ul>
</Window>
)
}

View File

@ -0,0 +1,27 @@
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

@ -0,0 +1,68 @@
import classNames from 'classnames'
import { AnimatePresence, motion } from 'framer-motion'
import { useState } from 'react'
export interface IDownloaderContent {
title: string
features: string[]
}
interface DownloaderProps {
className?: string
children: React.ReactNode
content: IDownloaderContent
}
export const Downloader: React.FC<DownloaderProps> = ({
className,
children,
content
}) => {
const [activeContent, setActiveContent] = useState(false)
const handleActiveContent = (): void => {
return setActiveContent(!activeContent)
}
return (
<li
className={classNames(
className,
'relative rounded-xl shadow-red-500 hover:shadow-2xl'
)}
onMouseEnter={handleActiveContent}
onMouseLeave={handleActiveContent}>
{children}
<AnimatePresence>
{activeContent && (
<motion.ul
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { staggerChildren: 0.5 } }}
exit={{ opacity: 0 }}
className='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-[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>
{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>
</li>
)
}

View File

@ -0,0 +1,19 @@
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

@ -0,0 +1,38 @@
import { Maximize, Minus, X } from 'react-feather'
import { appWindow } from '@tauri-apps/api/window'
import { Image } from '../Image'
export const TitleBar: React.FC = () => {
return (
<nav className='flex h-[37.5px] items-center justify-between bg-[#1f1f1f] text-white'>
<Image src='/Logo.svg' size={60} className='ml-3 p-1' />
<ul className='flex h-full'>
<li
onClick={async () => {
return await appWindow.minimize()
}}
className='grid h-full w-14 cursor-pointer place-items-center transition-colors duration-[10ms] hover:bg-[#2a2a2a]'>
<Minus />
</li>
<li
onClick={async () => {
return (await appWindow.isMaximized())
? await appWindow.unmaximize()
: await appWindow.maximize()
}}
className='grid h-full w-14 cursor-pointer place-items-center transition-colors duration-[10ms] hover:bg-[#2a2a2a]'>
<Maximize size={20} />
</li>
<li
onClick={async () => {
return await appWindow.close()
}}
className='grid h-full w-14 cursor-pointer place-items-center transition-colors duration-[10ms] hover:bg-red-500'>
<X />
</li>
</ul>
</nav>
)
}

View File

@ -0,0 +1,19 @@
import { Fragment } from 'react'
import { TitleBar } from '../TitleBar/TitleBar'
interface WindowProps {
children: React.ReactNode
}
export const Window: React.FC<WindowProps> = ({ children }) => {
return (
<Fragment>
<TitleBar />
<main className='flex h-screen w-screen flex-col items-center justify-center overflow-hidden bg-[#242424] p-10'>
{children}
</main>
</Fragment>
)
}

View File

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

View File

@ -1,22 +0,0 @@
import { Show } from 'solid-js'
import type { Component } from 'solid-js'
import type { MotionComponentProps, Variant } from '@motionone/solid'
import { Motion, Presence } from '@motionone/solid'
export interface AnimateViewProps extends MotionComponentProps {
animation?: Record<string, Variant>
condition: boolean
class?: string
}
export const AnimateView: Component<AnimateViewProps> = (props) => {
return (
<Presence>
<Show when={props.condition}>
<Motion.div class={props.class} {...props.animation}>
{props.children}
</Motion.div>
</Show>
</Presence>
)
}

View File

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

View File

@ -1,18 +0,0 @@
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

@ -1,22 +0,0 @@
import classNames from 'classnames'
import type { Component } from 'solid-js'
import { SUPPORTED_LANGS } from '../../../config'
interface FlagProps {
label?: boolean
domain: string
}
export const Flag: Component<FlagProps> = (props) => {
const lang = SUPPORTED_LANGS.filter((lang) => {
return lang.domain === props.domain
})[0]
return (
<>
<span class={classNames('', `fi fi-${lang.code}`)} />
{Boolean(props.label) && <span class='ml-3'>{lang.name}</span>}
</>
)
}

View File

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

View File

@ -1,25 +0,0 @@
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,67 +0,0 @@
import type { Component } from 'solid-js'
import { Animation } from '../../../config'
import { AnimateView } from '../AnimateView'
interface LoaderProps {
class?: string
active: boolean
}
export const Loader: Component<LoaderProps> = (props) => {
return (
<AnimateView
class={props.class}
condition={props.active}
variants={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' fill-rule='evenodd' stroke-width={2}>
<circle cx='22' cy='22' r='1'>
<animate
attributeName='r'
begin='0s'
dur='1.8s'
values='1; 20'
calcMode='spline'
keyTimes='0; 1'
keySplines='0.165, 0.84, 0.44, 1'
repeatCount='indefinite'
/>
<animate
attributeName='stroke-opacity'
begin='0s'
dur='1.8s'
values='1; 0'
calcMode='spline'
keyTimes='0; 1'
keySplines='0.3, 0.61, 0.355, 1'
repeatCount='indefinite'
/>
</circle>
<circle cx='22' cy='22' r='1'>
<animate
attributeName='r'
begin='-0.9s'
dur='1.8s'
values='1; 20'
calcMode='spline'
keyTimes='0; 1'
keySplines='0.165, 0.84, 0.44, 1'
repeatCount='indefinite'
/>
<animate
attributeName='stroke-opacity'
begin='-0.9s'
dur='1.8s'
values='1; 0'
calcMode='spline'
keyTimes='0; 1'
keySplines='0.3, 0.61, 0.355, 1'
repeatCount='indefinite'
/>
</circle>
</g>
</svg>
</AnimateView>
)
}

View File

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

View File

@ -1,5 +0,0 @@
export * from './Loader'
export * from './AnimateView'
export * from './Image'
export * from './Button'
export * from './Flag'

4
src/components/index.ts Normal file
View File

@ -0,0 +1,4 @@
export * from './Window'
export * from './Image'
export * from './Button'
export * from './Downloader'

View File

@ -1,48 +0,0 @@
import { Motion } from '@motionone/solid'
import classNames from 'classnames'
import type { Component, JSXElement } from 'solid-js'
import { For, createSignal } from 'solid-js'
import type { GameDataDownloader } from '../../../../config'
import { Animation } from '../../../../config'
import { AnimateView } from '../../../design'
interface DownloaderProps {
className?: string
children: JSXElement
content: typeof GameDataDownloader
}
export const Downloader: Component<DownloaderProps> = (props) => {
const [activeContent, setActiveContent] = createSignal(false)
const handleActiveContent = (): boolean => {
return setActiveContent(!activeContent())
}
return (
<li
class={classNames(props.className, 'relative rounded-xl shadow-red-500 hover:shadow-2xl')}
onMouseEnter={handleActiveContent}
onMouseLeave={handleActiveContent}>
{props.children}
<AnimateView
animation={Animation.fadeInOut()}
condition={activeContent()}
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 {...Animation.fadeInOut({ y: [-20, 0, -20] })} class='mb-3 text-[16px]'>
{props.content.title}
</Motion.h1>
<For each={props.content.features}>
{(feature) => {
return (
<Motion.span {...Animation.fadeInOut({ y: [50, 0, 50], scale: [0.75, 1, 0.75] })}>{feature}</Motion.span>
)
}}
</For>
</AnimateView>
</li>
)
}

View File

@ -1,96 +0,0 @@
import type { Component } from 'solid-js'
import { createSignal } from 'solid-js'
import classNames from 'classnames'
import type { ConvertionHandler } from '../../../types'
import { handleConvertion } from '../../../tools/handleConvertion'
import { GameAssetsDownloader, GameDataDownloader } from '../../../config'
import { Button, Image, Loader } from '../../design'
import { Popup } from '../Popup'
import { Downloader } from './Downloader/Downloader'
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) {
case 'loading':
setMessage(message)
return setLoading(true)
case 'success':
setMessage(message)
return setLoading(false)
case 'error':
setMessage(message)
setError(true)
return setLoading(false)
}
}
const downloadGameData = async (): Promise<void> => {
setPopup(true)
setLoading(true)
const startTime = new Date()
await handleConvertion('com', callback)
const endTime = new Date()
const seconds = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(2)
return callback(`Completed in: ${seconds} seconds`, 'success')
}
return (
<>
<Popup condition={popup()}>
<span class={classNames('', { 'text-red-600': error })}>{message()}</span>
<Loader active={loading()} class='mt-10' />
<Button
value='Abort'
icon={<Image src='/icons/cross.png' />}
class='mt-6 bg-red-600 p-2 px-4 active:opacity-40'
handler={() => {}}
/>
<Button
value='Close'
icon={<Image src='/icons/cross.png' />}
class={classNames('invisible mt-6 bg-red-600 p-2 px-4 opacity-0 active:opacity-40', {
'!visible !opacity-100': !loading()
})}
handler={() => {
return setPopup(!popup())
}}
/>
</Popup>
<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} />}
class='download-button border-gamedata-secondary bg-gamedata-primary shadow-gamedata-primary/20'
handler={downloadGameData}
/>
</Downloader>
<Downloader content={GameAssetsDownloader}>
<Image src='/images/GameAssets.png' />
<Button
value='Download GameAssets'
icon={<Image src='/icons/picture.png' pixelated />}
class='download-button border-gameAssets-secondary bg-gameAssets-primary shadow-gameAssets-primary/40'
/>
</Downloader>
</ul>
</>
)
}

View File

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

View File

@ -1,51 +0,0 @@
import type { Component } from 'solid-js'
import { createSignal, For } from 'solid-js'
import { Motion } from '@motionone/solid'
import { useLocalStorage } from '../../../hooks/useLocalStorage'
import { AnimateView, Flag } from '../../design'
import { OutSideEventHandler } from '../OutSideEventHandler'
import { Animation, SUPPORTED_LANGS } from '../../../config'
export const LangDropdown: Component = () => {
const { lang, setLang } = useLocalStorage()
let ref: HTMLDivElement | undefined
const [active, setActive] = createSignal(false)
return (
<OutSideEventHandler
class='relative text-sm'
onOutsideClick={() => {
return setActive(false)
}}>
<div
ref={ref}
class='cursor-pointer'
onclick={() => {
return setActive(!active())
}}>
Selected Lang: <Flag domain={lang()} />
</div>
<AnimateView condition={active()} class='absolute top-8 z-20' animation={Animation.fadeInOut()}>
<For each={SUPPORTED_LANGS}>
{(lang, index) => {
return (
<Motion.p
{...Animation.fadeInOut({ x: [50 / (index() + 1), 0, 0] })}
transition={{ delay: (index() + 1) / 15 }}
class='cursor-pointer'
onclick={() => {
setActive(false)
return setLang(lang.domain)
}}>
<Flag domain={lang.domain} label />
</Motion.p>
)
}}
</For>
</AnimateView>
</OutSideEventHandler>
)
}

View File

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

View File

@ -1,35 +0,0 @@
import type { Component, ComponentProps } from 'solid-js'
import { onCleanup, onMount } from 'solid-js'
interface OutSideEventHandlerProps extends ComponentProps<'div'> {
onOutsideClick: () => void
}
export const OutSideEventHandler: Component<OutSideEventHandlerProps> = (props) => {
let ref: HTMLDivElement | undefined
const handleClickOutside = (event: MouseEvent | KeyboardEvent): void => {
const currentEvent = event as KeyboardEvent
if (ref == null) return
if (ref.contains(event.target as Node) || currentEvent.key !== 'Escape') return
return props.onOutsideClick()
}
onMount(() => {
window.addEventListener('keydown', handleClickOutside)
return window.addEventListener('click', handleClickOutside)
})
onCleanup(() => {
window.removeEventListener('keydown', handleClickOutside)
return window.removeEventListener('click', handleClickOutside)
})
return (
<div {...props} ref={ref}>
{props.children}
</div>
)
}

View File

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

View File

@ -1,20 +0,0 @@
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'
condition={props.condition}>
{props.children}
</AnimateView>
)
}

View File

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

View File

@ -1,41 +0,0 @@
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,5 +0,0 @@
export * from './Downloaders'
export * from './Popup'
export * from './TitleBar'
export * from './LangDropdown'
export * from './OutSideEventHandler'

View File

@ -1,23 +0,0 @@
import type { Variant } from '@motionone/solid'
const fadeInOut = (variant?: Variant): Record<string, Variant> => {
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: 0, ...getVariantAtPosition(2) }
}
}
export const Animation = { fadeInOut }

View File

@ -1,10 +0,0 @@
import { downloadDir } from '@tauri-apps/api/path'
import { PROD_VERSION } from './Endpoints'
const outputDir = (await downloadDir()).concat(String(PROD_VERSION))
const gamedataDir = outputDir.concat('/gamedata')
const genericDir = outputDir.concat('/generic')
export const Convertion = { outputDir, gamedataDir, genericDir }

View File

@ -1,13 +0,0 @@
// ISO 3166-1-alpha-2 Flags
export const SUPPORTED_LANGS = [
{ name: 'Portuguese', code: 'br', domain: 'com.br' },
{ name: 'Turkish', code: 'tr', domain: 'com.tr' },
{ name: 'English', code: 'us', domain: 'com' },
{ name: 'German', code: 'de', domain: 'de' },
{ name: 'Spanish', code: 'es', domain: 'es' },
{ name: 'Finnish', code: 'fi', domain: 'fi' },
{ name: 'French', code: 'fr', domain: 'fr' },
{ name: 'Italian', code: 'it', domain: 'it' },
{ name: 'Dutch', code: 'nl', domain: 'nl' }
]

62
src/config/ENDPOINTS.ts Normal file
View File

@ -0,0 +1,62 @@
import { getClient, ResponseType } from '@tauri-apps/api/http'
import type { DomainTypes, GameEndPointsTypes } from '../types'
const PROD_VERSION_REGEX = /(production-[^/]+)/im
const STABLE_PROD_VERSION = 'PRODUCTION-202303282207-162719871'
export let PROD_VERSION: string | undefined
const HABBO_URL = (domain: DomainTypes): string => {
return `https://www.habbo.${domain}`
}
const HABBO_IMAGES = `https://images.habbo.com/gordon/${
PROD_VERSION ?? STABLE_PROD_VERSION
}`
export const client = await getClient()
await client
.get(`${HABBO_URL('com')}/gamedata/external_variables/0`, {
responseType: ResponseType.Text
})
.then(({ data }) => {
return (PROD_VERSION = (data as string).match(PROD_VERSION_REGEX)?.[0])
})
export const GAME_ENDPOINTS: GameEndPointsTypes = (domain) => {
return [
{
src: `${HABBO_URL(domain)}/gamedata/figuredata/0`,
convert: 'XML',
fileName: 'FigureData'
}
/* {
src: `${HABBO_IMAGES}/figuremap.xml`,
convert: 'XML',
fileName: 'FigureMap'
},
{
src: `${HABBO_URL(domain)}/gamedata/furnidata_json/0`,
fileName: 'FurniData'
},
{
src: `${HABBO_URL(domain)}/gamedata/productdata_json/0`,
fileName: 'ProductData'
},
{
src: `${HABBO_IMAGES}/effectmap.xml`,
convert: 'XML',
fileName: 'EffectMap'
},
{
src: `${HABBO_URL(domain)}/gamedata/external_variables/0`,
convert: 'TXT',
fileName: 'ExternalTexts'
} */
]
}
export const ASSETS_ENDPOINTS = (domain: DomainTypes): string[] => {
return [`${HABBO_URL(domain)}/`]
}

View File

@ -1,39 +0,0 @@
import { getClient, ResponseType } from '@tauri-apps/api/http'
import type { GamedataEndpoints } from '../tools/rusty'
import { useLocalStorage } from '../hooks/useLocalStorage'
const { lang } = useLocalStorage()
export const client = await getClient()
const PROD_VERSION_REGEX = /(production-[^/]+)/im
const STABLE_PROD_VERSION = 'PRODUCTION-202304181630-471782382'
export let PROD_VERSION: string | undefined
const HABBO_URL = (domain: string): string => {
return `https://www.habbo.${domain}`
}
export const HABBO_GORDON_URL = `https://images.habbo.com/gordon/${PROD_VERSION ?? STABLE_PROD_VERSION}`
export const GAMEDATA_ENDPOINTS = async (domain: string): Promise<GamedataEndpoints[]> => {
return [
{
src: `${HABBO_URL(domain)}/gamedata/furnidata_json/0`,
convert: 'JSON',
file_name: 'FurniData'
}
]
}
export const ASSETS_ENDPOINTS = (domain: string): string[] => {
return [`${HABBO_URL(domain)}/`]
}
await client
.get(`${HABBO_URL(lang())}/gamedata/external_variables/0`, {
responseType: ResponseType.Text
})
.then(({ data }) => {
return (PROD_VERSION = (data as string).match(PROD_VERSION_REGEX)?.[0])
})

View File

@ -1,15 +1,17 @@
export const GameDataDownloader = {
import type { IDownloaderContent } from '../components'
export const GameDataDownloader: IDownloaderContent = {
title: 'Converts and bundles:',
features: ['XML/TXT to minified JSON files', 'Converts SWF files to Sprite']
features: ['XML/TXT to minified JSON files', 'Converts SWF files to JSV']
}
export const GameAssetsDownloader = {
export const GameAssetsDownloader: IDownloaderContent = {
title: 'Fetches PNG/JPEG:',
features: [
'Badges + Badgeparts',
'Album + Recepetion images',
'Catalogue + Furni icons',
'Habbo Web Promo + Articles',
'MP3 Sounds'
'MP3 Sounds machine'
]
}

View File

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

View File

@ -1,19 +0,0 @@
import type { IEffectMap } from '../types'
export class EffectMap {
public data: IEffectMap = {}
public fileName: string
constructor(XML: any, fileName: string) {
this.fileName = fileName
this.parseLibrairies(XML.map.effect)
}
private parseLibrairies(effects: any[]): void {
for (const libraryXML of effects) {
this.data[libraryXML.type] == null && (this.data[libraryXML.type] = {})
this.data[libraryXML.type][libraryXML.id] = libraryXML.lib
}
}
}

View File

@ -1,78 +0,0 @@
import type { IFigureData, IFigureDataPalette, IFigureDataSet, IFigureDataSetType, IXML } from '../types'
export class FigureData {
public data: IFigureData = { palettes: [], setTypes: {} }
public fileName: string
constructor(XML: IXML, fileName: string) {
this.fileName = fileName
this.parsePalettes(XML.figuredata.colors.palette)
this.parseSetTypes(XML.figuredata.sets.settype)
}
private parsePalettes(palettes: any[]): void {
for (const paletteXML of palettes) {
const palette = {} as IFigureDataPalette
for (const colorXML of paletteXML.color) {
palette[Number(colorXML.id)] = {
index: Number(colorXML.index),
club: Number(colorXML.club),
selectable: Boolean(colorXML.selectable),
color: '#' + String(colorXML['#text'])
}
}
this.data.palettes.push(palette)
}
}
private parseSetTypes(setTypes: any[]): void {
for (const setTypeXML of setTypes) {
const settype: IFigureDataSetType = {
paletteId: Number(setTypeXML.paletteid),
mandatoryF0: Boolean(Number(setTypeXML.mand_f_0)),
mandatoryF1: Boolean(Number(setTypeXML.mand_f_1)),
mandatoryM0: Boolean(Number(setTypeXML.mand_m_0)),
mandatoryM1: Boolean(Number(setTypeXML.mand_m_1)),
sets: {}
}
for (const setXML of setTypeXML.set) {
const setType: IFigureDataSet = {
gender: setXML.gender,
club: Number(setXML.club),
colorable: Boolean(Number(setXML.colorable)),
selectable: Boolean(Number(setXML.selectable)),
preselectable: Boolean(Number(setXML.preselectable)),
sellable: setXML.sellable != null ? Boolean(Number(setXML.sellable)) : undefined,
parts: [],
hiddenLayers: []
}
for (const partXML of Array.isArray(setXML.part) ? setXML.part : [setXML.part]) {
setType.parts.push({
id: Number(partXML.id),
type: partXML.type,
colorable: Boolean(Number(partXML.colorable)),
index: Number(partXML.index),
colorindex: Number(partXML.colorindex)
})
}
if (setXML.hiddenLayers != null) {
for (const hiddenLayerXML of Array.isArray(setXML.hiddenLayers)
? setXML.hiddenLayers
: [setXML.hiddenLayers]) {
setType.hiddenLayers?.push(hiddenLayerXML.partType)
}
}
settype.sets[Number(setXML.id)] = setType
}
this.data.setTypes[String(setTypeXML.type)] = settype
}
}
}

View File

@ -1,25 +0,0 @@
import type { IFigureMap, IFigureMapLibrary, IXML } from '../types'
export class FigureMap {
public data: IFigureMap = { libraries: [], parts: {} }
public fileName: string
constructor(XML: IXML, fileName: string) {
this.fileName = fileName
this.parseLibrairies(XML.map.lib)
}
private parseLibrairies(librairies: any[]): void {
for (const libraryXML of librairies) {
const library: IFigureMapLibrary = { id: libraryXML.id, revision: Number(libraryXML.revision) }
for (const libraryPart of Array.isArray(libraryXML.part) ? libraryXML.part : [libraryXML.part]) {
this.data.parts[libraryPart.type] == null && (this.data.parts[libraryPart.type] = {})
this.data.parts[libraryPart.type][Number(libraryPart.id)] = librairies.indexOf(libraryXML)
}
this.data.libraries.push(library)
}
}
}

View File

@ -1,18 +0,0 @@
import type { Accessor } from 'solid-js'
import { createSignal } from 'solid-js'
export interface ILocalStorage {
lang: Accessor<string>
setLang: (domain: string) => void
}
const [lang, setLangeState] = createSignal<string>((localStorage.getItem('LANG_KEY') as string) ?? 'com')
const setLang: ILocalStorage['setLang'] = (domain) => {
localStorage.setItem('LANG_KEY', domain)
return setLangeState(domain)
}
export const useLocalStorage = (): ILocalStorage => {
return { lang, setLang }
}

View File

@ -1,11 +0,0 @@
import { render } from 'solid-js/web'
import './styles/fonts.css'
import './styles/styles.css'
import 'flag-icons/css/flag-icons.min.css'
import App from './App'
render(() => {
return <App />
}, document.getElementById('root') as HTMLElement)

12
src/main.tsx Normal file
View File

@ -0,0 +1,12 @@
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

@ -0,0 +1,3 @@
export const convertTXT = (data: string) => {
console.log(data)
}

194
src/mapping/convertXML.ts Normal file
View File

@ -0,0 +1,194 @@
import type {
IEffectMap,
IEffectMapLibrary,
IFigureData,
IFigureDataColor,
IFigureDataHiddenLayer,
IFigureDataPalette,
IFigureDataPart,
IFigureDataSet,
IFigureDataSetType,
IFigureMap,
IFigureMapLibrary,
IFigureMapLibraryPart
} from '../types'
export const convertXML = (
XML: any,
url: string
): IFigureData | IFigureMap | IEffectMap => {
if (url.includes('figuredata')) {
const output: IFigureData = { palettes: [], setTypes: [] }
for (const paletteXML of XML.figuredata.colors.palette) {
const palette: IFigureDataPalette = { id: 0, color: [] }
if (paletteXML.id !== undefined) palette.id = paletteXML.id
if (paletteXML.color !== undefined) {
for (const colorXML of paletteXML.color) {
const color: IFigureDataColor = {
id: 0,
index: 0,
club: 0,
selectable: false,
hexCode: ''
}
const hexColor = String(colorXML['#text' as keyof IFigureDataColor])
if (colorXML.id !== undefined) color.id = colorXML.id
if (colorXML.index !== undefined) color.index = colorXML.index
if (colorXML.club !== undefined) color.club = colorXML.club
if (colorXML.selectable !== undefined)
color.selectable = colorXML.selectable === 0 ? true : false
if (hexColor !== undefined) color.hexCode = hexColor
palette.color.push(color)
}
}
output.palettes.push(palette)
}
for (const setTypeXML of XML.figuredata.sets.settype) {
const settype: IFigureDataSetType = {
type: '',
paletteId: 0,
mandatoryF0: false,
mandatoryF1: false,
mandatoryM0: false,
mandatoryM1: false,
sets: []
}
if (setTypeXML.type !== undefined) settype.type = setTypeXML.type
if (setTypeXML.paletteId !== undefined)
settype.paletteId = setTypeXML.paletteId
if (setTypeXML.mandatoryF0 !== undefined)
settype.mandatoryF0 = setTypeXML.mandatoryF0
if (setTypeXML.mandatoryF1 !== undefined)
settype.mandatoryF1 = setTypeXML.mandatoryF1
if (setTypeXML.mandatoryM0 !== undefined)
settype.mandatoryM0 = setTypeXML.mandatoryM0
if (setTypeXML.mandatoryM1 !== undefined)
settype.mandatoryM1 = setTypeXML.mandatoryM1
if (setTypeXML.sets !== undefined) {
for (const setXML of setTypeXML.sets) {
const setType: IFigureDataSet = {
id: 0,
gender: 'U',
club: 0,
colorable: false,
selectable: false,
preselectable: false,
sellable: false,
parts: [],
hiddenLayers: []
}
if (setXML.id !== undefined) setType.id = setXML.id
if (setXML.gender !== undefined) setType.gender = setXML.gender
if (setXML.club !== undefined) setType.club = setXML.club
if (setXML.colorable !== undefined)
setType.colorable = setXML.colorable === 0 ? true : false
if (setXML.selectable !== undefined)
setType.selectable = setXML.selectable
if (setXML.preselectable !== undefined)
setType.preselectable = setXML.preselectable
if (setXML.sellable !== undefined) setType.sellable = setXML.sellable
if (setXML.parts !== undefined) {
for (const partXML of setXML.parts) {
const part: IFigureDataPart = {
id: 0,
type: '',
colorable: false,
index: 0,
colorindex: 0
}
if (partXML.id !== undefined) part.id = partXML.id
if (partXML.type !== undefined) part.type = partXML.type
if (partXML.colorable !== undefined)
part.colorable = partXML.colorable
if (partXML.index !== undefined) part.index = partXML.index
if (partXML.colorindex !== undefined)
part.colorindex = partXML.colorindex
setType.parts.push(part)
}
}
if (setXML.hiddenLayers !== undefined) {
for (const hiddenLayerXML of setXML.hiddenLayers) {
const hiddenLayer: IFigureDataHiddenLayer = { partType: '' }
if (hiddenLayerXML.partType !== undefined)
hiddenLayer.partType = hiddenLayerXML.partType
setType.hiddenLayers?.push(hiddenLayer)
}
}
settype.sets.push(setType)
}
}
output.setTypes.push(setTypeXML)
}
return output
} else if (url.includes('figuremap')) {
const output: IFigureMap = { libraries: [] }
for (const libraryXML of XML.map.lib as IFigureMapLibrary[]) {
const library: IFigureMapLibrary = { id: '', revision: 0, part: [] }
if (libraryXML.id !== undefined) library.id = libraryXML.id
if (libraryXML.revision !== undefined)
library.revision = libraryXML.revision
if (Array.isArray(libraryXML.part)) {
for (const libraryPart of libraryXML.part) {
const libraryPartXML: IFigureMapLibraryPart = { id: 0, type: '' }
if (libraryPart.id !== undefined) libraryPartXML.id = libraryPart.id
if (libraryPart.type !== undefined)
libraryPartXML.type = libraryPart.type
library.part.push(libraryPartXML)
}
} else {
const libraryPart = libraryXML.part as unknown as IFigureMapLibraryPart
library.part.push({ id: libraryPart.id, type: libraryPart.type })
}
output.libraries.push(library)
}
return output
} else {
const output: IEffectMap = { effects: [] }
for (const libraryXML of XML.map.effect as IEffectMapLibrary[]) {
const library: IEffectMapLibrary = {
id: 0,
lib: '',
type: '',
revision: 0
}
if (libraryXML.id !== undefined) library.id = libraryXML.id
if (libraryXML.lib !== undefined) library.lib = libraryXML.lib
if (libraryXML.type !== undefined) library.type = libraryXML.type
if (libraryXML.revision !== undefined)
library.revision = libraryXML.revision
output.effects.push(library)
}
return output
}
}

2
src/mapping/index.ts Normal file
View File

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

View File

@ -2,6 +2,10 @@
@tailwind components;
@tailwind utilities;
#root {
font-family: 'Press Start 2P';
}
@layer utilities {
.retro-text-shadow {
text-shadow: 0px 1.5px 0px #000000, -1px 3px 0px rgba(0, 0, 0, 0.54);
@ -16,6 +20,6 @@
@layer components {
.download-button {
@apply retro-text-shadow center-x-axis center-x-axis -bottom-4 z-10 h-12 w-10/12 border-2 text-xs shadow-2xl transition-all hover:-translate-x-1/2 hover:scale-105 hover:duration-200 active:-translate-x-1/2 active:scale-95 active:opacity-80 active:shadow-none active:duration-75;
@apply retro-text-shadow center-x-axis center-x-axis -bottom-4 z-10 h-12 w-10/12 border-2 text-xs text-white shadow-2xl transition-all hover:-translate-x-1/2 hover:scale-105 hover:duration-200 active:-translate-x-1/2 active:scale-95 active:opacity-80 active:shadow-none active:duration-75;
}
}

View File

@ -1,13 +0,0 @@
@font-face {
font-family: 'Press Start 2P';
font-style: normal;
font-display: swap;
font-weight: 400;
src: url('./PressStart2P-Regular.ttf');
}
#root {
font-family: 'Press Start 2P';
overflow: hidden;
height: 100vh;
}

View File

@ -1,54 +0,0 @@
import { parseData } from './parseData'
interface IBadge {
code: string
name?: string
description?: string
}
const badgePattern = /^badge_(?:name|desc)_([^=]+)/gim
const descriptionPattern = /^badge_desc_(\s*\w+)/
const namePattern = /^badge_name_(\s*\w+)/
export const convertTXT = async (path: string, data: string): Promise<void> => {
const badges: IBadge[] = []
const lines = data.split(/\r?\n/)
for (const line of lines) {
const [key, value] = line.split('=')
const badge = key.match(badgePattern)
if (badge != null) {
if (key.match(namePattern) != null) {
const badgeCode = key.match(namePattern)?.[1] as string
const existingBadge = badges.filter((badge) => {
return badge.code === badgeCode
})[0]
if (Boolean(existingBadge)) {
const index = badges.indexOf(existingBadge)
badges[index].name = value
} else {
badges.push({ code: badgeCode, name: value })
}
} else if (key.match(descriptionPattern) != null) {
const badgeCode = key.match(descriptionPattern)?.[1] as string
const existingBadge = badges.filter((badge) => {
return badge.code === badgeCode
})[0]
if (Boolean(existingBadge)) {
const index = badges.indexOf(existingBadge)
badges[index].description = value
} else {
badges.push({ code: badgeCode, description: value })
}
}
lines.splice(lines.indexOf(line), 1)
}
}
return await parseData(path, 'Badges', badges)
}

View File

@ -1,41 +0,0 @@
import { FigureMap } from '../controllers/FigureMap'
import { EffectMap } from '../controllers/EffectMap'
import { convertTXT } from './convertTXT'
import { FigureData } from '../controllers/FigureData'
import { FurniData } from '../controllers/FurniData'
import { Convertion } from '../config/Convertion'
import { parseData } from './parseData'
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.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) => {
return console.error(error)
})
case 'TXT':
return await convertTXT(Convertion.gamedataDir, data)
default: {
let parsedData: FurniData | undefined
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) => {
return console.error(error)
})
}
}
}

View File

@ -1,47 +0,0 @@
import { ResponseType } from '@tauri-apps/api/http'
import { GAMEDATA_ENDPOINTS, client } from '../config/Endpoints'
import type { ConvertionHandler } from '../types'
import { parseData } from './parseData'
import { Convertion } from '../config/Convertion'
import { downloadGamedata } from './rusty'
export const handleConvertion = async (
domain: string,
callback: ConvertionHandler,
assetsOption = false
): Promise<void> => {
if (!assetsOption) {
callback('Initializing Gamedata configuration...', 'loading')
const gameData = await GAMEDATA_ENDPOINTS(domain)
await Promise.all(
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)
}
})
)
// callback('Converting shockwave files...', 'loading')
// fetch, read and convert the files from the production folder in the user downloads' folder
// write the files into a seperate folder
} else {
/* ASSETS_ENDPOINTS(domain).map((endpoint) => {
client.get()
}) */
}
}

View File

@ -1,20 +0,0 @@
import { createDir, exists, writeFile } from '@tauri-apps/api/fs'
import { Convertion } from '../config/Convertion'
export const parseData = async (
path: string,
fileName: string | undefined,
fileContent: string | object | undefined
): Promise<void> => {
if (fileName == null || fileContent == null) return
const fileDir = path.concat(`/${fileName}.json`)
// 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.gamedataDir))) await createDir(Convertion.gamedataDir, { recursive: true })
return await writeFile(fileDir, typeof fileContent === 'object' ? JSON.stringify(fileContent) : fileContent)
}

View File

@ -1,17 +0,0 @@
// 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,16 +1,29 @@
import type { IFigureDataPalette, IFigureDataSetType, IFigureMapLibrary, IProduct } from './SubConverters'
import type {
IEffectMapLibrary,
IFigureDataPalette,
IFigureDataSetType,
IFigureMapLibrary,
IFurni,
IProduct
} from './SubConverters'
export interface IFigureData {
palettes: IFigureDataPalette[]
setTypes: Record<string, IFigureDataSetType>
setTypes: IFigureDataSetType[]
}
export interface IFigureMap {
libraries: IFigureMapLibrary[]
parts: Record<string, Record<number, number>>
}
export type IEffectMap = Record<string, Record<string, string>>
export interface IFurniData {
roomitemtypes: { furnitype: IFurni }
wallitemtypes: { furnitype: IFurni }
}
export interface IEffectMap {
effects: IEffectMapLibrary[]
}
export interface IProductData {
productData: { product: IProduct }

10
src/types/Domain.d.ts vendored Normal file
View File

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

3
src/types/Endpoint.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
export type GameEndPointsTypes = (
domain: DomainTypes
) => Array<{ src: string; convert?: 'TXT' | 'XML'; fileName: string }>

View File

@ -1,40 +1,76 @@
export type Club = 'idle' | 'HC' | 'VIP'
export interface IFigureDataPaletteType {
index: number
club: number
selectable: boolean
color: string
export interface IFurni {
id: number
classname: string
revision: number
category: string
defaultdir: number
xdim: number
ydim: number
partcolors: { color: string[] }
name: string
description: 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
}
export type IFigureDataPalette = Record<number, IFigureDataPaletteType>
export interface IFigureDataColor {
id: number
index: number
club: number // must be changed, either 0, 1, 2
selectable: boolean
hexCode: string
}
export interface IFigureDataPalette {
id: number
color: IFigureDataColor[]
}
export interface IFigureDataPart {
id: number
type: string
colorable: boolean
type: string // must be changed
colorable: boolean // must be changed
index: number
colorindex: number
}
export interface IFigureDataHiddenLayer {
partType: string // must be changed
}
export interface IFigureDataSet {
id: number
gender: 'M' | 'F' | 'U'
club: number
colorable: boolean
selectable: boolean
preselectable: boolean
sellable?: boolean
hiddenLayers?: string[]
club: number // 0, 1, 2
colorable: boolean // must be changed
selectable: boolean // must be changed
preselectable: boolean // must be changed
sellable?: boolean // must be changed
parts: IFigureDataPart[]
hiddenLayers?: IFigureDataHiddenLayer[]
}
export interface IFigureDataSetType {
type: string // must be changed
paletteId: number
mandatoryF0: boolean
mandatoryF1: boolean
mandatoryM0: boolean
mandatoryM1: boolean
sets: Record<number, IFigureDataSet>
mandatoryF0: boolean // 0, 1
mandatoryF1: boolean // 0, 1
mandatoryM0: boolean // 0, 1
mandatoryM1: boolean // 0, 1
sets: IFigureDataSet[]
}
export interface IFigureMapLibraryPart {
@ -45,6 +81,14 @@ export interface IFigureMapLibraryPart {
export interface IFigureMapLibrary {
id: string
revision: number
part: IFigureMapLibraryPart[]
}
export interface IEffectMapLibrary {
id: number
lib: string
type: string
revision: number
}
export interface IProduct {

View File

@ -1,2 +0,0 @@
export type StateTypes = 'idle' | 'loading' | 'error' | 'success'
export type ConvertionHandler = (message: string, state: StateTypes) => void

View File

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

View File

@ -0,0 +1,45 @@
import { ResponseType } from '@tauri-apps/api/http'
import { XMLParser } from 'fast-xml-parser'
import { GAME_ENDPOINTS, client } from '../config/ENDPOINTS'
import type { DomainTypes } from '../types/Domain'
import { convertXML } from '../mapping'
import { parseData } from './parseData'
export const fetchGameData = async (
domain: DomainTypes,
callback: (message: string, error: boolean) => void,
assetsOption = false
): Promise<void> => {
if (!assetsOption) {
await Promise.all(
GAME_ENDPOINTS(domain).map(async (endpoint) => {
await client
.get(endpoint.src, { responseType: ResponseType.Text })
.then(async ({ data }) => {
switch (endpoint.convert) {
case 'XML':
const convertedData = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: ''
}).parse(data as string)
const XML2JSON = convertXML(convertedData, endpoint.src)
console.log(XML2JSON)
return await parseData(endpoint.fileName, XML2JSON)
}
})
.catch((error) => {
console.error(error)
return callback(error, true)
})
})
)
} else {
/* ASSETS_ENDPOINTS(domain).map((endpoint) => {
client.get()
}) */
}
}

19
src/utils/parseData.ts Normal file
View File

@ -0,0 +1,19 @@
import { downloadDir } from '@tauri-apps/api/path'
import { createDir, exists, writeTextFile } from '@tauri-apps/api/fs'
import { PROD_VERSION } from '../config/ENDPOINTS'
export const parseData = async (
fileName: string,
fileContent: string | object
): Promise<void> => {
const outputDir = (await downloadDir()).concat(String(PROD_VERSION))
const fileDir = outputDir.concat(`/${fileName}.json`)
if (!(await exists(outputDir))) await createDir(outputDir)
await writeTextFile(
fileDir,
typeof fileContent === 'object' ? JSON.stringify(fileContent) : fileContent
)
}

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

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

View File

@ -1,6 +1,6 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx,css,md,mdx,html,json,scss}'],
content: ['./src/**/*.tsx'],
theme: {
extend: {
colors: {

View File

@ -4,6 +4,8 @@
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
@ -12,9 +14,8 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"types": ["vite/client"],
"jsxImportSource": "solid-js"
"jsx": "react-jsx"
},
"include": ["src"]
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

9
tsconfig.node.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -1,32 +0,0 @@
import { defineConfig, mergeConfig } from 'vite'
import baseViteConfig from './vite.config'
import { tauri } from 'vite-plugin-tauri'
// https://vitejs.dev/config/
export default defineConfig(
mergeConfig(
baseViteConfig,
defineConfig({
plugins: [tauri()],
// prevent vite from obscuring rust errors
clearScreen: false,
// Tauri expects a fixed port, fail if that port is not available
server: {
strictPort: true,
open: false
},
// to make use of `TAURI_PLATFORM`, `TAURI_ARCH`, `TAURI_FAMILY`,
// `TAURI_PLATFORM_VERSION`, `TAURI_PLATFORM_TYPE` and `TAURI_DEBUG`
// env variables
envPrefix: ['VITE_', 'TAURI_'],
build: {
// Tauri supports es2021
target: ['es2022', 'chrome100', 'safari13'],
// don't minify for debug builds
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
// produce sourcemaps for debug builds
sourcemap: !!process.env.TAURI_DEBUG,
}
})
)
);

View File

@ -1,7 +1,26 @@
import { defineConfig } from 'vite'
import solidPlugin from 'vite-plugin-solid'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [solidPlugin()]
})
export default defineConfig(async () => ({
plugins: [react()],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
// prevent vite from obscuring rust errors
clearScreen: false,
// tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true
},
// to make use of `TAURI_DEBUG` and other env variables
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
envPrefix: ['VITE_', 'TAURI_'],
build: {
// Tauri supports es2021
target: process.env.TAURI_PLATFORM == 'windows' ? 'chrome105' : 'safari13',
// don't minify for debug builds
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
// produce sourcemaps for debug builds
sourcemap: !!process.env.TAURI_DEBUG
}
}))