additional ui feedback

This commit is contained in:
Tobias Merkle 2024-05-07 16:43:54 -04:00
parent df3c0bdbb3
commit 542a3362d4
19 changed files with 263 additions and 105 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -14,8 +14,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="/main:app_store:sys/assets/index-C8ofW7WS.js"></script>
<link rel="stylesheet" crossorigin href="/main:app_store:sys/assets/index-_Aqzph9X.css">
<script type="module" crossorigin src="/main:app_store:sys/assets/index-nztUlpw-.js"></script>
<link rel="stylesheet" crossorigin href="/main:app_store:sys/assets/index-zq6Um8Qt.css">
</head>
<body>

View File

@ -4,6 +4,7 @@ import { appId } from "../utils/app";
import { useNavigate } from "react-router-dom";
import classNames from "classnames";
import { APP_DETAILS_PATH } from "../constants/path";
import ColorDot from "./ColorDot";
interface AppHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
app: AppInfo;
@ -23,11 +24,17 @@ export default function AppHeader({
className={classNames('flex w-full justify-content-start', size, props.className, { 'cursor-pointer': size !== 'large' })}
onClick={() => navigate(`/${APP_DETAILS_PATH}/${appId(app)}`)}
>
{app.metadata?.image && <img
src={app.metadata.image}
alt="app icon"
className={classNames('mr-2', { 'h-32 rounded-md': size === 'large', 'h-12 rounded': size !== 'large' })}
/>}
{app.metadata?.image
? <img
src={app.metadata.image}
alt="app icon"
className={classNames('mr-2', { 'h-32 rounded-md': size === 'large', 'h-12 rounded': size !== 'large' })}
/>
: <ColorDot
num={app.metadata_hash}
dotSize={size}
className={classNames('mr-2')}
/>}
<div className="flex flex-col w-full">
<div
className={classNames("whitespace-nowrap overflow-hidden text-ellipsis", { 'text-3xl': size === 'large', })}

View File

@ -0,0 +1,56 @@
import classNames from 'classnames'
import React from 'react'
import { hexToRgb, hslToRgb, rgbToHex, rgbToHsl } from '../utils/colors'
import { isMobileCheck } from '../utils/dimensions'
interface ColorDotProps extends React.HTMLAttributes<HTMLSpanElement> {
num: string,
dotSize?: 'small' | 'medium' | 'large'
}
const ColorDot: React.FC<ColorDotProps> = ({
num,
dotSize,
...props
}) => {
const isMobile = isMobileCheck()
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={classNames('m-0 align-self-center border rounded-full outline-black', {
'h-20 w-20': !isMobile && dotSize === 'large',
'h-16 w-16': !isMobile && dotSize === 'medium',
'h-12 w-12': isMobile || dotSize === 'small',
'border-4': !isMobile,
'border-2': isMobile,
})}
style={{
borderTopColor: leftColor,
borderRightColor: rightColor,
borderBottomColor: rightColor,
borderLeftColor: leftColor,
background: `linear-gradient(${angle}deg, ${leftColor} 0 50%, ${rightColor} 50% 100%)`,
filter: 'saturate(0.25)',
opacity: '0.75'
}} />
{props.children}
</div>
)
}
export default ColorDot

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

View File

@ -0,0 +1 @@
export const isMobileCheck = () => window.innerWidth <= 600

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-BMzDi7wQ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-1PguYU6W.css">
<script type="module" crossorigin src="/assets/index-CiPfZ2kc.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C9o9YkgK.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-BMzDi7wQ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-1PguYU6W.css">
<script type="module" crossorigin src="/assets/index-CiPfZ2kc.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C9o9YkgK.css">
</head>
<body>

View File

@ -1,29 +1,20 @@
import classNames from "classnames"
import useHomepageStore, { HomepageApp } from "../store/homepageStore"
import useHomepageStore from "../store/homepageStore"
import { isMobileCheck } from "../utilities/dimensions"
import AppDisplay from "./AppDisplay"
import usePersistentStore from "../store/persistentStore"
import { useEffect, useState } from "react"
const AllApps: React.FC = () => {
const AllApps: React.FC<{ expanded: boolean }> = ({ expanded }) => {
const { apps } = useHomepageStore()
const { favoriteApps } = usePersistentStore()
const isMobile = isMobileCheck()
const [undockedApps, setUndockedApps] = useState<HomepageApp[]>([])
useEffect(() => {
setUndockedApps(apps.filter(a => !favoriteApps[a.package_name]))
}, [apps, favoriteApps])
return <div className={classNames('flex-center flex-wrap overflow-y-auto flex-grow self-stretch', {
'px-8 gap-4': isMobile,
'px-16 gap-8': !isMobile
return <div className={classNames('flex-center flex-wrap gap-4 overflow-y-auto self-stretch relative', {
'max-h-0': !expanded,
'p-8 max-h-[1000px]': expanded,
'placeholder': isMobile
})}>
{undockedApps.length === 0
? (apps && apps.length === 0)
? <div>Loading apps...</div>
: <></>
: undockedApps.map(app => <AppDisplay app={app} />)}
{apps.length === 0
? <div>Loading apps...</div>
: apps.map(app => <AppDisplay app={app} />)}
</div>
}

View File

@ -16,7 +16,7 @@ const AppDisplay: React.FC<AppDisplayProps> = ({ app }) => {
const isMobile = isMobileCheck()
return <a
className={classNames("flex-col-center gap-2 relative hover:opacity-90", {
className={classNames("flex-col-center gap-2 relative hover:opacity-90 transition-opacity", {
'cursor-pointer': app.path,
'pointer-events-none': !app.path,
})}
@ -29,8 +29,8 @@ const AppDisplay: React.FC<AppDisplayProps> = ({ app }) => {
? <img
src={app.base64_icon}
className={classNames('rounded', {
'h-12 w-12': isMobile,
'h-32 w-32': !isMobile
'h-8 w-8': isMobile,
'h-16 w-16': !isMobile
})}
/>
: <ColorDot num={app.state?.our_version || '0'} />}

View File

@ -31,8 +31,8 @@ const ColorDot: React.FC<ColorDotProps> = ({
<div {...props} className={classNames('flex', props.className)}>
<div
className={classNames('m-0 align-self-center border rounded-full outline-black', {
'h-32 w-32 border-8': !isMobile,
'h-12 w-12 border-3': isMobile
'h-16 w-16 border-4': !isMobile,
'h-8 w-8 border-2': isMobile
})}
style={{
borderTopColor: leftColor,
@ -40,7 +40,8 @@ const ColorDot: React.FC<ColorDotProps> = ({
borderBottomColor: rightColor,
borderLeftColor: leftColor,
background: `linear-gradient(${angle}deg, ${leftColor} 0 50%, ${rightColor} 50% 100%)`,
filter: 'saturate(0.25)'
filter: 'saturate(0.25)',
opacity: '0.75'
}} />
{props.children}
</div>

View File

@ -36,7 +36,7 @@ const Widget: React.FC<WidgetProps> = ({ package_name, widget, forceLarge }) =>
data-widget-code={widget}
/>
{isHovered && <button
className="absolute top-0 left-2 icon"
className="absolute top-0 left-0 icon"
onClick={() => toggleWidgetVisibility(package_name)}
>
{widgetSettings[package_name]?.hide ? <FaEye /> : <FaEyeSlash />}

View File

@ -1,39 +1,25 @@
import { FaGear } from "react-icons/fa6"
import useHomepageStore from "../store/homepageStore"
import Widget from "./Widget"
import WidgetsSettingsModal from "./WidgetsSettingsModal"
import usePersistentStore from "../store/persistentStore"
import { isMobileCheck } from "../utilities/dimensions"
import classNames from "classnames"
const Widgets = () => {
const { apps, showWidgetsSettings, setShowWidgetsSettings } = useHomepageStore()
const { apps } = useHomepageStore()
const { widgetSettings } = usePersistentStore();
const isMobile = isMobileCheck()
return <div className={classNames("flex-col-center flex-grow self-stretch relative", {
'm-4': !isMobile,
'm-2': isMobile
return <div className={classNames("flex-center flex-wrap flex-grow self-stretch", {
'gap-2 m-2': isMobile,
'gap-4 m-4': !isMobile
})}>
<button
className="icon ml-4 absolute top-0 right-4"
onClick={() => setShowWidgetsSettings(true)}
>
<FaGear />
</button>
<div className={classNames("flex-center flex-wrap flex-grow self-stretch", {
'gap-2': isMobile,
'gap-4': !isMobile
})}>
{apps
.filter(app => app.widget)
.map(({ widget, package_name }, _i, _appsWithWidgets) => !widgetSettings[package_name]?.hide && <Widget
package_name={package_name}
widget={widget!}
forceLarge={_appsWithWidgets.length === 1}
/>)}
</div>
{showWidgetsSettings && <WidgetsSettingsModal />}
{apps
.filter(app => app.widget)
.map(({ widget, package_name }, _i, _appsWithWidgets) => !widgetSettings[package_name]?.hide && <Widget
package_name={package_name}
widget={widget!}
forceLarge={_appsWithWidgets.length === 1}
/>)}
</div>
}

View File

@ -2,12 +2,13 @@ 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 { FaChevronDown, FaChevronUp, FaScrewdriverWrench, FaV } from 'react-icons/fa6'
import AppsDock from '../components/AppsDock'
import AllApps from '../components/AllApps'
import Widgets from '../components/Widgets'
import { isMobileCheck } from '../utilities/dimensions'
import classNames from 'classnames'
import WidgetsSettingsModal from '../components/WidgetsSettingsModal'
interface AppStoreApp {
package: string,
@ -18,7 +19,8 @@ interface AppStoreApp {
}
function Homepage() {
const [our, setOur] = useState('')
const { apps, setApps, isHosted, fetchHostedStatus } = useHomepageStore()
const [allAppsExpanded, setAllAppsExpanded] = useState(false)
const { apps, setApps, isHosted, fetchHostedStatus, showWidgetsSettings, setShowWidgetsSettings } = useHomepageStore()
const isMobile = isMobileCheck()
useEffect(() => {
@ -60,9 +62,7 @@ function Homepage() {
}, [our])
return (
<div className={classNames("flex-col-center relative w-screen overflow-hidden special-bg-homepage", {
'h-screen': !isMobile,
'min-h-screen': isMobile
<div className={classNames("flex-col-center relative w-screen overflow-hidden special-bg-homepage min-h-screen", {
})}>
<h5 className={classNames('absolute flex gap-4 c', {
'top-8 left-8 right-8': !isMobile,
@ -75,26 +75,36 @@ function Homepage() {
<FaV />
</a>}
{our}
<a
href='/settings:settings:sys'
className='button icon ml-auto'
<button
className="icon ml-auto"
onClick={() => setShowWidgetsSettings(true)}
>
<FaGear />
</a>
<FaScrewdriverWrench />
</button>
</h5>
{isMobile
? <div className='flex-center gap-4 p-8 mt-8 max-w-screen'>
<KinodeBird />
<KinodeText />
</div>
: <div className={classNames("flex-col-center mx-0 gap-6 mt-8 mb-16")}>
: <div className={classNames("flex-col-center mx-0 gap-4 mt-8 mb-4")}>
<h3 className='text-center'>Welcome to</h3>
<KinodeText />
<KinodeBird />
</div>}
<AppsDock />
<AllApps />
<Widgets />
<button
className={classNames("clear flex-center self-center", {
'-mb-1': !allAppsExpanded,
})}
onClick={() => setAllAppsExpanded(!allAppsExpanded)}
>
{allAppsExpanded ? <FaChevronDown /> : <FaChevronUp />}
<span className="ml-2">{allAppsExpanded ? 'Collapse' : 'All installed apps'}</span>
</button>
<AllApps expanded={allAppsExpanded} />
{showWidgetsSettings && <WidgetsSettingsModal />}
</div>
)
}