show all installed packages on homepage

This commit is contained in:
Tobias Merkle 2024-05-02 17:13:32 -04:00
parent 078a898053
commit e1bc001d1b
31 changed files with 550 additions and 241 deletions

View File

@ -14,8 +14,7 @@
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" />
<link href='https://fonts.googleapis.com/css?family=Montserrat' rel='stylesheet'>
<script type="module" crossorigin src="/main:app_store:sys/assets/index-mfHVbCij.js"></script>
<script type="module" crossorigin src="/main:app_store:sys/assets/index-BW3CPqe4.js"></script>
<link rel="stylesheet" crossorigin href="/main:app_store:sys/assets/index-McZbj3o1.css">
</head>

View File

@ -14,7 +14,6 @@
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" />
<link href='https://fonts.googleapis.com/css?family=Montserrat' rel='stylesheet'>
</head>
<body>

View File

@ -1,6 +1,5 @@
import React, { useEffect, useState } from "react";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import { ethers } from "ethers";
import { Web3ReactProvider, Web3ReactHooks } from '@web3-react/core';
import type { MetaMask } from '@web3-react/metamask'
@ -8,7 +7,7 @@ import { PackageStore, PackageStore__factory } from "./abis/types";
import StorePage from "./pages/StorePage";
import MyAppsPage from "./pages/MyAppsPage";
import AppPage from "./pages/AppPage";
import { MY_APPS_PATH } from "./constants/path";
import { APP_DETAILS_PATH, MY_APPS_PATH, PUBLISH_PATH, STORE_PATH } from "./constants/path";
import { ChainId, PACKAGE_STORE_ADDRESSES } from "./constants/chain";
import PublishPage from "./pages/PublishPage";
import { hooks as metaMaskHooks, metaMask } from './utils/metamask'
@ -115,10 +114,10 @@ function App() {
<Web3ReactProvider connectors={connectors}>
<Router basename={BASE_URL}>
<Routes>
<Route path="/" element={<StorePage />} />
<Route path={STORE_PATH} element={<StorePage />} />
<Route path={MY_APPS_PATH} element={<MyAppsPage />} />
<Route path="/app-details/:id" element={<AppPage />} />
<Route path="/publish" element={<PublishPage {...props} />} />
<Route path={`${APP_DETAILS_PATH}/:id`} element={<AppPage />} />
<Route path={PUBLISH_PATH} element={<PublishPage {...props} />} />
</Routes>
</Router>
</Web3ReactProvider>

View File

@ -3,7 +3,7 @@ import { AppInfo } from "../types/Apps";
import { appId } from "../utils/app";
import { useNavigate } from "react-router-dom";
import classNames from "classnames";
import { FaCircleQuestion } from "react-icons/fa6";
import { APP_DETAILS_PATH } from "../constants/path";
interface AppHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
app: AppInfo;
@ -21,7 +21,7 @@ export default function AppHeader({
<div
{...props}
className={classNames('flex w-full justify-content-start', size, props.className, { 'cursor-pointer': size !== 'large' })}
onClick={() => navigate(`/app-details/${appId(app)}`)}
onClick={() => navigate(`/${APP_DETAILS_PATH}/${appId(app)}`)}
>
{app.metadata?.image && <img
src={app.metadata.image}

View File

@ -5,6 +5,7 @@ import Dropdown from "./Dropdown";
import { AppInfo } from "../types/Apps";
import { appId } from "../utils/app";
import useAppsStore from "../store/apps-store";
import { APP_DETAILS_PATH } from "../constants/path";
interface MoreActionsProps extends React.HTMLAttributes<HTMLButtonElement> {
app: AppInfo;
@ -26,7 +27,7 @@ export default function MoreActions({ app, className }: MoreActionsProps) {
{app.metadata?.description && (
<button
className="my-1 whitespace-nowrap clear"
onClick={() => navigate(`/app-details/${appId(app)}`)}
onClick={() => navigate(`/${APP_DETAILS_PATH}/${appId(app)}`)}
>
View Details
</button>
@ -50,7 +51,7 @@ export default function MoreActions({ app, className }: MoreActionsProps) {
<div className="flex flex-col bg-black/50 p-2 rounded-lg">
<button
className="my-1 whitespace-nowrap clear"
onClick={() => navigate(`/app-details/${appId(app)}`)}
onClick={() => navigate(`/${APP_DETAILS_PATH}/${appId(app)}`)}
>
View Details
</button>

View File

@ -8,7 +8,7 @@ import {
FaX,
} from "react-icons/fa6";
import { MY_APPS_PATH } from "../constants/path";
import { MY_APPS_PATH, PUBLISH_PATH } from "../constants/path";
import classNames from "classnames";
import { FaHome } from "react-icons/fa";
@ -57,7 +57,7 @@ export default function SearchHeader({
)}
{!hidePublish && <button
className="flex flex-col c mr-2 icon"
onClick={() => navigate("/publish")}
onClick={() => navigate(PUBLISH_PATH)}
>
<FaUpload />
</button>}

View File

@ -1 +1,4 @@
export const MY_APPS_PATH = '/my-apps';
export const MY_APPS_PATH = '/';
export const STORE_PATH = '/store';
export const PUBLISH_PATH = '/publish';
export const APP_DETAILS_PATH = '/app-details';

View File

@ -8,6 +8,7 @@ import AppHeader from "../components/AppHeader";
import SearchHeader from "../components/SearchHeader";
import { PageProps } from "../types/Page";
import { appId } from "../utils/app";
import { PUBLISH_PATH } from "../constants/path";
interface AppPageProps extends PageProps { }
@ -35,7 +36,7 @@ export default function AppPage() {
}, [params.id]);
const goToPublish = useCallback(() => {
navigate("/publish", { state: { app } });
navigate(PUBLISH_PATH, { state: { app } });
}, [app, navigate]);
const version = useMemo(

View File

@ -8,8 +8,7 @@ import SearchHeader from "../components/SearchHeader";
import { PageProps } from "../types/Page";
import { useNavigate } from "react-router-dom";
import { appId } from "../utils/app";
interface MyAppsPageProps extends PageProps { }
import { PUBLISH_PATH } from "../constants/path";
export default function MyAppsPage() { // eslint-disable-line
const { myApps, getMyApps } = useAppsStore()
@ -57,7 +56,7 @@ export default function MyAppsPage() { // eslint-disable-line
<SearchHeader value={searchQuery} onChange={searchMyApps} />
<div className="flex justify-between items-center mt-2">
<h4 className="mb-2">My Packages</h4>
<button onClick={() => navigate('/publish')}>
<button onClick={() => navigate(PUBLISH_PATH)}>
<FaUpload className="mr-2" />
Publish Package
</button>

View File

@ -80,13 +80,6 @@ export default function StorePage() {
}
}, []);
// const viewDetails = useCallback(
// (app: AppInfo) => () => {
// navigate(`/app-details/${appId(app)}`);
// },
// [navigate]
// );
const searchApps = useCallback(
(query: string) => {
setSearchQuery(query);

View File

@ -1,27 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
// "strict": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"../src",
],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

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

View File

@ -5,10 +5,10 @@ use kinode_process_lib::{
bind_http_path, bind_http_static_path, send_response, serve_ui, HttpServerError,
HttpServerRequest, StatusCode,
},
println, Address, Message,
println, Address, Message, ProcessId,
};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::collections::{BTreeMap, HashMap};
/// The request format to add or remove an app from the homepage. You must have messaging
/// access to `homepage:homepage:sys` in order to perform this. Serialize using serde_json.
@ -21,6 +21,7 @@ enum HomepageRequest {
label: String,
icon: String,
path: String,
widget: Option<String>,
},
Remove,
}
@ -31,6 +32,17 @@ struct HomepageApp {
path: String,
label: String,
base64_icon: String,
is_favorite: bool,
widget: Option<String>,
}
fn get_package_id(url_params: &HashMap<String, String>) -> anyhow::Result<ProcessId> {
let Some(package_id) = url_params.get("id") else {
return Err(anyhow::anyhow!("Missing id"));
};
let id = package_id.parse::<ProcessId>()?;
Ok(id)
}
wit_bindgen::generate!({
@ -40,7 +52,7 @@ wit_bindgen::generate!({
call_init!(init);
fn init(our: Address) {
let mut app_data: BTreeMap<ProcessId, HomepageApp> = BTreeMap::new();
let mut app_data: BTreeMap<String, HomepageApp> = BTreeMap::new();
serve_ui(&our, "ui", true, false, vec!["/"]).expect("failed to serve ui");
@ -65,6 +77,7 @@ fn init(our: Address) {
.expect("failed to bind to /our.js");
bind_http_path("/apps", true, false).expect("failed to bind /apps");
bind_http_path("/apps/:id/favorite", true, false).expect("failed to bind /apps/:id/favorite");
loop {
let Ok(ref message) = await_message() else {
@ -84,7 +97,12 @@ fn init(our: Address) {
// they must have messaging access to us in order to perform this.
if let Ok(request) = serde_json::from_slice::<HomepageRequest>(message.body()) {
match request {
HomepageRequest::Add { label, icon, path } => {
HomepageRequest::Add {
label,
icon,
path,
widget,
} => {
app_data.insert(
message.source().process.to_string(),
HomepageApp {
@ -98,22 +116,24 @@ fn init(our: Address) {
),
label: label.clone(),
base64_icon: icon.clone(),
is_favorite: false,
widget: widget.clone(),
},
);
}
HomepageRequest::Remove => {
app_data.remove(&message.source().process);
app_data.remove(&message.source().process.to_string());
}
}
} else if let Ok(request) = serde_json::from_slice::<HttpServerRequest>(message.body())
{
match request {
} else if let Ok(req) = serde_json::from_slice::<HttpServerRequest>(message.body()) {
match req {
HttpServerRequest::Http(incoming) => {
let path = incoming.bound_path(None);
if path == "/apps" {
match path {
"/apps" => {
send_response(
StatusCode::OK,
Some(std::collections::HashMap::from([(
Some(HashMap::from([(
"Content-Type".to_string(),
"application/json".to_string(),
)])),
@ -128,7 +148,32 @@ fn init(our: Address) {
.as_bytes()
.to_vec(),
);
} else {
}
"/apps/:id/favorite" => {
let url_params = incoming.url_params();
let Ok(app_id) = get_package_id(url_params) else {
send_response(
StatusCode::BAD_REQUEST,
None,
format!("Missing id").into_bytes(),
);
println!("failed to parse app id");
for key in url_params {
println!("{} {}", key.0, key.1);
}
println!("All application IDs currently stored:");
for key in app_data.keys() {
println!("{}", key);
}
continue;
};
let app = app_data.get_mut(&app_id.to_string());
if let Some(app) = app {
app.is_favorite = !app.is_favorite;
}
send_response(StatusCode::CREATED, Some(HashMap::new()), vec![]);
}
_ => {
send_response(
StatusCode::OK,
Some(HashMap::new()),
@ -136,6 +181,7 @@ fn init(our: Address) {
);
}
}
}
_ => {}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -9,8 +9,8 @@
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" />
<script type="module" crossorigin src="/assets/index-zsBPcPK6.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-a1_1vktn.css">
<script type="module" crossorigin src="/assets/index-BSV0lkgx.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-v9ePqnS0.css">
</head>
<body>

View File

@ -9,8 +9,8 @@
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" />
<script type="module" crossorigin src="/assets/index-zsBPcPK6.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-a1_1vktn.css">
<script type="module" crossorigin src="/assets/index-BSV0lkgx.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-v9ePqnS0.css">
</head>
<body>

View File

@ -1,61 +0,0 @@
import { useEffect, useState } from 'react'
import KinodeText from './components/KinodeText'
import KinodeBird from './components/KinodeBird'
import useHomepageStore from './store/homepageStore'
import { FaV } from 'react-icons/fa6'
interface HomepageApp {
package_name: string,
path: string
label: string,
base64_icon: string,
}
function App() {
const [our, setOur] = useState('')
const [apps, setApps] = useState<HomepageApp[]>([])
const { isHosted, fetchHostedStatus } = useHomepageStore()
useEffect(() => {
fetch('/apps')
.then(res => res.json())
.then(data => setApps(data))
fetch('/our')
.then(res => res.text())
.then(data => setOur(data))
.then(() => fetchHostedStatus(our))
}, [our])
return (
<div className="flex-col-center relative h-screen w-screen overflow-hidden special-bg-homepage">
<h5 className='absolute top-8 left-8'>
{isHosted && <a
href={`https://${our.replace('.os', '')}.hosting.kinode.net/`}
className='button icon'
>
<FaV />
</a>}
{our}
</h5>
<div className="flex-col-center gap-6 mt-8 mx-0 mb-16">
<h3 className='text-center'>Welcome to</h3>
<KinodeText />
<KinodeBird />
</div>
<div className='flex-center flex-wrap gap-8'>
{apps.length === 0 ? <div>Loading apps...</div> : apps.map(app => <a
className="flex-col-center mb-8 cursor-pointer gap-2 hover:opacity-90"
id={app.package_name}
href={app.path}
>
<img
src={app.base64_icon}
className='h-32 w-32'
/>
<h6>{app.label}</h6>
</a>)}
</div>
</div>
)
}
export default App

View File

@ -0,0 +1,14 @@
import { HomepageApp } from "../store/homepageStore"
import AppDisplay from "./AppDisplay"
interface AllAppsProps {
apps: HomepageApp[]
}
const AllApps: React.FC<AllAppsProps> = ({ apps }) => {
return <div className='flex-center flex-wrap gap-8 overflow-y-auto flex-grow self-stretch px-16'>
{apps.length === 0 ? <div>Loading apps...</div> : apps.map(app => <AppDisplay app={app} />)}
</div>
}
export default AllApps

View File

@ -0,0 +1,49 @@
import classNames from "classnames"
import ColorDot from "./HexNum"
import useHomepageStore, { HomepageApp } from "../store/homepageStore"
import { FaHeart, FaRegHeart } from "react-icons/fa6"
import { useState } from "react"
interface AppDisplayProps {
app: HomepageApp
}
const AppDisplay: React.FC<AppDisplayProps> = ({ app }) => {
const { favoriteApp, apps, setApps } = useHomepageStore()
const [isHovered, setIsHovered] = useState(false)
const onAppFavorited = async (appId: string) => {
const res = await favoriteApp(appId.replace('/', ''))
if (res.status === 201) {
setApps(apps.map(a => a.package_name === app.package_name ? { ...app, is_favorite: !app.is_favorite } : a))
} else {
alert('Something went wrong. Please try again.')
}
}
return <a
className={classNames("flex-col-center gap-2 relative hover:opacity-90", { 'cursor-pointer': app.path })}
id={app.package_name}
href={app.path}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{app.base64_icon
? <img
src={app.base64_icon}
className='h-32 w-32 rounded'
/>
: <ColorDot num={app.state?.our_version || '0'} />}
<h6>{app.label}</h6>
{app.path && isHovered && <button
className="absolute p-2 -top-2 -right-2 clear text-sm saturate-50"
onClick={(e) => {
e.preventDefault()
onAppFavorited(app.path)
}}
>
{app.is_favorite ? <FaHeart /> : <FaRegHeart />}
</button>}
</a>
}
export default AppDisplay

View File

@ -0,0 +1,14 @@
import { HomepageApp } from "../store/homepageStore"
import AppDisplay from "./AppDisplay"
interface AppsDockProps {
apps: HomepageApp[]
}
const AppsDock: React.FC<AppsDockProps> = ({ apps }) => {
return <div className='flex-center flex-wrap obox border border-orange gap-8'>
{apps.length === 0 ? <div>Favorite an app to pin it to your dock.</div> : apps.map(app => <AppDisplay app={app} />)}
</div>
}
export default AppsDock

View File

@ -0,0 +1,44 @@
import classNames from 'classnames'
import React from 'react'
import { hexToRgb, hslToRgb, rgbToHex, rgbToHsl } from '../utils/colors'
interface ColorDotProps extends React.HTMLAttributes<HTMLSpanElement> {
num: string,
}
const ColorDot: React.FC<ColorDotProps> = ({
num,
...props
}) => {
num = (num || '').replace(/(0x|\.)/g, '')
while (num.length < 6) {
num = '0' + num
}
const leftHsl = rgbToHsl(hexToRgb(num.slice(0, 6)))
const rightHsl = rgbToHsl(hexToRgb(num.length > 6 ? num.slice(num.length - 6) : num))
leftHsl.s = rightHsl.s = 1
const leftColor = rgbToHex(hslToRgb(leftHsl))
const rightColor = rgbToHex(hslToRgb(rightHsl))
const angle = (parseInt(num, 16) % 360) || -45
return (
<div {...props} className={classNames('flex', props.className)}>
<div
className='m-0 align-self-center h-32 w-32 border border-8 rounded-full outline-black'
style={{
borderTopColor: leftColor,
borderRightColor: rightColor,
borderBottomColor: rightColor,
borderLeftColor: leftColor,
background: `linear-gradient(${angle}deg, ${leftColor} 0 50%, ${rightColor} 50% 100%)`,
filter: 'saturate(0.25)'
}} />
{props.children}
</div>
)
}
export default ColorDot

View File

@ -1,12 +1,19 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import Homepage from './pages/Homepage.tsx'
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import '@unocss/reset/tailwind.css'
import 'uno.css'
import './index.css'
import { Settings } from './pages/Settings.tsx'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
<BrowserRouter>
<Routes>
<Route path='/' element={<Homepage />} />
<Route path='/settings' element={<Settings />} />
</Routes>
</BrowserRouter>
</React.StrictMode>,
)

View File

@ -0,0 +1,86 @@
import { useEffect, useState } from 'react'
import KinodeText from '../components/KinodeText'
import KinodeBird from '../components/KinodeBird'
import useHomepageStore from '../store/homepageStore'
import { FaGear, FaV } from 'react-icons/fa6'
import AppsDock from '../components/AppsDock'
import AllApps from '../components/AllApps'
interface AppStoreApp {
package: string,
publisher: string,
state: {
our_version: string
}
}
function Homepage() {
const [our, setOur] = useState('')
const { apps, setApps, isHosted, fetchHostedStatus } = useHomepageStore()
useEffect(() => {
fetch('/apps')
.then(res => res.json())
.then(data => setApps(data))
.then(() => {
fetch('/main:app_store:sys/apps')
.then(res => res.json())
.then(data => {
const appz = [...apps]
data.forEach((app: AppStoreApp) => {
if (!appz.find(a => a.package_name === app.package)) {
appz.push({
package_name: app.package,
path: '',
label: app.package,
state: app.state,
is_favorite: false
})
} else {
const i = appz.findIndex(a => a.package_name === app.package)
if (i !== -1) {
appz[i] = { ...appz[i], state: app.state }
}
}
})
setApps(appz)
})
})
fetch('/our')
.then(res => res.text())
.then(data => {
if (data.match(/^[a-zA-Z0-9\-\.]+\.[a-zA-Z]+$/)) {
setOur(data)
fetchHostedStatus(data)
}
})
}, [our])
return (
<div className="flex-col-center relative h-screen w-screen overflow-hidden special-bg-homepage">
<h5 className='absolute top-8 left-8 right-8 flex gap-4 c'>
{isHosted && <a
href={`https://${our.replace('.os', '')}.hosting.kinode.net/`}
className='button icon'
>
<FaV />
</a>}
{our}
<a
href='/settings:settings:sys'
className='button icon ml-auto'
>
<FaGear />
</a>
</h5>
<div className="flex-col-center gap-6 mt-8 mx-0 mb-16">
<h3 className='text-center'>Welcome to</h3>
<KinodeText />
<KinodeBird />
</div>
<AppsDock apps={apps.filter(a => a.is_favorite)} />
<AllApps apps={apps} />
</div>
)
}
export default Homepage

View File

@ -0,0 +1,3 @@
export const Settings = () => <div>
Settings go here!
</div>

View File

@ -1,6 +1,16 @@
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
export interface HomepageApp {
package_name: string,
path: string
label: string,
base64_icon?: string,
is_favorite: boolean,
state?: {
our_version: string
}
}
export interface HomepageStore {
get: () => HomepageStore
@ -9,6 +19,10 @@ export interface HomepageStore {
isHosted: boolean
setIsHosted: (isHosted: boolean) => void
fetchHostedStatus: (our: string) => Promise<void>
favoriteApp: (appId: string) => Promise<Response>
apps: HomepageApp[]
setApps: (apps: HomepageApp[]) => void
}
const useHomepageStore = create<HomepageStore>()(
@ -17,6 +31,8 @@ const useHomepageStore = create<HomepageStore>()(
get,
set,
apps: [],
setApps: (apps: HomepageApp[]) => set({ apps }),
isHosted: false,
setIsHosted: (isHosted: boolean) => set({ isHosted }),
fetchHostedStatus: async (our: string) => {
@ -30,6 +46,7 @@ const useHomepageStore = create<HomepageStore>()(
set({ isHosted: hosted })
}
},
favoriteApp: async (appId: string) => await fetch(`/apps/${appId.replace('/', '')}/favorite`, { method: 'POST' }),
}),
{
name: 'homepage_store', // unique name

View File

@ -0,0 +1,106 @@
export type RgbType = { r: number, g: number, b: number }
export type HslType = { h: number, s: number, l: number }
export const rgbToHsl: (rgb: RgbType) => HslType = ({ r, g, b }) => {
r /= 255; g /= 255; b /= 255;
let max = Math.max(r, g, b);
let min = Math.min(r, g, b);
let d = max - min;
let h = 0;
if (d === 0) h = 0;
else if (max === r) h = ((((g - b) / d) % 6) + 6) % 6; // the javascript modulo operator handles negative numbers differently than most other languages
else if (max === g) h = (b - r) / d + 2;
else if (max === b) h = (r - g) / d + 4;
let l = (min + max) / 2;
let s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
return { h: h * 60, s, l };
}
export const hslToRgb: (hsl: HslType) => RgbType = ({ h, s, l }) => {
let c = (1 - Math.abs(2 * l - 1)) * s;
let hp = h / 60.0;
let x = c * (1 - Math.abs((hp % 2) - 1));
let rgb1 = [0, 0, 0];
if (isNaN(h)) rgb1 = [0, 0, 0];
else if (hp <= 1) rgb1 = [c, x, 0];
else if (hp <= 2) rgb1 = [x, c, 0];
else if (hp <= 3) rgb1 = [0, c, x];
else if (hp <= 4) rgb1 = [0, x, c];
else if (hp <= 5) rgb1 = [x, 0, c];
else if (hp <= 6) rgb1 = [c, 0, x];
let m = l - c * 0.5;
return {
r: Math.round(255 * (rgb1[0] + m)),
g: Math.round(255 * (rgb1[1] + m)),
b: Math.round(255 * (rgb1[2] + m))
};
}
export const hexToRgb: (hex: string) => RgbType = (hex) => {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : { r: 0, g: 0, b: 0 };
}
export const rgbToHex: (rgb: RgbType) => string = ({ r, g, b }) => {
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
export const generateDistinguishableColors = (numColors: number, mod?: number, sat?: number, val?: number) => {
const colors: string[] = [];
const saturation = sat || 0.75;
const value = val || 0.75;
for (let i = 0; i < numColors; i++) {
const hue = i / numColors * (+(mod || 1) || 1);
const [r, g, b] = hsvToRgb(hue, saturation, value);
const hexColor = rgbToHex2(r, g, b);
colors.push(hexColor);
}
return colors;
}
export const hsvToRgb = (h: number, s: number, v: number) => {
let [r, g, b] = [0, 0, 0];
const i = Math.floor(h * 6);
const f = h * 6 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0:
(r = v), (g = t), (b = p); // eslint-disable-line
break;
case 1:
(r = q), (g = v), (b = p); // eslint-disable-line
break;
case 2:
(r = p), (g = v), (b = t); // eslint-disable-line
break;
case 3:
(r = p), (g = q), (b = v); // eslint-disable-line
break;
case 4:
(r = t), (g = p), (b = v); // eslint-disable-line
break;
case 5:
(r = v), (g = p), (b = q); // eslint-disable-line
break;
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
export const componentToHex = (c: number) => {
const hex = c.toString(16);
return hex.length === 1 ? '0' + hex : hex;
}
export const rgbToHex2 = (r: number, g: number, b: number) => {
return '#' + componentToHex(r) + componentToHex(g) + componentToHex(b);
}

File diff suppressed because one or more lines are too long