Compare commits
18 Commits
045cfb46ee
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bdd1cae5ae | |||
|
|
3af86d80c7 | ||
|
8093f563f9
|
|||
|
0db96869e1
|
|||
|
96d59c763d
|
|||
|
b70d768eee
|
|||
|
7fcbf1398a
|
|||
|
17d6ee234d
|
|||
|
ac4eaa83e0
|
|||
|
475e88bffb
|
|||
|
9edda5808f
|
|||
|
30cd1c2815
|
|||
|
ee95be1bb3
|
|||
|
9c46cdf9cf
|
|||
|
4e47111a4d
|
|||
|
9624eab798
|
|||
|
1a2bea01d1
|
|||
|
5db9d6b139
|
@@ -8,6 +8,7 @@ beszel.use("*", beszelAuth())
|
|||||||
|
|
||||||
interface BeszelSystemInfo {
|
interface BeszelSystemInfo {
|
||||||
name: string
|
name: string
|
||||||
|
status: "up" | "down"
|
||||||
info: {
|
info: {
|
||||||
cpu: number
|
cpu: number
|
||||||
ram: number
|
ram: number
|
||||||
@@ -17,6 +18,7 @@ interface BeszelSystemInfo {
|
|||||||
|
|
||||||
interface BeszelApiSystem {
|
interface BeszelApiSystem {
|
||||||
name: string
|
name: string
|
||||||
|
status: "up" | "down"
|
||||||
info: {
|
info: {
|
||||||
cpu: number
|
cpu: number
|
||||||
mp: number // memory percentage
|
mp: number // memory percentage
|
||||||
@@ -30,6 +32,7 @@ beszel.get("/systems", async (c) => {
|
|||||||
const token = c.get("beszelToken")
|
const token = c.get("beszelToken")
|
||||||
|
|
||||||
if (!beszelHost) {
|
if (!beszelHost) {
|
||||||
|
console.error("[Beszel API] BESZEL_HOST environment variable not set")
|
||||||
return c.json({ error: "BESZEL_HOST environment variable not set" }, 500)
|
return c.json({ error: "BESZEL_HOST environment variable not set" }, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,10 +43,17 @@ beszel.get("/systems", async (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text()
|
||||||
|
console.error(
|
||||||
|
`[Beszel API] Failed to fetch systems: ${response.status} ${response.statusText}`,
|
||||||
|
errorText ? `- ${errorText}` : "",
|
||||||
|
)
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
error: "Failed to fetch Beszel data",
|
error: "Failed to fetch Beszel data",
|
||||||
status: response.status,
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
status: response.status,
|
status: response.status,
|
||||||
@@ -56,6 +66,7 @@ beszel.get("/systems", async (c) => {
|
|||||||
|
|
||||||
const systems: BeszelSystemInfo[] = data.items.map((system) => ({
|
const systems: BeszelSystemInfo[] = data.items.map((system) => ({
|
||||||
name: system.name,
|
name: system.name,
|
||||||
|
status: system.status,
|
||||||
info: {
|
info: {
|
||||||
cpu: system.info.cpu,
|
cpu: system.info.cpu,
|
||||||
ram: system.info.mp,
|
ram: system.info.mp,
|
||||||
@@ -63,12 +74,15 @@ beszel.get("/systems", async (c) => {
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
console.log(`[Beszel API] Successfully fetched ${systems.length} systems`)
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
lastUpdated: new Date().toISOString(),
|
lastUpdated: new Date().toISOString(),
|
||||||
systems,
|
systems,
|
||||||
totalSystems: systems.length,
|
totalSystems: systems.length,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("[Beszel API] Internal server error:", error)
|
||||||
return c.json({ error: "Internal server error", message: String(error) }, 500)
|
return c.json({ error: "Internal server error", message: String(error) }, 500)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,12 +6,26 @@ interface BeszelAuthResponse {
|
|||||||
|
|
||||||
export function beszelAuth(): MiddlewareHandler {
|
export function beszelAuth(): MiddlewareHandler {
|
||||||
let cachedToken: string | null = null
|
let cachedToken: string | null = null
|
||||||
|
let tokenExpiry: number | null = null
|
||||||
|
|
||||||
|
// Token lifetime: 50 minutes (tokens typically expire after 1 hour, refresh before that)
|
||||||
|
const TOKEN_LIFETIME_MS = 50 * 60 * 1000
|
||||||
|
|
||||||
const authenticate = async (): Promise<string> => {
|
const authenticate = async (): Promise<string> => {
|
||||||
if (cachedToken) {
|
const now = Date.now()
|
||||||
|
|
||||||
|
// Return cached token if it exists and hasn't expired
|
||||||
|
if (cachedToken && tokenExpiry && now < tokenExpiry) {
|
||||||
return cachedToken
|
return cachedToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log re-authentication for debugging
|
||||||
|
if (cachedToken && tokenExpiry && now >= tokenExpiry) {
|
||||||
|
console.log("[Beszel Auth] Token expired, re-authenticating...")
|
||||||
|
} else {
|
||||||
|
console.log("[Beszel Auth] Initial authentication...")
|
||||||
|
}
|
||||||
|
|
||||||
const beszelHost = process.env.BESZEL_HOST
|
const beszelHost = process.env.BESZEL_HOST
|
||||||
const beszelEmail = process.env.BESZEL_EMAIL
|
const beszelEmail = process.env.BESZEL_EMAIL
|
||||||
const beszelPassword = process.env.BESZEL_PASSWORD
|
const beszelPassword = process.env.BESZEL_PASSWORD
|
||||||
@@ -34,11 +48,16 @@ export function beszelAuth(): MiddlewareHandler {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text()
|
||||||
|
console.error(`[Beszel Auth] Authentication failed: ${response.status} - ${errorText}`)
|
||||||
throw new Error(`Beszel authentication failed: ${response.status}`)
|
throw new Error(`Beszel authentication failed: ${response.status}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = (await response.json()) as BeszelAuthResponse
|
const data = (await response.json()) as BeszelAuthResponse
|
||||||
cachedToken = data.token
|
cachedToken = data.token
|
||||||
|
tokenExpiry = now + TOKEN_LIFETIME_MS
|
||||||
|
|
||||||
|
console.log(`[Beszel Auth] Authentication successful, token valid until ${new Date(tokenExpiry).toISOString()}`)
|
||||||
|
|
||||||
return cachedToken
|
return cachedToken
|
||||||
}
|
}
|
||||||
@@ -49,6 +68,7 @@ export function beszelAuth(): MiddlewareHandler {
|
|||||||
c.set("beszelToken", token)
|
c.set("beszelToken", token)
|
||||||
await next()
|
await next()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("[Beszel Auth] Middleware error:", error)
|
||||||
return c.json({ error: "Authentication failed", message: String(error) }, 500)
|
return c.json({ error: "Authentication failed", message: String(error) }, 500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
import { type JrpcRequest, type JrpcResponse, newJrpcRequestId } from "@eva/jrpc"
|
import { type JrpcRequest, type JrpcResponse, newJrpcRequestId } from "@eva/jrpc"
|
||||||
import { ZIGBEE_DEVICE, type ZigbeeDeviceName } from "@eva/zigbee"
|
import { ZIGBEE_DEVICE, type ZigbeeDeviceName } from "@eva/zigbee"
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import { useDrag } from "@use-gesture/react"
|
|
||||||
import Chart from "chart.js/auto"
|
import Chart from "chart.js/auto"
|
||||||
import { atom, useAtomValue, useSetAtom, useStore } from "jotai"
|
import { useStore } from "jotai"
|
||||||
import { useEffect, useLayoutEffect, useRef, useState } from "react"
|
import { useEffect, useLayoutEffect, useRef, useState } from "react"
|
||||||
import { beszelSystemsQuery } from "./beszel"
|
import { beszelSystemsQuery } from "./beszel"
|
||||||
import cn from "./components/lib/cn"
|
import cn from "./components/lib/cn"
|
||||||
|
import { Tile } from "./components/tile"
|
||||||
|
import { Kuromi } from "./kuromi"
|
||||||
|
import {
|
||||||
|
LightControlTile,
|
||||||
|
type LightSceneConfig,
|
||||||
|
LightSceneTile,
|
||||||
|
brightnessStepAtoms,
|
||||||
|
brightnessToStep,
|
||||||
|
stepToBrightness,
|
||||||
|
} from "./light-control"
|
||||||
import { StatusSeverity, TubeLine, formatLineName, tflDisruptionsQuery } from "./tfl"
|
import { StatusSeverity, TubeLine, formatLineName, tflDisruptionsQuery } from "./tfl"
|
||||||
|
import { useAutoTheme } from "./use-auto-theme"
|
||||||
import {
|
import {
|
||||||
DEFAULT_LATITUDE,
|
DEFAULT_LATITUDE,
|
||||||
DEFAULT_LONGITUDE,
|
DEFAULT_LONGITUDE,
|
||||||
@@ -17,40 +27,20 @@ import {
|
|||||||
weatherDescriptionQuery,
|
weatherDescriptionQuery,
|
||||||
} from "./weather"
|
} from "./weather"
|
||||||
|
|
||||||
const LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT = 44
|
const kuromi = `
|
||||||
|
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣀⣀⣀⢠⠋⠉⠉⠒⠲⢤⣀⣠⡀
|
||||||
// Store brightness as step (0-43) to match the 44 bars exactly
|
⠀⠀⠀⠀⠀⠀⣀⣀⣀⢀⡠⠖⠋⠉⠀⠀⠀⠀⠉⠉⠢⣄⠀⠀⠀⢀⠼⠤⠇
|
||||||
// Step 0 = OFF, Steps 1-43 map to bars 42-0
|
⠀⠀⠀⣀⠔⠊⠁⠀⢨⠏⠀⠀⠀⣠⣶⣶⣦⠀⠀⠀⠀⠀⠱⣄⡴⠃⠀⠀⠀⠀
|
||||||
const brightnessStepAtoms = atom({
|
⢸⣉⠿⣁⠀⠀⠀⢀⡇⠀⠀⠀⠀⢿⣽⣿⣼⡠⠤⢄⣀⠀⠀⢱⠀⠀⠀⠀⠀⠀
|
||||||
[ZIGBEE_DEVICE.deskLamp]: atom(0),
|
⠀⠀⠀⠀⠑⢦⡀⢸⠀⠀⠀⡠⠒⠒⠚⠛⠉⠀⢠⣀⡌⠳⡀⡌⠀⠀⠀⠀⠀⠀
|
||||||
[ZIGBEE_DEVICE.livingRoomFloorLamp]: atom(0),
|
⠀⠀⠀⠀⠀⠀⠉⠉⣆⠀⢰⠁⣀⣀⠀⠀⣀⠀⠈⡽⣧⢀⡷⠁⠀⠀⠀⠀⠀⠀
|
||||||
})
|
⠀⠀⠀⠀⠀⡤⢄⠀⠈⠢⣸⣄⢽⣞⡂⠀⠈⠁⣀⡜⠁⣩⡷⠿⠆⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⢯⣁⡸⠀⠀⠀⡬⣽⣿⡀⠙⣆⡸⠛⠠⢧⠀⡿⠯⠆⠀⠀⠀⠀⠀⠀
|
||||||
const intermediateBrightnessStepAtoms = atom({
|
⠀⠀⠀⠀⣀⡀⠀⠀⡤⠤⣵⠁⢸⣻⡤⠏⠀⠀⠀⠀⢹⠀⠀⠀⡊⠱⣀⠀⠀⠀
|
||||||
[ZIGBEE_DEVICE.deskLamp]: atom(-1),
|
⠀⠀⢀⠜⠀⢘⠀⠀⠱⠲⢜⣢⣤⣧⠀⠀⠀⠀⠀⢴⠇⠀⠀⠀⠧⠠⠜⠀⠀⠀
|
||||||
[ZIGBEE_DEVICE.livingRoomFloorLamp]: atom(-1),
|
⠀⠀⠘⠤⠤⠚⠀⠀⠀⠀⠀⠀⢸⠁⠁⠀⣀⠎⠀⠻⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
})
|
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠣⣀⣀⡴⠤⠄⠴⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
`
|
||||||
// Convert brightness (0-254) to step (0-43)
|
|
||||||
// Step 0 = brightness 0, steps 1-43 map to brightness 1-254
|
|
||||||
function brightnessToStep(brightness: number): number {
|
|
||||||
if (brightness === 0) return 0
|
|
||||||
// Map brightness 1-254 to steps 1-43
|
|
||||||
return Math.max(1, Math.round((brightness / 254) * (LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert step (0-43) to brightness (0-254)
|
|
||||||
// Step 0 = brightness 0, steps 1-43 map to brightness 1-254
|
|
||||||
function stepToBrightness(step: number): number {
|
|
||||||
if (step === 0) return 0
|
|
||||||
// Map steps 1-43 to brightness 1-254
|
|
||||||
return Math.max(1, Math.round((step / (LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)) * 254))
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEVICE_FRIENDLY_NAMES = {
|
|
||||||
[ZIGBEE_DEVICE.deskLamp]: "Desk Lamp",
|
|
||||||
[ZIGBEE_DEVICE.livingRoomFloorLamp]: "Floor Lamp",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:"
|
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:"
|
||||||
@@ -59,6 +49,8 @@ function App() {
|
|||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
|
useAutoTheme(DEFAULT_LATITUDE, DEFAULT_LONGITUDE)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const ws = websocket.current
|
const ws = websocket.current
|
||||||
|
|
||||||
@@ -117,6 +109,22 @@ function App() {
|
|||||||
ws.send(JSON.stringify(req))
|
ws.send(JSON.stringify(req))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setScene(scene: LightSceneConfig) {
|
||||||
|
const ws = websocket.current
|
||||||
|
for (const [deviceName, state] of Object.entries(scene.deviceStates)) {
|
||||||
|
const req: JrpcRequest<"setDeviceState"> = {
|
||||||
|
id: newJrpcRequestId(),
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "setDeviceState",
|
||||||
|
params: {
|
||||||
|
deviceName: deviceName as ZigbeeDeviceName,
|
||||||
|
state,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ws.send(JSON.stringify(req))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen bg-neutral-300 dark:bg-neutral-800 p-2 select-none">
|
<div className="h-screen bg-neutral-300 dark:bg-neutral-800 p-2 select-none">
|
||||||
<div className="w-full h-full grid grid-cols-4 grid-rows-5 gap-2 bg-neutral-300 text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300">
|
<div className="w-full h-full grid grid-cols-4 grid-rows-5 gap-2 bg-neutral-300 text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300">
|
||||||
@@ -128,40 +136,36 @@ function App() {
|
|||||||
<SystemTile className="row-start-2 row-span-1" systemName="helian" displayName="Helian" />
|
<SystemTile className="row-start-2 row-span-1" systemName="helian" displayName="Helian" />
|
||||||
<SystemTile className="row-start-2 row-span-1" systemName="akira" displayName="Akira" />
|
<SystemTile className="row-start-2 row-span-1" systemName="akira" displayName="Akira" />
|
||||||
|
|
||||||
|
<LightSceneTile
|
||||||
|
className="row-start-3 col-start-3 col-span-1 row-span-2"
|
||||||
|
onSceneChange={(scene) => {
|
||||||
|
setScene(scene)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<LightControlTile
|
<LightControlTile
|
||||||
className="row-start-3 col-start-3 col-span-1"
|
className="row-start-3 col-start-4 col-span-1"
|
||||||
deviceName={ZIGBEE_DEVICE.livingRoomFloorLamp}
|
deviceName={ZIGBEE_DEVICE.livingRoomFloorLamp}
|
||||||
onRequestBrightnessStepChange={(step) => {
|
onRequestBrightnessStepChange={(step) => {
|
||||||
setBrightnessStep(ZIGBEE_DEVICE.livingRoomFloorLamp, step)
|
setBrightnessStep(ZIGBEE_DEVICE.livingRoomFloorLamp, step)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<LightControlTile
|
<LightControlTile
|
||||||
className="row-start-3 col-start-4 col-span-1"
|
className="row-start-4 col-start-4 col-span-1"
|
||||||
deviceName={ZIGBEE_DEVICE.deskLamp}
|
deviceName={ZIGBEE_DEVICE.deskLamp}
|
||||||
onRequestBrightnessStepChange={(step) => {
|
onRequestBrightnessStepChange={(step) => {
|
||||||
setBrightnessStep(ZIGBEE_DEVICE.deskLamp, step)
|
setBrightnessStep(ZIGBEE_DEVICE.deskLamp, step)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tile className="row-start-4 col-span-2 row-span-3" />
|
<Tile className="row-start-5 col-start-3 col-span-2 row-span-1 flex items-center justify-center overflow-hidden">
|
||||||
|
<Kuromi />
|
||||||
|
</Tile>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Tile({ children, className }: { children?: React.ReactNode; className?: string }) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"relative rounded-xl bg-neutral-200 dark:bg-neutral-900 flex flex-col justify-end items-start",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DateTimeTile() {
|
function DateTimeTile() {
|
||||||
const [time, setTime] = useState(new Date())
|
const [time, setTime] = useState(new Date())
|
||||||
|
|
||||||
@@ -247,7 +251,11 @@ function WeatherTile() {
|
|||||||
const temperature = Math.round(currentWeather.temperature)
|
const temperature = Math.round(currentWeather.temperature)
|
||||||
const lowTemp = Math.round(dailyForecastData?.forecastDaily?.days[0].temperatureMin ?? 0)
|
const lowTemp = Math.round(dailyForecastData?.forecastDaily?.days[0].temperatureMin ?? 0)
|
||||||
const highTemp = Math.round(dailyForecastData?.forecastDaily?.days[0].temperatureMax ?? 0)
|
const highTemp = Math.round(dailyForecastData?.forecastDaily?.days[0].temperatureMax ?? 0)
|
||||||
const percentage = lowTemp && highTemp ? (temperature - lowTemp) / (highTemp - lowTemp) : 0
|
// Calculate percentage: handle case where lowTemp might be 0 (falsy) by checking for valid numbers
|
||||||
|
const tempRange = highTemp - lowTemp
|
||||||
|
const percentage = tempRange !== 0 && !Number.isNaN(tempRange)
|
||||||
|
? Math.max(0, Math.min(1, (temperature - lowTemp) / tempRange))
|
||||||
|
: 0
|
||||||
const highlightIndexStart = Math.floor((1 - percentage) * 23)
|
const highlightIndexStart = Math.floor((1 - percentage) * 23)
|
||||||
const WeatherIcon = getWeatherIcon(currentWeather.conditionCode)
|
const WeatherIcon = getWeatherIcon(currentWeather.conditionCode)
|
||||||
|
|
||||||
@@ -273,7 +281,7 @@ function WeatherTile() {
|
|||||||
L:{lowTemp}°
|
L:{lowTemp}°
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-2 flex-[1]">
|
<div className="flex flex-col space-y-2 flex-[2]">
|
||||||
{Array.from({ length: 24 }).map((_, index) => {
|
{Array.from({ length: 24 }).map((_, index) => {
|
||||||
if (index === highlightIndexStart) {
|
if (index === highlightIndexStart) {
|
||||||
return (
|
return (
|
||||||
@@ -311,9 +319,9 @@ function WeatherTile() {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col justify-start h-full space-y-2 flex-[2]">
|
<div className="flex flex-col justify-start h-full space-y-2 flex-[3]">
|
||||||
<p
|
<p
|
||||||
className={cn("text-3xl leading-none tracking-tight font-light", {
|
className={cn("text-3xl leading-none tracking-tight font-light pl-4", {
|
||||||
"text-red-400": errorWeatherDescription,
|
"text-red-400": errorWeatherDescription,
|
||||||
"animate-pulse": isLoadingWeatherDescription,
|
"animate-pulse": isLoadingWeatherDescription,
|
||||||
})}
|
})}
|
||||||
@@ -389,7 +397,7 @@ function TFLTile({ className }: { className?: string }) {
|
|||||||
return (
|
return (
|
||||||
<Tile
|
<Tile
|
||||||
className={cn(
|
className={cn(
|
||||||
"gap-x-1 pt-1 h-full col-span-2 row-span-1 grid grid-cols-[min-content_1fr] auto-rows-min overflow-y-auto",
|
"pt-1 h-full col-span-2 row-span-1 grid grid-cols-[min-content_1fr] auto-rows-min overflow-y-auto",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -439,7 +447,7 @@ function TFLDistruptionItem({ lineId, reason, severity }: { lineId: TubeLine; re
|
|||||||
lineStyleClass = "bg-purple-800"
|
lineStyleClass = "bg-purple-800"
|
||||||
break
|
break
|
||||||
case "northern":
|
case "northern":
|
||||||
lineStyleClass = "bg-black"
|
lineStyleClass = "bg-black text-neutral-200 dark:bg-neutral-200 dark:text-black"
|
||||||
break
|
break
|
||||||
case "piccadilly":
|
case "piccadilly":
|
||||||
lineStyleClass = "bg-blue-900"
|
lineStyleClass = "bg-blue-900"
|
||||||
@@ -512,7 +520,7 @@ function TFLDistruptionItem({ lineId, reason, severity }: { lineId: TubeLine; re
|
|||||||
<div className="h-full flex items-center justify-center px-2 py-0.5">
|
<div className="h-full flex items-center justify-center px-2 py-0.5">
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-neutral-200 text-xl uppercase font-bold w-full text-center px-1 rounded-lg",
|
"text-neutral-200 text-sm uppercase w-full text-center px-1 rounded-lg",
|
||||||
lineStyleClass,
|
lineStyleClass,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -548,10 +556,16 @@ function SystemTile({
|
|||||||
const onCanvasRef = (elem: HTMLCanvasElement | null) => {
|
const onCanvasRef = (elem: HTMLCanvasElement | null) => {
|
||||||
if (!elem || chartRef.current) return
|
if (!elem || chartRef.current) return
|
||||||
|
|
||||||
const fillGradient = elem?.getContext("2d")?.createLinearGradient(0, 0, 0, elem.height)
|
const cpuFillGradient = elem?.getContext("2d")?.createLinearGradient(0, 0, 0, elem.height)
|
||||||
fillGradient?.addColorStop(0, "#2dd4bf")
|
cpuFillGradient?.addColorStop(0, "#2dd4bf")
|
||||||
fillGradient?.addColorStop(0.5, "rgba(45, 212, 191, 0)")
|
cpuFillGradient?.addColorStop(0.5, "rgba(45, 212, 191, 0)")
|
||||||
fillGradient?.addColorStop(1, "rgba(45, 212, 191, 0)")
|
cpuFillGradient?.addColorStop(1, "rgba(45, 212, 191, 0)")
|
||||||
|
|
||||||
|
const ramFillGradient = elem?.getContext("2d")?.createLinearGradient(0, 0, 0, elem.height)
|
||||||
|
ramFillGradient?.addColorStop(0, "#a78bfa")
|
||||||
|
ramFillGradient?.addColorStop(0.5, "rgba(167, 139, 250, 0)")
|
||||||
|
ramFillGradient?.addColorStop(1, "rgba(167, 139, 250, 0)")
|
||||||
|
|
||||||
chartRef.current = new Chart(elem, {
|
chartRef.current = new Chart(elem, {
|
||||||
type: "line",
|
type: "line",
|
||||||
data: {
|
data: {
|
||||||
@@ -560,10 +574,17 @@ function SystemTile({
|
|||||||
{
|
{
|
||||||
data: Array.from({ length: 20 }, (_, __) => null),
|
data: Array.from({ length: 20 }, (_, __) => null),
|
||||||
fill: true,
|
fill: true,
|
||||||
backgroundColor: fillGradient,
|
backgroundColor: cpuFillGradient,
|
||||||
borderColor: "#2dd4bf",
|
borderColor: "#2dd4bf",
|
||||||
tension: 0.1,
|
tension: 0.1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
data: Array.from({ length: 20 }, (_, __) => null),
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: ramFillGradient,
|
||||||
|
borderColor: "#a78bfa",
|
||||||
|
tension: 0.1,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
@@ -590,19 +611,30 @@ function SystemTile({
|
|||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const cpu = beszelSystemsData?.info.cpu
|
const cpu = beszelSystemsData?.info.cpu
|
||||||
if (!chartRef.current || cpu === undefined) return
|
const ram = beszelSystemsData?.info.ram
|
||||||
|
if (!chartRef.current || cpu === undefined || ram === undefined) return
|
||||||
|
|
||||||
const dataset = chartRef.current.data.datasets[0]
|
const cpuDataset = chartRef.current.data.datasets[0]
|
||||||
|
const ramDataset = chartRef.current.data.datasets[1]
|
||||||
|
|
||||||
const nextData = Array.from({ length: 20 }, (_, i) => {
|
const nextCpuData = Array.from({ length: 20 }, (_, i) => {
|
||||||
if (i === 19) {
|
if (i === 19) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return dataset.data[i + 1]
|
return cpuDataset.data[i + 1]
|
||||||
})
|
})
|
||||||
nextData[19] = cpu
|
nextCpuData[19] = cpu
|
||||||
|
|
||||||
dataset.data = nextData
|
const nextRamData = Array.from({ length: 20 }, (_, i) => {
|
||||||
|
if (i === 19) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return ramDataset.data[i + 1]
|
||||||
|
})
|
||||||
|
nextRamData[19] = ram
|
||||||
|
|
||||||
|
cpuDataset.data = nextCpuData
|
||||||
|
ramDataset.data = nextRamData
|
||||||
chartRef.current.update()
|
chartRef.current.update()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -614,12 +646,36 @@ function SystemTile({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let systemStatusContent: React.ReactNode
|
||||||
|
switch (beszelSystemsData.status) {
|
||||||
|
case "up":
|
||||||
|
systemStatusContent = (
|
||||||
|
<div className="w-full flex-1 min-w-0 basis-0 relative mb-2">
|
||||||
|
<canvas ref={onCanvasRef} className="min-h-0 absolute top-0 left-0 w-full h-full" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "down":
|
||||||
|
systemStatusContent = (
|
||||||
|
<div className="w-full flex-1 flex items-center justify-center">
|
||||||
|
<p className="font-mono text-red-500 uppercase font-bold">System offline</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tile className={cn("h-full flex flex-col justify-start items-start", className)}>
|
<Tile className={cn("h-full flex flex-col justify-start items-start", className)}>
|
||||||
<div className="grid grid-cols-6 px-4 pt-3 w-full">
|
<div className="grid grid-cols-6 px-3 pt-2 w-full">
|
||||||
<div className="col-span-3 flex flex-row items-center space-x-2">
|
<div className="col-span-3 self-start flex flex-row items-center space-x-2">
|
||||||
<p className="text-2xl">{displayName}</p>
|
<p className="leading-none tracking-tight text-2xl">{displayName}</p>
|
||||||
<div className="size-2 border border-green-300 bg-green-500 rounded-full animate-pulse" />
|
<div
|
||||||
|
className={cn("size-2 border rounded-full", {
|
||||||
|
"animate-pulse border-green-300 bg-green-500": beszelSystemsData.status === "up",
|
||||||
|
"border-red-300 bg-red-500": beszelSystemsData.status === "down",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col font-mono">
|
<div className="flex flex-col font-mono">
|
||||||
<p className="text-neutral-400 text-right leading-none">CPU</p>
|
<p className="text-neutral-400 text-right leading-none">CPU</p>
|
||||||
@@ -634,235 +690,9 @@ function SystemTile({
|
|||||||
<p className="text-right">{beszelSystemsData.info.disk.toFixed(0).padStart(3, "0")}</p>
|
<p className="text-right">{beszelSystemsData.info.disk.toFixed(0).padStart(3, "0")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex-1 min-w-0 basis-0 relative mb-2">
|
{systemStatusContent}
|
||||||
<canvas ref={onCanvasRef} className="min-h-0 absolute top-0 left-0 w-full h-full" />
|
|
||||||
</div>
|
|
||||||
</Tile>
|
</Tile>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function LightControlTile({
|
|
||||||
deviceName,
|
|
||||||
className,
|
|
||||||
onRequestBrightnessStepChange,
|
|
||||||
}: { deviceName: ZigbeeDeviceName; className?: string; onRequestBrightnessStepChange: (step: number) => void }) {
|
|
||||||
const currentBrightnessStep = useAtomValue(useAtomValue(brightnessStepAtoms)[deviceName])
|
|
||||||
// Map step to bar index for thumb position
|
|
||||||
// Step 0 = OFF (no thumb shown, set to invalid index)
|
|
||||||
// Step 1-43 map to bars 42-0
|
|
||||||
const initialHighlightIndexStart =
|
|
||||||
currentBrightnessStep === 0
|
|
||||||
? LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1 // No thumb (index out of range, but no bars highlighted)
|
|
||||||
: LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1 - currentBrightnessStep
|
|
||||||
const touchContainerRef = useRef<HTMLDivElement | null>(null)
|
|
||||||
const barRefs = useRef<(HTMLDivElement | null)[]>(
|
|
||||||
Array.from({ length: LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT }, () => null),
|
|
||||||
)
|
|
||||||
const setIntermediateBrightnessStep = useSetAtom(useAtomValue(intermediateBrightnessStepAtoms)[deviceName])
|
|
||||||
const store = useStore()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const brightnessStepAtom = store.get(brightnessStepAtoms)[deviceName]
|
|
||||||
if (store.get(brightnessStepAtom) === currentBrightnessStep) {
|
|
||||||
setIntermediateBrightnessStep(-1)
|
|
||||||
}
|
|
||||||
}, [currentBrightnessStep, deviceName, setIntermediateBrightnessStep, store])
|
|
||||||
|
|
||||||
const bind = useDrag(({ xy: [x], first, last }) => {
|
|
||||||
if (!touchContainerRef.current) return
|
|
||||||
|
|
||||||
if (!first) {
|
|
||||||
touchContainerRef.current.dataset.active = "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (last) {
|
|
||||||
delete touchContainerRef.current.dataset.active
|
|
||||||
let thumbIndex = -1
|
|
||||||
for (let i = 0; i < LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT; i++) {
|
|
||||||
const bar = barRefs.current[i]
|
|
||||||
if (!bar) continue
|
|
||||||
|
|
||||||
const barRect = bar.getBoundingClientRect()
|
|
||||||
|
|
||||||
if (x >= barRect.left - 2 && x < barRect.right + 2 && thumbIndex === -1) {
|
|
||||||
thumbIndex = i
|
|
||||||
bar.dataset.thumb = "true"
|
|
||||||
} else {
|
|
||||||
delete bar.dataset.thumb
|
|
||||||
}
|
|
||||||
|
|
||||||
delete bar.dataset.touched
|
|
||||||
delete bar.dataset.touchProximity
|
|
||||||
}
|
|
||||||
|
|
||||||
if (thumbIndex !== -1) {
|
|
||||||
// Map bar index to step: bar 42 -> step 1, bar 0 -> step 43
|
|
||||||
const step = LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1 - thumbIndex
|
|
||||||
onRequestBrightnessStepChange(step)
|
|
||||||
} else {
|
|
||||||
const firstElement = barRefs.current[barRefs.current.length - 1]
|
|
||||||
const lastElement = barRefs.current[0]
|
|
||||||
if (lastElement && x > lastElement.getBoundingClientRect().right) {
|
|
||||||
lastElement.dataset.thumb = "true"
|
|
||||||
setIntermediateBrightnessStep(LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)
|
|
||||||
if (last) {
|
|
||||||
onRequestBrightnessStepChange(LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)
|
|
||||||
}
|
|
||||||
} else if (firstElement && x < firstElement.getBoundingClientRect().left) {
|
|
||||||
firstElement.dataset.thumb = "true"
|
|
||||||
setIntermediateBrightnessStep(0)
|
|
||||||
if (last) {
|
|
||||||
onRequestBrightnessStepChange(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let touchedIndex = -1
|
|
||||||
for (let i = 0; i < LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT; i++) {
|
|
||||||
const bar = barRefs.current[i]
|
|
||||||
if (!bar) continue
|
|
||||||
|
|
||||||
const barRect = bar.getBoundingClientRect()
|
|
||||||
|
|
||||||
delete bar.dataset.thumb
|
|
||||||
|
|
||||||
if (x >= barRect.left - 2 && x < barRect.right + 2 && touchedIndex === -1) {
|
|
||||||
touchedIndex = i
|
|
||||||
|
|
||||||
bar.dataset.touched = "true"
|
|
||||||
bar.dataset.highlighted = "false"
|
|
||||||
delete bar.dataset.touchProximity
|
|
||||||
|
|
||||||
const step = LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - i - 1
|
|
||||||
setIntermediateBrightnessStep(step)
|
|
||||||
|
|
||||||
if (barRefs.current[i - 1]) {
|
|
||||||
barRefs.current[i - 1]!.dataset.touchProximity = "close"
|
|
||||||
}
|
|
||||||
if (barRefs.current[i - 2]) {
|
|
||||||
barRefs.current[i - 2]!.dataset.touchProximity = "medium"
|
|
||||||
}
|
|
||||||
if (barRefs.current[i - 3]) {
|
|
||||||
barRefs.current[i - 3]!.dataset.touchProximity = "far"
|
|
||||||
}
|
|
||||||
} else if (barRect.left < x) {
|
|
||||||
if (bar.dataset.touched === "true") {
|
|
||||||
bar.dataset.prevTouched = "true"
|
|
||||||
} else {
|
|
||||||
delete bar.dataset.prevTouched
|
|
||||||
}
|
|
||||||
bar.dataset.touched = "false"
|
|
||||||
bar.dataset.highlighted = "true"
|
|
||||||
if (touchedIndex >= 0) {
|
|
||||||
const diff = i - touchedIndex
|
|
||||||
if (diff === 1) {
|
|
||||||
bar.dataset.touchProximity = "close"
|
|
||||||
} else if (diff === 2) {
|
|
||||||
bar.dataset.touchProximity = "medium"
|
|
||||||
} else if (diff === 3) {
|
|
||||||
bar.dataset.touchProximity = "far"
|
|
||||||
} else {
|
|
||||||
delete bar.dataset.touchProximity
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delete bar.dataset.touchProximity
|
|
||||||
}
|
|
||||||
} else if (barRect.right > x) {
|
|
||||||
bar.dataset.highlighted = "false"
|
|
||||||
bar.dataset.touched = "false"
|
|
||||||
if (touchedIndex >= 0) {
|
|
||||||
const diff = i - touchedIndex
|
|
||||||
if (diff === 1) {
|
|
||||||
bar.dataset.touchProximity = "close"
|
|
||||||
} else if (diff === 2) {
|
|
||||||
bar.dataset.touchProximity = "medium"
|
|
||||||
} else if (diff === 3) {
|
|
||||||
bar.dataset.touchProximity = "far"
|
|
||||||
} else {
|
|
||||||
delete bar.dataset.touchProximity
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delete bar.dataset.touchProximity
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bar.dataset.touched = "false"
|
|
||||||
bar.dataset.highlighted = "false"
|
|
||||||
delete bar.dataset.touchProximity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (touchedIndex === -1) {
|
|
||||||
const firstElement = barRefs.current[barRefs.current.length - 1]
|
|
||||||
const lastElement = barRefs.current[0]
|
|
||||||
if (lastElement && x > lastElement.getBoundingClientRect().right) {
|
|
||||||
lastElement.dataset.thumb = "true"
|
|
||||||
setIntermediateBrightnessStep(LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)
|
|
||||||
} else if (firstElement && x < firstElement.getBoundingClientRect().left) {
|
|
||||||
firstElement.dataset.thumb = "true"
|
|
||||||
setIntermediateBrightnessStep(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tile className={cn("h-full flex flex-col justify-start items-start", className)}>
|
|
||||||
<div
|
|
||||||
{...bind()}
|
|
||||||
ref={touchContainerRef}
|
|
||||||
className="group flex-1 flex flex-row-reverse justify-center items-center touch-none gap-x-1 w-full translate-y-6"
|
|
||||||
>
|
|
||||||
{Array.from({ length: LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT }).map((_, index) => {
|
|
||||||
const highlighted = index > initialHighlightIndexStart
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-highlighted={highlighted}
|
|
||||||
data-thumb={index === initialHighlightIndexStart}
|
|
||||||
data-prev-touched={false}
|
|
||||||
data-touched={false}
|
|
||||||
ref={(ref) => {
|
|
||||||
barRefs.current[index] = ref
|
|
||||||
}}
|
|
||||||
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
|
||||||
key={index}
|
|
||||||
className="transition-all transition-75 w-[2px] h-[2px] bg-neutral-400 rounded-full data-[highlighted=true]:h-2 data-[touch-proximity=close]:h-6 data-[touch-proximity=medium]:h-4 data-[touch-proximity=far]:h-2 data-[highlighted=true]:bg-teal-500 data-[touched=true]:h-8 data-[touched=true]:w-1 data-[touched=true]:bg-teal-500 data-[touched=true]:transition-none data-[prev-touched=true]:transition-none data-[thumb=true]:h-8 data-[thumb=true]:bg-teal-500"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="px-4 pb-2 w-full flex flex-row items-center justify-center space-x-2">
|
|
||||||
<p className="tracking-tigher uppercase">{DEVICE_FRIENDLY_NAMES[deviceName]}</p>
|
|
||||||
<BrightnessLevelLabel deviceName={deviceName} />
|
|
||||||
</div>
|
|
||||||
</Tile>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BrightnessLevelLabel({ deviceName }: { deviceName: ZigbeeDeviceName }) {
|
|
||||||
const currentBrightnessStep = useAtomValue(useAtomValue(brightnessStepAtoms)[deviceName])
|
|
||||||
const intermediateBrightnessStep = useAtomValue(useAtomValue(intermediateBrightnessStepAtoms)[deviceName])
|
|
||||||
|
|
||||||
const step = intermediateBrightnessStep === -1 ? currentBrightnessStep : intermediateBrightnessStep
|
|
||||||
|
|
||||||
let label: string
|
|
||||||
if (step === 0) {
|
|
||||||
label = "OFF"
|
|
||||||
} else {
|
|
||||||
// Convert step to percentage: step 1 = ~2%, step 43 = 100%
|
|
||||||
const brightnessPercentage = Math.round((step / (LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)) * 100)
|
|
||||||
label = `${brightnessPercentage}%`
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<p
|
|
||||||
className={cn(
|
|
||||||
"flex-1 text-right font-bold font-mono tracking-tigher",
|
|
||||||
step === 0 ? "text-neutral-400" : "text-teal-400",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
|||||||
550
apps/dashboard/src/assets/kuromi-frames.json
Normal file
550
apps/dashboard/src/assets/kuromi-frames.json
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
[
|
||||||
|
[
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" #*++*# ",
|
||||||
|
" *=.....= ",
|
||||||
|
" *-.......= ",
|
||||||
|
" %*.........* ",
|
||||||
|
" #-.........* ",
|
||||||
|
" #..........+ ",
|
||||||
|
" #..........* ",
|
||||||
|
" *+.........* ",
|
||||||
|
" %*........+# ",
|
||||||
|
" #*......=# ",
|
||||||
|
" #*+::=**# ",
|
||||||
|
" @%******# ",
|
||||||
|
" *******# ",
|
||||||
|
" %********# ",
|
||||||
|
" **********# ",
|
||||||
|
" ****..-****# ",
|
||||||
|
" #***-....+***# ",
|
||||||
|
" #***......=***# ",
|
||||||
|
" ***-.......:***# ",
|
||||||
|
" @***..........***% ",
|
||||||
|
" #**-...........*** ",
|
||||||
|
" ***.............+** ",
|
||||||
|
" **+..............=** ",
|
||||||
|
" %**................-** ",
|
||||||
|
" ***.................:** ",
|
||||||
|
" **-..................:** ",
|
||||||
|
" %**.....................** ",
|
||||||
|
" #*+.......:..............** ",
|
||||||
|
" **........-...............** ",
|
||||||
|
" **........=..-.............*# ",
|
||||||
|
" %*=........+.+...............+# ##*#% ",
|
||||||
|
" #*.......-+**.................+% *:...=*% ",
|
||||||
|
" *+.....:-+***-:................+# =.......*% ",
|
||||||
|
" *-........**+...................*% #........:* ",
|
||||||
|
" %*........+:*-:...................** :.........* ",
|
||||||
|
" *+.......=..=.:....................* ..........+# ",
|
||||||
|
" *:......:...-.......................* :.........+# ",
|
||||||
|
" %*...........:.......................-# %##+.........*@ ",
|
||||||
|
" %+.......:...:........................=% ###****+=--:*.........* ",
|
||||||
|
" *........:.............................+% *:---...............:*.......*% ",
|
||||||
|
" *........=..............................* +-.........................-*:...=*% ",
|
||||||
|
" +.......=+.......................:-=+++******##% @#****-:.............................:*#***# ",
|
||||||
|
" #:.....:-+++:................-+**+++==-----::--=+****#% %**+=...................................*% ",
|
||||||
|
" #.........+..............-**+--------------::.......:+***%% **.......................................* ",
|
||||||
|
" *.........:...........-*+::----------------::...........:+**% @*.......................................=# ",
|
||||||
|
" *.........:.........=+:.--------------------:..............:+*## #+.......................................* ",
|
||||||
|
" +.................+=.-----------------------:.................-**% *.......................................-* ",
|
||||||
|
" -................:.-------------------------::..................:** *.......................................*# ",
|
||||||
|
" #-...................:-----------------------::.....................*+.......................................* ",
|
||||||
|
" %:.....................::---------------------::....................:-......................................+# ",
|
||||||
|
" *........................:-------------------::....................=.......................................* ",
|
||||||
|
" *:.......+................::-----------------::...................+......................................+* ",
|
||||||
|
" #=.....+..................::=+++=-----------::...................+.....................................:* ",
|
||||||
|
" %*..:+..................=**#%%%#**+---------::........................................................+* ",
|
||||||
|
" #=+.................-*#%%%%%%%%%%*=-------:::......................................................=*% ",
|
||||||
|
" #+.................+*%%%%%%%%%%%%%%*-------::......................................................+* ",
|
||||||
|
" %*.................+*%%%%%%%%%%%%%%%%#------:::....................................:..:............+*# ",
|
||||||
|
" *.................+*%%%%%%%%%%%%%%%%%%*------:::................................-..-.-............:+* ",
|
||||||
|
" *:.................*%%%%%%%%%%%%%%%%%%%%+-----::::................................+.+=.............++% ",
|
||||||
|
" #=.................*%%%%%%%%%%%%%%%%%%%%%%=-----:::.................................**+............=+* ",
|
||||||
|
" #*..................*%%%%%%%%%%%%%%%%%%%%%%*------:::.............................-=****:...........++# ",
|
||||||
|
" *:.................:#%%%#%%%%%%%%%%%%%%%%%%*------::::...............................+*+...........++* ",
|
||||||
|
" #+..................=%%%=.#%%%%%%%%%%%%%%%%%#+::----::::.............................+.*.:.........=++# ",
|
||||||
|
" *...................=%%%...#%%%%%%%%%%%%%%%%%+:::----::::...........................-..+..........:++*% ",
|
||||||
|
" #+...................:%%=....%%%%%%%%%%%%%%%%%=.:::----::::.............................=..........+++# ",
|
||||||
|
" *.....................#%:....%%%%%%%%%%%%%%%%%...:::---:::::............................:.........+++* ",
|
||||||
|
" #*.....................*%*...%%%%%%%%%#=..%%%%%....:::---:::::...........................:........=+++# ",
|
||||||
|
" *:.....................:%%*=%%%%%%%%%%....-%%%%.....::::--::::::.........................:.......:++++ ",
|
||||||
|
" *.......................*%%%%%%%%%%%%#.....%%%*......::::--::::::................................++++# ",
|
||||||
|
" #+........................*%%%%%%%%%%%%....:%%%:.......::::--::::::..............................+++++ ",
|
||||||
|
" *:.........................*%%%%%%%%%%%:...%%%*.........::::--::::::............................+++++# ",
|
||||||
|
" *..............=**=........+%%%=%%%%%%%%#+%%%#:..........::::--:::::::.........................++++++ ",
|
||||||
|
" *...........:*@@@@@*-......*%%=:%.%%%%%%%%%%#=............::::--::::::::......................++++++* ##*% ",
|
||||||
|
" %=.........*@@@@@@@@@*:.....+*#.#%.%%%%%%%%%*=..............::::--::::::::....................+++++++% #**-.# ",
|
||||||
|
" #-.......*@@@@@@@@@@@@*:......-***.%%#*****+.................:::---:::::::::................-+++++++*#**=....* ",
|
||||||
|
" *......:*@@@@+@@@@@@@@@*-........+**%*.......................::::---::::::::::............-++++++++**+.......+ ",
|
||||||
|
" *......*@@@@@+#@@@@@@@@@*:.........:+*........................::::----::::::::::........+*+++++++++*.........- ",
|
||||||
|
" *.....*@@@@#@%=@@@@@@@@@@*:......................:+**=.........::::----:::::::::::........-+**++++*+..........# ",
|
||||||
|
" *....-@@@@@#++==@@@@@@@@@@*:..................=*#@@@@@**:.......::::----:::::::::::::........*#***##*:........+ ",
|
||||||
|
" *....@@@@@@@====+@@@@@@@@@@*:..............=@@@@@@@@@@@@*+......:::::-----:::=::::::::::.....* #*........- ",
|
||||||
|
" *...=@@@@@@@=++==+@@@@@@@@@@*:.......:=*%@@@@@@@@@@@@@@@@%*......::::------::+:::::::::::::::* *.........**# ",
|
||||||
|
" *...%@@@@@@+#@@%==*@@@@@@@@@@**=-:+**%@@@@@@@@@@@@@@@@@@@@@*:.....::::-------+:::::::::::::::* *.....*:..%@#* ",
|
||||||
|
" *...@@@@@@@+#*=====#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*-....:::::------+:::::::::::::::* %-.....**+.*@@%* ",
|
||||||
|
" *..-@@@@@@#+@*++====#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*:....::::-----+*+-:::::::::::::* *.....+%@@**@@@* ",
|
||||||
|
" *..=@@@@@@+++++++++==@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*....:::::--+=-*-=+::::::::::::* @=.....*@@@@#@@@*% ",
|
||||||
|
" *..+@@@@@@++++++++++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*....:::::---+*+-==-::::::::::* #*@*.....-*@@@@@@@@*% ",
|
||||||
|
" #..+@@@@@@++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+#@@@@%+....::::*-=***+-*---::::::::***%@#......*@@@@@@@@@* ",
|
||||||
|
" ..+@@@@@@++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%++@@@@@@*:...:::-+********=----::::::*%@@+......=#@@@@@@@@@* ",
|
||||||
|
" ..=@@@@@@*++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+===@%@@@@@@*....:::+-*****=*-------:::.*#+........*@@@@@@@@@%* ",
|
||||||
|
" +.-@@@@@@@+++++++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*======%+@@@@@@#=...:::+:-***--+---------::+.........*@@@@@@@@@@* ",
|
||||||
|
" #..%@@@@@@%**+++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#======+==+%@@@@@@@*...::::+--*=-*-----------==.......-+@@@@@@@@@@@* ",
|
||||||
|
" -.*@@@@@@@@***@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*++===+%@+==@@@@@@@@#:...::::*+*+*------------*+++++++++@@@@@@@@@@@* ",
|
||||||
|
" *.*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*+++++++@===@@@@@@@@@*...:::::-*--------------*%++++++#@@@@@@@@@@@*# ",
|
||||||
|
" #:=@@@@@@@@@@@@@@@@@@@@@@%%@@@@@@@@@@@@@@@@@*++++++++@++@@@@@@@@@*....:::::+--------------*@@*+#%@@@@@@@@@@@@#* ",
|
||||||
|
" *.*@@@@@@@@@@@@@@@@@@@@*##*@@@@@@@@@@@@@@@@#+++++++++++@@@@@@@@@#:...::=::+-------------+%@@@*#@@@@@@@@@@@@#* ",
|
||||||
|
" @=*@@@@@@@@@@@@@@@@@@@*#%%%%@@@@@@@@@@@@@@@%+++++++++++@@@@@@@@@@+...::=::+-------------*@@@@@#*@@@@@@@@@@*# ",
|
||||||
|
" *-#@@@@@@@@@@@@@@@@@@@*%%%#@@@@@@@@@@@@@@@@++++++++++%@@@@@@@@@@+....:#:::------------=#@@@@@@#*@@@@@@@@@* ",
|
||||||
|
" **@@@@@@@@@@@@@@@@@@@#*###@@@@@@@@@@@@@@@@++++++++++@@@@@@@@@@@*....=%#:::-----------*@@@@@@@@#*@@@@@@@@* ",
|
||||||
|
" #*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%++++++++*@@@@@@@@@@@*....#%%-::----------=#@@@@@@@@@#*@@@@@@%% ",
|
||||||
|
" #*@@@@@@@@@@@@@@@@@@@@@@@@@#*#@@@@@@@@@@@@**++++++@@@@@@@@@@@@*.....#-:::----------*%@@@@@@@@@@#*@@@@@*% ",
|
||||||
|
" **@@@@@@@@@@@@@@@*@@@@@@@%*%##@@@@@@@@@@@@%*****@@@@@@@@@@@@@*.....+:::::--------+#@@@@@@@@@@@@%*%@@@* ",
|
||||||
|
" **@@@@@@@@@@@@@@@*@@@@@@*%#%#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+.....=:::::-------=*@@@@@@@@@@@@@@@*#@%% ****% ",
|
||||||
|
" #**##**#@@@@@@@@@@**%@@*%###@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@=.....::::::-------*%@@@@@@@@@@@@@@@@%**%@@%@@@@**@ ",
|
||||||
|
" #*@@@@@@%*%@@@@@@@@@@#*****#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@:......::::::-----*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*# ",
|
||||||
|
" @*@@@@@@@@@*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#.......::::::----*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" #%@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*.......::::::---+%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" *@@@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%........:::::::-*%@@%#*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||||
|
" *@@@@@@@@@@@@@%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@=........:::::::*%@@%*####@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||||
|
" #%@@@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#.........:::::=*#@@@*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||||
|
" *@@@@@@@@@@@@#*%@@@@@@@@@@@@@@@@@@@@@@@*****#@@@@@@@@@@@@%...........:::*+..=*#*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||||
|
" %#@@@@@@@@@@@%@#*#@@@@@@@@@@@@@@@@@@@**@@@@@%*%@@@@@@@@@%............:-+-......*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||||
|
" ##@@@@@@@@@@@@@@%**%@@@@@@@@@@@@@@@*%@@@@@@@@*%@@@@@@@#:..........=+++........*#####*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||||
|
" %*@@@@@@@@@@@@@@@****#@@@@@@@@@@@*@@@@@@@@@@@*@@@@@%*.........-*+++:........*%*###*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||||
|
" %*% @@@@*+++++*#@@@@@@@*%@@@@@@@@@@@*@@@%*:......-+*++++-........+#@@@***@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" %*#@ @*+++++++++++**#%*@@@@@@@@@@@@*%**...:=+***+++++-.......-*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||||
|
" @% **###% *-=+++++++++++*@*@@@@@@@@@@@@@********+++++****++==++**%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||||
|
" *#*######*:....:=++++**%@@*@@@@@@@@@@@@@*+++++++++++=*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" %*######*.......-+**%@@@@@*@@@@@@@@@@@@#+++++++++-...#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" *######********##%*%@@@@@*@@@@@@@@@@@@*+==-........-%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" %*#####* #* #*@@@@*@@@@@@@@@@@**............=@@@@@@@@@@@@@@@@@@@@@@@@#%@@@@@@@@@@@@@@@@@#@@@@@@@@@@@%# ",
|
||||||
|
" **###*@ %** *#@@#@@@@@@@@@%*@#*...........=@@@@@@@@@@@@@@@@@@@@@@%* #*%%@@@@@@@@@%%**% *%@@@@@@@@@*% ",
|
||||||
|
" #***#***# #*#@*@@@@@@@@@@@@#+..........=@@@@@@@@@@@@@@@@@@@@@## #***********# %*%@@@@@@@%* ",
|
||||||
|
" %%# %**@@@@@@@@@@@@@#*.........:%@@@@@@@@@@@@@@@@@@%*@ #**# **%@@@@#*@ ",
|
||||||
|
" %*@@@@@@@@@@@@@@*-........*@@@@@@@@@@@@@@@@%*% %****** ",
|
||||||
|
" #*@@@@@@@@@@@@@@**:......*#*#@@@@@@@@@@%**# ",
|
||||||
|
" #*@@@@@@@@@@@@@@%**-...:*####@@@@@%#**# ",
|
||||||
|
" #*@@@@@@@@@@@@@@@@#**+*#####*****#@ ",
|
||||||
|
" #**@@@@@@@@@@@@@%*****######% ",
|
||||||
|
" #***#%%@@%##*## #*###### ",
|
||||||
|
" %##**#%@ *#####* ",
|
||||||
|
" %*###*# ",
|
||||||
|
" %***# "
|
||||||
|
],
|
||||||
|
[
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" #**#@ ",
|
||||||
|
" #*....:+ ",
|
||||||
|
" #+.......= ",
|
||||||
|
" *.........@ ",
|
||||||
|
" #=.........* ",
|
||||||
|
" *:.........+ ",
|
||||||
|
" *:.........* ",
|
||||||
|
" #=.........* ",
|
||||||
|
" *........-# ",
|
||||||
|
" #+......:* ",
|
||||||
|
" #*-..:+** ",
|
||||||
|
" ********% ",
|
||||||
|
" %*******% ",
|
||||||
|
" #********# ",
|
||||||
|
" *********** ",
|
||||||
|
" ****..:***** ",
|
||||||
|
" %***-....-**** ",
|
||||||
|
" ****.......+***% ",
|
||||||
|
" ***=........-***% ",
|
||||||
|
" ***...........***% ",
|
||||||
|
" ***............=**# ",
|
||||||
|
" @**=..............*** #**# ",
|
||||||
|
" #**................+** #=...-*# ",
|
||||||
|
" ***.................-**@ #.......*% ",
|
||||||
|
" **+...................**% .........* ",
|
||||||
|
" @**.....................+*@ -.........+% ",
|
||||||
|
" #**......................=*# -.........-* ",
|
||||||
|
" %*+........................*# =.........:* ",
|
||||||
|
" **-.......-.................*% *.........=# ",
|
||||||
|
" **........+..................+# ##***-........* ",
|
||||||
|
" **......-.*.=.................=# %##**+=....*.......+# ",
|
||||||
|
" *+.......***...................-* #*+=...........*=...:*# ",
|
||||||
|
" #*.......=****-..................-* %--.................=*****% ",
|
||||||
|
" #*........+**.....................:* #+=......................*% ",
|
||||||
|
" **........:*.-.....................:* =...........................* ",
|
||||||
|
" *=.......:.-........................:# @#**=.............................:* ",
|
||||||
|
" *.......:............................:# #**+-................................*# ",
|
||||||
|
" *.....................................-# #*+....................................* ",
|
||||||
|
" %+......................................*####*##*%% *+.....................................:* ",
|
||||||
|
" *-...............................:=****+++==----==+****## *......................................+% ",
|
||||||
|
" *........=....................+**+=-------::...........-+***%% *......................................* ",
|
||||||
|
" *.......:+:...............:+*+-------------:...............:+**# @=.....................................-* ",
|
||||||
|
" *.....:=+++:............=*=.---------------::..................=**%#:.....................................*# ",
|
||||||
|
" %+........+............*+.------------------::.....................+*......................................* ",
|
||||||
|
" #-........=..........+=.---------------------:......................*.....................................+* ",
|
||||||
|
" *:........:........+=.-----------------------::.....................*.....................................*# ",
|
||||||
|
" #:................+.--------------------------::....................*....................................=* ",
|
||||||
|
" #...................:-------------------------::....................=....................................+* ",
|
||||||
|
" *......................::---------------------:::.......................................................=*% ",
|
||||||
|
" #.........................::-------------------:::......................................:...............+* ",
|
||||||
|
" %...........................:::-=+++=-----------::......................................+..............++# ",
|
||||||
|
" *:........+.................-**#####**+--------:::...................................:.*.+............+*% ",
|
||||||
|
" #=......*.................**%%%%%%%%%%*=-------:::...................................***............++* ",
|
||||||
|
" %*:...*................=*%%%%%%%%%%%%%%*-------:::.................................:****:.........:++% ",
|
||||||
|
" %+.+................=*%%%%%%%%%%%%%%%%#------::::.................................-**=..........++*% ",
|
||||||
|
" *:...............-*%%%%%%%%%%%%%%%%%%#------::::................................+-=.=........:++* ",
|
||||||
|
" %+................*%%%%%%%%%%%%%%%%%%%%*------::::..............................-..=..:.......+++% ",
|
||||||
|
" @*................+#%%%%%%%%%%%%%%%%%%%%#+------::::................................=.........=++* ",
|
||||||
|
" *.................*%%%%%%%%%%%%%%%%%%%%%%*:-----:::::...............................=.........+++* ",
|
||||||
|
" %+................:*%%%%%%%%%%%%%%%%%%%%%%*=::----:::::..............................:........++++ ",
|
||||||
|
" *.................=%%%%*%%%%%%%%%%%%%%%%%%%+:::----:::::.....................................=+++* ",
|
||||||
|
" #+.................=%%%..=%%%%%%%%%%%%%%%%%%=.::::---::::::...................................++++% ",
|
||||||
|
" *..................=%%%....%%%%%%%%%%%%%%%%%-..::::---::::::.................................+++++ ",
|
||||||
|
" *+..................:%%+....%%%%%%%%%%%#%%%%%:....:::---:::::::..............................+++++* ",
|
||||||
|
" *....................*%+....%%%%%%%%=....%%%%......::::--:::::::............................=+++++* ",
|
||||||
|
" %*....................+%%...%%%%%%%%%.....%%%%.......::::---:::::::.........................-+++++* ",
|
||||||
|
" *-.....................#%%%%%%%%%%%%%.....%%%*........::::---:::::::.......................-++++++* ",
|
||||||
|
" *.......................%%%%%%%%%%%%%-....%%%-.........::::---::::::::....................=++++++*% ",
|
||||||
|
" #*.......................:#%%%%%%%%%%%%:..*%%*............::::--:::::::::.................++++++++* ",
|
||||||
|
" #+.........................*%%%%%%%%%%%%%%%%#-.............::::---:::::::::...........:-++++++++++# ##*% ",
|
||||||
|
" *:.........................*%%:+%+%%%%%%%%%*=...............::::---:::::::::::..........-****++++* #**:.# ",
|
||||||
|
" *.............-****=.......*%%.#%.%%%%%%%#*-.................::::---::::::::::::............*###*# %#**-....* ",
|
||||||
|
" *...........-#@@@@@#*......-**+%*.%%*****=...................:::::----::::::::::::::........* %#**+.......+ ",
|
||||||
|
"%*.........-@@@@@@@@@%*........:+***%*:........................:::::-----::::::::::::::::::::+ *+..........- ",
|
||||||
|
"#+.......-#@@@@@@@@@@@@*:..........-+*...........:*****=........:::::-----:::-:::::::::::::::+# %*+..........# ",
|
||||||
|
"%=......+*@@@*@@@@@@@@@@*-.....................+*@@@@@@%*+.......:::::------:::::::::::::::::-* #*:........+ ",
|
||||||
|
"#:.....+*@@@@*+@@@@@@@@@@*+..................#@@@@@@@@@@@%*-......::::--------::::::::::::::::* %*........- ",
|
||||||
|
"*......*@@@@%@+%@@@@@@@@@@**.............-*@@@@@@@@@@@@@@@@*+......::::-------=-::::::::::::::* *.........**# ",
|
||||||
|
"*.....+@@@@@+@==%@@@@@@@@@@#*.........=*@@@@@@@@@@@@@@@@@@@@%*.....:::::----+=*=*-::::::::::::* *.....*:..%@%* ",
|
||||||
|
"*.....@@@@@@+=@==#@@@@@@@@@@%*=....+*#@@@@@@@@@@@@@@@@@@@@@@@@*.....:::::--+-+*--*--::::::::::* %-.....**+.*@@%* ",
|
||||||
|
"#....#@@@@@@*=@===*@@@@@@@@@@@#***#@@@@@@@@@@@@@@@@@@@@@@@@@@@@*.....::::-+--***--+---::::::::* *.....+%@@**@@@* ",
|
||||||
|
"%....@@@@@@@+@@@+==+@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*....:::::+=*******=----::::::* @=.....*@@@@%@@@*% ",
|
||||||
|
"#:...@@@@@@@++@=====+@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+%@@@@%*....::-+*******+*--------::-* #*@*.....-#@@@@@@@@*% ",
|
||||||
|
"%=..-@@@@@@+#@@++++==+@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*+@@@@@@#+....:::+:+***--*-----------* **%@#......*@@@@@@@@@* ",
|
||||||
|
" +..+@@@@@@+++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*==@@@@@@@@*:...:::-:-=**-=-----------=#*%@@+......=#@@@@@@@@@* ",
|
||||||
|
" *..+@@@@@@++++++++++%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+====@+%@@@@@@*....:::+--*=++-----------+*%#+........*@@@@@@@@@%* ",
|
||||||
|
" *..+@@@@@@++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+===%===+@@@@@@@#=...:::::+*+-------------*-:.........*@@@@@@@@@@* ",
|
||||||
|
" %..=@@@@@@++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@======@===@@@@@@@@@*....:::::---------------*=........-+@@@@@@@@@@@* ",
|
||||||
|
" ..:@@@@@@*++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@++====@@%==@@@@@@@@@%=...:::-::-------------=*++++++++++@@@@@@@@@@@* ",
|
||||||
|
" -..%@@@@@@+++++++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@+++++++%+@+@@@@@@@@@@*....::-::-------------+%@%++++++#@@@@@@@@@@@*# ",
|
||||||
|
" ..*@@@@@@@***++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+++++++*+#+@@@@@@@@@@*....::*:::------------*@@@@*+#%@@@@@@@@@@@@#* ",
|
||||||
|
" :.*@@@@@@@@#**@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+++++++++++@@@@@@@@@@%-....:%:::-----------=#@@@@@*#@@@@@@@@@@@@#* ",
|
||||||
|
" #.=@@@@@@@@@@@@@@@@@@@@@@#*#@@@@@@@@@@@@@@@@@*++++++++++@@@@@@@@@@@+....%%%#+:----------*%@@@@@@#*@@@@@@@@@@*# ",
|
||||||
|
" +.*@@@@@@@@@@@@@@@@@@@@*#%%*@@@@@@@@@@@@@@@@%++++++++++@@@@@@@@@@@*....=%*:::---------=*@@@@@@@@#*@@@@@@@@@* ",
|
||||||
|
" #.+@@@@@@@@@@@@@@@@@@@@*%%%%@@@@@@@@@@@@@@@@@+++++++++*@@@@@@@@@@@*.....#:::::--------*@@@@@@@@@@#*@@@@@@@@* ",
|
||||||
|
" @*.#@@@@@@@@@@@@@@@@@@@%*%%%*@@@@@@@@@@@@@@@@%++++++++@@@@@@@@@@@@*.....-:::::-------+#@@@@@@@@@@@#*@@@@@@%% ",
|
||||||
|
" %++@@@@@@@@@@@@@@@@@@@@%***@@@@@@@@@@@@@@@@@@#*****+%@@@@@@@@@@@@*.....:::::::------*@@@@@@@@@@@@@%*@@@@@*% ",
|
||||||
|
" *=*@@@@@@@@@@@@@@@@@@@@@@@@@@@*#@@@@@@@@@@@@@@#**#@@@@@@@@@@@@@@*......::::::-----*%@@@@@@@@@@@@@@%*%@@@* ",
|
||||||
|
" *+%@@@@@@@@@@@@@@@@@@@@@@@@@*%%*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+......::::::----*%@@@@@@@@@@@@@@@@@*#@%% @****# ",
|
||||||
|
" **%**##**#@@@@@@@@#@@@@@@@##%%*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@=......:::::::--+#@@@@@@@@@@@@@@@@@@@@**%@@%@@@@**% ",
|
||||||
|
" **@@@@@@%*%@@@@@@@#*@@@@@*%*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@:.......::::::-=#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*# ",
|
||||||
|
" @*@@@@@@@@@*#@@@@@@@@***%*#**@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#........::::::=#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" #%@@@@@@@@@@*@@@@@@@@@@@%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@:........:::::+%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||||
|
" *@@@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%.........::::*%@@@@@%#*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||||
|
" *@@@@@@@@@@@@@%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@..........:::*%@@@@@%*####@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||||
|
" #%@@@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@:..........:...:*#@@@*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||||
|
" *@@@@@@@@@@@@#@@@@@@@@@@@@@@@@@@@@@@@@@*****#@@@@@@@@@@@@*.........-+...-+..-*#*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||||
|
" ##@@@@@@@@@@@%@@@@@@@@@@@@@@@@@@@@@@@**@@@@@%*%@@@@@@@@%*........++..-++=......*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||||
|
" ##@@@@@@@@@@@*#@@@@@@@@@@@@@@@@@@@@*%@@@@@@@@*%@@@@@@%+......=*+-=++++........*#####*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||||
|
" %*@@@@@@@@@@@%**#@@@@@@@@@@@@@@@@*@@@@@@@@@@@*@@@@%*:....=**+++++++:........*%*###*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||||
|
" %*% @@@%***%@@@@@@@@@@@*%@@@@@@@@@@@*@@%*=.:=***++++++++-........+#@@@***@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" %*#% @*++++++**%@@@@@%*@@@@@@@@@@@@********++++++++++-.......=*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||||
|
" @% **#### *--+++++++++++***@@@@@@@@@@@@@**+++++++++++****++==++**%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%# ",
|
||||||
|
" *#*#####%*:....:=++++**%@@*@@@@@@@@@@@@@*+++++++++++=*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" %*######*.......-+**%@@@@@*@@@@@@@@@@@@#+++++++++-...#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" *######********##%*%@@@@@*@@@@@@@@@@@@*+=--........-%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" %*#####* #* #*@@@@*@@@@@@@@@@@**............=@@@@@@@@@@@@@@@@@@@@@@@@#%@@@@@@@@@@@@@@@@@#@@@@@@@@@@@%# ",
|
||||||
|
" **###*@ %** *#@@#@@@@@@@@@%*@#*...........=@@@@@@@@@@@@@@@@@@@@@@%* #*%%@@@@@@@@@%%**% *%@@@@@@@@@*% ",
|
||||||
|
" #***#***# #*#@*@@@@@@@@@@@@#+..........-@@@@@@@@@@@@@@@@@@@@@## #***********# %*%@@@@@@@%* ",
|
||||||
|
" %%* #**@@@@@@@@@@@@@#*.........:%@@@@@@@@@@@@@@@@@@%*@ #**# **%@@@@#*@ ",
|
||||||
|
" %*@@@@@@@@@@@@@@*-........*@@@@@@@@@@@@@@@@%*% %****** ",
|
||||||
|
" #*@@@@@@@@@@@@@@**:......*#*#@@@@@@@@@@%**# ",
|
||||||
|
" **@@@@@@@@@@@@@@%**=...:*####@@@@@%#**# ",
|
||||||
|
" #*@@@@@@@@@@@@@@@@#**+*#####*****#@ ",
|
||||||
|
" #**@@@@@@@@@@@@@%*****######% ",
|
||||||
|
" #***#%%@@%##*#% **###### ",
|
||||||
|
" %##**##@ *#####* ",
|
||||||
|
" %*###*# ",
|
||||||
|
" %***% "
|
||||||
|
],
|
||||||
|
[
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" #*++*# ",
|
||||||
|
" *=.....= ",
|
||||||
|
" *-.......= ",
|
||||||
|
" %*.........* ",
|
||||||
|
" #-.........* ",
|
||||||
|
" #..........+ ",
|
||||||
|
" #..........* ",
|
||||||
|
" *+.........* ",
|
||||||
|
" %*........+# ",
|
||||||
|
" #*......=# ",
|
||||||
|
" #*+::=**# ",
|
||||||
|
" @%******# ",
|
||||||
|
" *******# ",
|
||||||
|
" %********# ",
|
||||||
|
" **********# ",
|
||||||
|
" ****..-****# ",
|
||||||
|
" #***-....+***# ",
|
||||||
|
" #***......=***# ",
|
||||||
|
" ***-.......:***# ",
|
||||||
|
" @***..........***% ",
|
||||||
|
" #**-...........*** ",
|
||||||
|
" ***.............+** ",
|
||||||
|
" **+..............=** ",
|
||||||
|
" %**................-** ",
|
||||||
|
" ***.................:** ",
|
||||||
|
" **-..................:** ",
|
||||||
|
" %**.....................** ",
|
||||||
|
" #*+.......:..............** ",
|
||||||
|
" **........-...............** ",
|
||||||
|
" **........=..-.............*# ",
|
||||||
|
" %*=........+.+...............+# ##*#% ",
|
||||||
|
" #*.......-+**.................+# *:...=*% ",
|
||||||
|
" *+.....:-+***-:................+# =.......*% ",
|
||||||
|
" *-........**+...................*% #........:* ",
|
||||||
|
" %*........+:*-:...................** :.........* ",
|
||||||
|
" *+.......=..=.:....................* ..........+# ",
|
||||||
|
" *:......:...-.......................* :.........+# ",
|
||||||
|
" %*...........:.......................-# %##+.........*@ ",
|
||||||
|
" %+.......:...:........................=% ###****+=--:*.........* ",
|
||||||
|
" *........:.............................+% *:---...............:*.......*% ",
|
||||||
|
" *........=..............................* +-.........................-*:...=*% ",
|
||||||
|
" +.......=+.......................:-=+++******##% @#****-:.............................:*#***# ",
|
||||||
|
" #:.....:-+++:................-+**+++==-----::--=+****#% %**+=...................................*% ",
|
||||||
|
" #.........+..............-**+--------------::.......:+***%% **.......................................* ",
|
||||||
|
" *.........:...........-*+::----------------::...........:+**% @*.......................................=# ",
|
||||||
|
" *.........:.........=+:.--------------------:..............:+*## #+.......................................* ",
|
||||||
|
" +.................+=.-----------------------:.................-**% *.......................................-* ",
|
||||||
|
" -................:.-------------------------::..................:** *.......................................*# ",
|
||||||
|
" #-...................:-----------------------::.....................*+.......................................* ",
|
||||||
|
" %:.....................::---------------------::....................:-......................................+# ",
|
||||||
|
" *........................:-------------------::....................=.......................................* ",
|
||||||
|
" *:.......+................::-----------------::...................+......................................+* ",
|
||||||
|
" #=.....+..................::=+++==----------::...................+.....................................:* ",
|
||||||
|
" %*..:+..................=**#%%%#**+---------::........................................................+* ",
|
||||||
|
" #=+.................-*#%%%%%%%%%%*=-------:::......................................................=*% ",
|
||||||
|
" #+.................+*%%%%%%%%%%%%%%*-------::......................................................+* ",
|
||||||
|
" %*.................+*%%%%%%%%%%%%%%%%#------:::....................................:..:............+*# ",
|
||||||
|
" *.................+*%%%%%%%%%%%%%%%%%%*------:::................................-..-.-............:+* ",
|
||||||
|
" *:.................*%%%%%%%%%%%%%%%%%%%%+-----::::................................+.+=.............++% ",
|
||||||
|
" #=.................*%%%%%%%%%%%%%%%%%%%%%%=-----:::.................................**+............=+* ",
|
||||||
|
" #*..................*%%%%%%%%%%%%%%%%%%%%%%*------:::.............................-=****-...........++# ",
|
||||||
|
" *:.................:#%%%#%%%%%%%%%%%%%%%%%%*------::::...............................+*+...........++* ",
|
||||||
|
" #+..................=%%%=.#%%%%%%%%%%%%%%%%%#+::----::::.............................+.*.:.........=++# ",
|
||||||
|
" *...................=%%%...#%%%%%%%%%%%%%%%%%+:::----::::...........................-..+..........:++*% ",
|
||||||
|
" #+...................:%%=....%%%%%%%%%%%%%%%%%=.:::----::::.............................=..........+++# ",
|
||||||
|
" *.....................#%:....%%%%%%%%%%%%%%%%%...:::---:::::............................:.........+++* ",
|
||||||
|
" #*.....................*%*...%%%%%%%%%#=..%%%%%....:::---:::::...........................:........=+++# ",
|
||||||
|
" *:.....................-%%*=%%%%%%%%%%....-%%%%.....::::--::::::.........................:.......:++++ ",
|
||||||
|
" *.......................*%%%%%%%%%%%%#.....%%%*......::::--::::::................................++++# ",
|
||||||
|
" #+........................*%%%%%%%%%%%%....:%%%:.......::::--::::::..............................+++++ ",
|
||||||
|
" *:.........................*%%%%%%%%%%%:...%%%*.........::::--::::::............................+++++# ",
|
||||||
|
" *..............=**=........+%%%=%%%%%%%%#+%%%#:..........::::--:::::::.........................++++++ ",
|
||||||
|
" *...........:*@@@@@*-......*%%=:%.%%%%%%%%%%#=............::::--::::::::......................++++++* ##*# ",
|
||||||
|
" %=.........*@@@@@@@@@*:.....+*#.#%.%%%%%%%%%*=..............::::--::::::::....................+++++++% #**-.# ",
|
||||||
|
" #-.......*@@@@@@@@@@@@*:......-***.%%#*****+.................:::---:::::::::................-+++++++*%**=....* ",
|
||||||
|
" *......:*@@@@+@@@@@@@@@*-........+**%*.......................::::---::::::::::............-++++++++**+.......+ ",
|
||||||
|
" *......*@@@@@+#@@@@@@@@@*:.........:+*........................::::----::::::::::........+*+++++++++*.........- ",
|
||||||
|
" *.....*@@@@#@%=@@@@@@@@@@*:......................:+**=.........::::----:::::::::::........-+**++++*+..........# ",
|
||||||
|
" *....-@@@@@#++==@@@@@@@@@@*:..................=*#@@@@@**:.......::::----:::::::::::::........*#***##*:........+ ",
|
||||||
|
" *....@@@@@@@====+@@@@@@@@@@*:..............=@@@@@@@@@@@@*+......:::::-----:::=::::::::::.....* #*........- ",
|
||||||
|
" *...=@@@@@@@=++==+@@@@@@@@@@*:.......:=*%@@@@@@@@@@@@@@@@%*......::::------::+:::::::::::::::* *.........**# ",
|
||||||
|
" *...%@@@@@@+#@@%==*@@@@@@@@@@**=-:+**%@@@@@@@@@@@@@@@@@@@@@*:.....::::-------+:::::::::::::::* *.....*:..%@#* ",
|
||||||
|
" *...@@@@@@@+#*=====#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*-....:::::------+:::::::::::::::* %-.....**+.*@@%* ",
|
||||||
|
" *..-@@@@@@#+@*++====#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*:....::::-----+*+-:::::::::::::* *.....+%@@**@@@* ",
|
||||||
|
" *..=@@@@@@+++++++++==@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*....:::::--+=-*-=+::::::::::::* @=.....*@@@@#@@@*% ",
|
||||||
|
" *..+@@@@@@++++++++++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*....:::::---+**-==-::::::::::* #*@*.....-*@@@@@@@@*% ",
|
||||||
|
" #..+@@@@@@++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+#@@@@%+....::::*-=***+-*---::::::::***%@#......*@@@@@@@@@* ",
|
||||||
|
" ..+@@@@@@++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%++@@@@@@*:...:::-*********=----::::::*%@@+......=#@@@@@@@@@* ",
|
||||||
|
" ..=@@@@@@*++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+===@%@@@@@@*....:::+-*****=*-------:::.*#+........*@@@@@@@@@%* ",
|
||||||
|
" +.-@@@@@@@+++++++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*======%+@@@@@@#=...:::+:-***--+---------::+.........*@@@@@@@@@@* ",
|
||||||
|
" #..%@@@@@@%***++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#======+==+%@@@@@@@*...::::+--*=-*-----------==.......-+@@@@@@@@@@@* ",
|
||||||
|
" -.*@@@@@@@@***@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*++===+%@+==@@@@@@@@#:...::::*+*+*------------*+++++++++@@@@@@@@@@@* ",
|
||||||
|
" *.*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*+++++++@===@@@@@@@@@*...:::::-*--------------*%++++++#@@@@@@@@@@@*# ",
|
||||||
|
" #:=@@@@@@@@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@@@@@*++++++++@++@@@@@@@@@*....:::::+--------------*@@*+#%@@@@@@@@@@@@#* ",
|
||||||
|
" *.*@@@@@@@@@@@@@@@@@@@@*##*@@@@@@@@@@@@@@@@#+++++++++++@@@@@@@@@#:...::=::+-------------+%@@@*#@@@@@@@@@@@@#* ",
|
||||||
|
" @=*@@@@@@@@@@@@@@@@@@@*#%%%%@@@@@@@@@@@@@@@%+++++++++++@@@@@@@@@@+...::=::+-------------*@@@@@#*@@@@@@@@@@*# ",
|
||||||
|
" *-#@@@@@@@@@@@@@@@@@@@*%%%#@@@@@@@@@@@@@@@@++++++++++%@@@@@@@@@@+....:#:::------------=#@@@@@@#*@@@@@@@@@* ",
|
||||||
|
" **@@@@@@@@@@@@@@@@@@@#*###@@@@@@@@@@@@@@@@++++++++++@@@@@@@@@@@*....=%#:::-----------*@@@@@@@@#*@@@@@@@@# ",
|
||||||
|
" #*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%++++++++*@@@@@@@@@@@*....#%%-::----------=#@@@@@@@@@#*@@@@@@%% ",
|
||||||
|
" #*@@@@@@@@@@@@@@@@@@@@@@@@@#*#@@@@@@@@@@@@**++++++@@@@@@@@@@@@*.....#-:::----------*%@@@@@@@@@@#*@@@@@*% ",
|
||||||
|
" **@@@@@@@@@@@@@@@*@@@@@@@%*%##@@@@@@@@@@@@%*****@@@@@@@@@@@@@*.....+:::::--------+#@@@@@@@@@@@@%*%@@@* ",
|
||||||
|
" **@@@@@@@@@@@@@@@*@@@@@@*%#%#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+.....=:::::-------=*@@@@@@@@@@@@@@@*#@%# ****% ",
|
||||||
|
" #**##**#@@@@@@@@@@**%@@*%###@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@=.....::::::-------*%@@@@@@@@@@@@@@@@%**%@@%@@@@**@ ",
|
||||||
|
" #*@@@@@@%*%@@@@@@@@@@#*****#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%:......::::::-----*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*# ",
|
||||||
|
" @*@@@@@@@@@*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#.......::::::----*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" #%@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*.......::::::---+%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" *@@@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%........:::::::-*%@@%#*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||||
|
" *@@@@@@@@@@@@@%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@=........:::::::*%@@%*####@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||||
|
" #%@@@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#.........:::::=*#@@@*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||||
|
" *@@@@@@@@@@@@#*%@@@@@@@@@@@@@@@@@@@@@@@*****#@@@@@@@@@@@@%...........:::*+..=*#*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||||
|
" %#@@@@@@@@@@@%@**#@@@@@@@@@@@@@@@@@@@**@@@@@%*%@@@@@@@@@%............:-+-......*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||||
|
" ##@@@@@@@@@@@@@@%**%@@@@@@@@@@@@@@@*%@@@@@@@@*%@@@@@@@#:..........=+++........*#####*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||||
|
" #*@@@@@@@@@@@@@@@****#@@@@@@@@@@@*@@@@@@@@@@@*@@@@@%*.........-*+++:........*%*###*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||||
|
" %*% @@@@*+++++*#%@@@@@@*%@@@@@@@@@@@*@@@%*:......-+*++++-........+#@@@***@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" %*#@ @*+++++++++++**#%*@@@@@@@@@@@@*%**...:=+***+++++-.......-*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||||
|
" @% **###% *-=+++++++++++*%*@@@@@@@@@@@@@********+++++****++==++**%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%# ",
|
||||||
|
" *#*######*:....:=++++**%@@*@@@@@@@@@@@@@*+++++++++++=*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" %*######*.......-+**%@@@@@*@@@@@@@@@@@@#+++++++++-...#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" *######********##%*%@@@@@*@@@@@@@@@@@@*+==-........-%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" %*#####* #* #*@@@@*@@@@@@@@@@@**............=@@@@@@@@@@@@@@@@@@@@@@@@#%@@@@@@@@@@@@@@@@@#@@@@@@@@@@@%# ",
|
||||||
|
" **###*@ %** *#@@#@@@@@@@@@%*@#*...........=@@@@@@@@@@@@@@@@@@@@@@%* #*%%@@@@@@@@@%%**% *%@@@@@@@@@*% ",
|
||||||
|
" #***#***# #*#@*@@@@@@@@@@@@#+..........=@@@@@@@@@@@@@@@@@@@@@## #***********# %*%@@@@@@@%* ",
|
||||||
|
" %%# %**@@@@@@@@@@@@@#*.........:%@@@@@@@@@@@@@@@@@@%*% #**# **%@@@@#*@ ",
|
||||||
|
" %*@@@@@@@@@@@@@@*-........*@@@@@@@@@@@@@@@@%*% %****** ",
|
||||||
|
" #*@@@@@@@@@@@@@@**:......*#*#@@@@@@@@@@%**# ",
|
||||||
|
" #*@@@@@@@@@@@@@@%**=...:*####@@@@@%#**# ",
|
||||||
|
" #*@@@@@@@@@@@@@@@@#**+*#####*****#@ ",
|
||||||
|
" #**@@@@@@@@@@@@@%*****######% ",
|
||||||
|
" #***#%%@@%##*## #*###### ",
|
||||||
|
" %##**#%@ *#####* ",
|
||||||
|
" %*###*# ",
|
||||||
|
" #***% "
|
||||||
|
],
|
||||||
|
[
|
||||||
|
" #**#% ",
|
||||||
|
" %*=...=# ",
|
||||||
|
" %*.......+ ",
|
||||||
|
" *:........# ",
|
||||||
|
" *.........- ",
|
||||||
|
" %+.........- ",
|
||||||
|
" #=.........- ",
|
||||||
|
" +.........+ ",
|
||||||
|
" *.........* ",
|
||||||
|
" #*.......*@ ",
|
||||||
|
" #*:...-*% ",
|
||||||
|
" #******# ",
|
||||||
|
" %******% ",
|
||||||
|
" ******** ",
|
||||||
|
" ********* ",
|
||||||
|
" #****-***** ",
|
||||||
|
" ****...+**** ",
|
||||||
|
" #***:....-***% ",
|
||||||
|
" ***+......:***# ",
|
||||||
|
" ***........:*** ",
|
||||||
|
" #**+.........:*** ",
|
||||||
|
" ***............**# ",
|
||||||
|
" #**=............:**% ",
|
||||||
|
" ***..............:**@ ",
|
||||||
|
" @**=...............:** ",
|
||||||
|
" %**.................-** ",
|
||||||
|
" **=..................-*# ",
|
||||||
|
" %**.......:............=*# ",
|
||||||
|
" #*=.......=.............=* ",
|
||||||
|
" **........+..............+*@ ",
|
||||||
|
" %*+.....-..+..-............+* ",
|
||||||
|
" **.......+.+.+..............*# ",
|
||||||
|
" *+........***................*# ",
|
||||||
|
" %*.......:****+:...............* ",
|
||||||
|
" *+........+**=.................:* ",
|
||||||
|
" *........=.---..................-# ",
|
||||||
|
" #*.......:..-..-..................+% ",
|
||||||
|
" *:..........-......................* ",
|
||||||
|
" *.......:...-.......................* ",
|
||||||
|
" #-.......-...-.......................=# %*# ",
|
||||||
|
" *........=...:........................* @*=-=** ",
|
||||||
|
" +.......:+.............................# -.....:*% ",
|
||||||
|
" #:.....:=+++:...........................-% ........:* ",
|
||||||
|
" *........:+..............................* =.........*%",
|
||||||
|
" *.........:.................-=++***********### -.........=#",
|
||||||
|
" #-......................:+*++-::------------=++***#% +.........-*",
|
||||||
|
" #....................=*+..------------------::....+**** %*=-*%%##*###*****.........+#",
|
||||||
|
" *.................-+:.:---------------------::.......:***% %##******::..........................*.........*%",
|
||||||
|
" *..................:------------------------::..........:**% #**+--..................................:*.......-* ",
|
||||||
|
" #...................:-----------------------::.............+*#- %*........................................+*.....=* ",
|
||||||
|
" *....................:----------------------:...............=*# *:........................................=#**++*# ",
|
||||||
|
" *.......::............:--------------------::................=*# @*.........................................* #@ ",
|
||||||
|
" *.....+................:------------------::..................+*%%=........................................+# ",
|
||||||
|
" %:..*..................::----------------::....................**.........................................*% ",
|
||||||
|
" %++.....................:====------------::....................*........................................+# ",
|
||||||
|
" #=...................:***###***=---------::...................:-........................................* ",
|
||||||
|
" *=...................**%%%%%%%%%#*=-------:::..................+........................................+# ",
|
||||||
|
" #+..................=*%%%%%%%%%%%%%#+-------::..................+........................................* ",
|
||||||
|
" #+..................+*%%%%%%%%%%%%%%%%+------:::.................:.......................................*% ",
|
||||||
|
" @*..................=*%%%%%%%%%%%%%%%%%%+------::........................................................-* ",
|
||||||
|
" @*...................*%%%%%%%%%%%%%%%%%%%%------:::...................................:...................*% ",
|
||||||
|
" *-..................+#%%%%%%%%%%%%%%%%%%%%%-----:::...................................:..................+* ",
|
||||||
|
" #+...................*%%%%%%%%%%%%%%%%%%%%%%*-----:::..................................-.................=* ",
|
||||||
|
" *....................*%%%:%%%%%%%%%%%%%%%%%%*=-----:::.................................=................:+* ",
|
||||||
|
" *=...................:#%%...%%%%%%%%%%%%%%%%%#+-----::::................................+..-.............+* ",
|
||||||
|
" %*....................:%%#...:%%%%%%%%%%%%%%%%%*:-----:::..............................:-*.+.............++# ",
|
||||||
|
" *-.....................#%-....%%%%%%%%%%%%%%%%%*::----::::..............................***.............-+* ",
|
||||||
|
" @*......................*%-...:%%%%%%%%%%%%%%%%%+:::----::::............................-***+-:..........++% ",
|
||||||
|
" #=......................+%%..:%%%%%%%%%%%%%%%%%%..:::----::::............................+*+............++* ",
|
||||||
|
" *........................%%%%%%%%%%%%%#....%%%%%...:::---:::::..........................:.*.=..........++*% ",
|
||||||
|
" %+........................:%%%%%%%%%%%%-....#%%%%....:::---:::::...........................+..-........=++* ",
|
||||||
|
" *:.........................+%%%%%%%%%%%:....#%%%......:::---:::::..........................=..........-++*@ ",
|
||||||
|
" *............=*#%%*+........*%%%%%%%%%%+....%%%#.......:::--::::::.........................-.........:+++% ",
|
||||||
|
" #*.........+@@@@@@@@@*......:#%%.%%%%%%%%-..-%%%=........:::--::::::........................:........:+++* ",
|
||||||
|
" #-......:*@@@@@@@@@@@%+.....+#%#.%++%%%%%%%%%%%+..........:::--::::::.......................:........++++% ",
|
||||||
|
" *......+*@@@@#@@@@@@@@#+.....-*+=%.*%%%%%%%%%#*...........::::--::::::..............................++++##*# ",
|
||||||
|
" *.....=#@@@@@+#@@@@@@@@*-......-**.%%%*#%%%#*=.............::::--::::::............................+++++*-.# ",
|
||||||
|
" *.....@@@@@#%%+@@@@@@@@@*:.......:**%*=-+++-................:::---::::::..........................+++++....* ",
|
||||||
|
" +....@@@@@@**+=*@@@@@@@@@*.........:**.......................:::---:::::::......................:+++++-....+ ",
|
||||||
|
" %=...+@@@@@@@====@@@@@@@@@@*..................................::::---:::::::....................=++++++.....- ",
|
||||||
|
" #-...@@@@@@@#=#===@@@@@@@@@#*......................::..........::::---:::::::..................++++++++......# ",
|
||||||
|
" *:..=@@@@@@@=%@@===@@@@@@@@@*=................-+**#%@#*=.......::::----::::::::..............=+++++++*.......+ ",
|
||||||
|
" *...*@@@@@@+*+@====*@@@@@@@@@*+.........-++%@@@@@@@@@@@%*:......::::----:::::::::.......-.-=++++++++++.......- ",
|
||||||
|
" *...%@@@@@@+@**+====%@@@@@@@@@%**+=***%@@@@@@@@@@@@@@@@@@*=......::::----::::::::::......-*+++++++++*.........**# ",
|
||||||
|
" %...@@@@@@%++++++++==@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*+.....::::-----:::::::::::.......+*+++++**.....*:..%@#* ",
|
||||||
|
" ..:@@@@@@*++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*+.....::::-----::::::::::::......-******=.....**+.*@@%* ",
|
||||||
|
" ..-@@@@@@+++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*=....::::------::::::::::::::...-# @#*.....+%@@**@@@* ",
|
||||||
|
" ..-@@@@@@++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*-....::::-------:::::::::::::::-* @=.....*@@@@%@@@*# ",
|
||||||
|
" +.-@@@@@@#++++++++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*....:::::-----=-::::::::::::::=##*@*.....-*@@@@@@@@*# ",
|
||||||
|
" +.:@@@@@@@++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%*....::::---++*++:::::::::::::=*%@%......*@@@@@@@@@*@ ",
|
||||||
|
" #..%@@@@@@#++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+*@@@@*-...:::::-+--*=-*-:::::::::::+%@+......=#@@@@@@@@@* ",
|
||||||
|
" +.*@@@@@@@@***#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+++@@@@@@*....::::+--***--+-::::::::::*+........*@@@@@@@@@@* ",
|
||||||
|
" *.*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*=====@#@@@@@#=...::::++****+-*--:::::::::*........*@@@@@@@@@@*@ ",
|
||||||
|
" %-+@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*=========*+@@@@@@*...::::=********=---::::::.*......-+@@@@@@@@@@@* ",
|
||||||
|
" *-@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+======+===+@@@@@@@*:...:::+:+***+-*-----:::::.*+++++++@@@@@@@@@@@* ",
|
||||||
|
" #=*@@@@@@@@@@@@@@@@@@@****@@@@@@@@@@@@@@@@@++++++#@@#=@@@@@@@@@*...:::+:-**=-==-------:::-++++++#@@@@@@@@@@@*# ",
|
||||||
|
" *+@@@@@@@@@@@@@@@@@@@*%%%*@@@@@@@@@@@@@@@@+++++++%++=@@@@@@@@@*...::::*-+*-=+----------:+%*+#%@@@@@@@@@@@@#* ",
|
||||||
|
" #+#@@@@@@@@@@@@@@@@@@*%%%%@@@@@@@@@@@@@@@@+++++++%#@+@@@@@@@@@*:...::::=***=------------*@@*#@@@@@@@@@@@@#* ",
|
||||||
|
" **@@@@@@@@@@@@@@@@@@#*%%#@@@@@@@@@@@@@@@@+++++++++++@@@@@@@@@@=...:::::----------------*@@@#*@@@@@@@@@@*% ",
|
||||||
|
" **@@@@@@@@@@@@@@@@@@@**@@@@@@@@@@@@@@@@@+++++++++++@@@@@@@@@@+...:::::---------------+%@@@@#*@@@@@@@@@* ",
|
||||||
|
" %*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+++++++++++@@@@@@@@@@*....::::---------------*@@@@@@#*@@@@@@@@* ",
|
||||||
|
" **@@@@@@@@@@@@@@*@@@@@@@@***@@@@@@@@@@@@+++++++++@@@@@@@@@@@*....:=::=-------------=#@@@@@@@#*@@@@@@%% ",
|
||||||
|
" #*@@@@@@@@@@@@@@*@@@@@@*#%#%@@@@@@@@@@@**++++++#@@@@@@@@@@@*....:%+::-------------*@@@@@@@@@%*@@@@@*# ",
|
||||||
|
" #*@@@@@@@@@@@@@@*%@@@#*%*%%@@@@@@@@@@@@****++#@@@@@@@@@@@@*...=%%%#-:-----------=#@@@@@@@@@@%*%@@@* ",
|
||||||
|
" #*@@@@@@@@@@@@@@#***%%*%%@@@@@@@@@@@@@@@%##@@@@@@@@@@@@@@+....:%=:::-----------*@@@@@@@@@@@@@*#@%% ****% ",
|
||||||
|
" %***#**#@@@@@@@@@@@@@@#*#%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+.....#::::----------*%@@@@@@@@@@@@@@%**%@@%@@@@**% ",
|
||||||
|
" #*@@@@@@%*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%:.....+:::::--------=*@@@@@@@@@@@@@@@@@@@@@@@@@@@@*% ",
|
||||||
|
" *@@@@@@@@@*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#......=:::::-------=*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" #%@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*......::::::-------*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||||
|
" *@@@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%:......::::::------*%#*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||||
|
" *@@@@@@@@@@@@@**@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#.......:::::::----*%*####@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" #%@@@@@@@@@@@@*%**@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@-.......:::::::--=*%*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||||
|
" *@@@@@@@@@@@@#@@%**@@@@@@@@@@@@@@@@@@@@*****#@@@@@@@@@@@@@:........:::::::-++*#*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||||
|
" ##@@@@@@@@@@@%@@@%@#*%@@@@@@@@@@@@@@@**@@@@@%*%@@@@@@@@@@-.........:::::::*:...*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||||
|
" ##@@@@@@@@@@@@@@@#@%+*#@@@@@@@@@@@@*%@@@@@@@@*%@@@@@@@@:..........:::::+=.....*#####*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||||
|
" %*@@@@@@@@@@@@@@@#*++++*%@@@@@@@@*%@@@@@@@@@@*@@@@@@%=...........:::.-......+%*###*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||||
|
" #*# @@@@*++++++++*#%%@@*%@@@@@@@@@@@*%@@@%+.............-........+#@@@***@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" %##% @*+++++++++++++***@@@@@@@@@@@@*%%**...........=*+.......-*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||||
|
" %% #*#### *-=+++++++++++*%#@@@@@@@@@@@@@*+-.....:-=+*****++===+**#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||||
|
" *%*#####%*:....:=++++**%@@*@@@@@@@@@@@@@*********++++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" %*######*.......-+**%@@@@@*@@@@@@@@@@@@#+++++++++=...#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" *######*********#%*%@@@@@*@@@@@@@@@@@@*++=-:.......-%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||||
|
" #*#####* #*@ #*@@@@*@@@@@@@@@@@**............=@@@@@@@@@@@@@@@@@@@@@@@@#%@@@@@@@@@@@@@@@@@#@@@@@@@@@@@%# ",
|
||||||
|
" **###*% %** *#@@#%@@@@@@@@%*@#*...........=@@@@@@@@@@@@@@@@@@@@@@%* #*%%@@@@@@@@@%%**% *%@@@@@@@@@*% ",
|
||||||
|
" #***#***# #*#@*@@@@@@@@@@@@#+..........-@@@@@@@@@@@@@@@@@@@@@%# %***********# #*%@@@@@@@%* ",
|
||||||
|
" @#% %**@@@@@@@@@@@@@#*.........:%@@@@@@@@@@@@@@@@@@%*% #%%# **%@@@@#*% ",
|
||||||
|
" %*@@@@@@@@@@@@@@*-........*@@@@@@@@@@@@@@@@%*% %****** ",
|
||||||
|
" #*@@@@@@@@@@@@@@**:......*#*#@@@@@@@@@@%**% ",
|
||||||
|
" **@@@@@@@@@@@@@@%**=...:*####@@@@@%#**# ",
|
||||||
|
" #*@@@@@@@@@@@@@@@@#**+*#####******% ",
|
||||||
|
" #**@@@@@@@@@@@@@%*****######% ",
|
||||||
|
" #***#%@@@%%#**# #*###### ",
|
||||||
|
" %*#**##% *#####* ",
|
||||||
|
" %*###*% ",
|
||||||
|
" #***% "
|
||||||
|
]
|
||||||
|
]
|
||||||
@@ -16,6 +16,7 @@ const API_BASE_URL = getApiBaseUrl()
|
|||||||
// System Info
|
// System Info
|
||||||
export interface BeszelSystemInfo {
|
export interface BeszelSystemInfo {
|
||||||
name: string
|
name: string
|
||||||
|
status: "up" | "down"
|
||||||
info: {
|
info: {
|
||||||
cpu: number
|
cpu: number
|
||||||
ram: number
|
ram: number
|
||||||
|
|||||||
14
apps/dashboard/src/components/tile.tsx
Normal file
14
apps/dashboard/src/components/tile.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import cn from "./lib/cn"
|
||||||
|
|
||||||
|
export function Tile({ children, className }: { children?: React.ReactNode; className?: string }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative rounded-xl bg-neutral-200 dark:bg-neutral-900 flex flex-col justify-end items-start",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
25
apps/dashboard/src/kuromi.tsx
Normal file
25
apps/dashboard/src/kuromi.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import kuromiFrames from "./assets/kuromi-frames.json"
|
||||||
|
|
||||||
|
export function Kuromi() {
|
||||||
|
const [frameIndex, setFrameIndex] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setFrameIndex((prev) => (prev + 1) % kuromiFrames.length)
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const currentFrame = kuromiFrames[frameIndex]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<pre className="leading-none tracking-[0.6em] select-none font-mono text-black dark:text-white scale-[5%]">
|
||||||
|
{currentFrame.map((line, index) => (
|
||||||
|
// biome-ignore lint/suspicious/noArrayIndexKey: frame lines don't have unique identifiers
|
||||||
|
<div key={index}>{line}</div>
|
||||||
|
))}
|
||||||
|
</pre>
|
||||||
|
)
|
||||||
|
}
|
||||||
359
apps/dashboard/src/light-control.tsx
Normal file
359
apps/dashboard/src/light-control.tsx
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
import { ZIGBEE_DEVICE, type ZigbeeDeviceName, ZigbeeDeviceState, type ZigbeeDeviceStates } from "@eva/zigbee"
|
||||||
|
import { useDrag } from "@use-gesture/react"
|
||||||
|
import { atom, useAtom, useAtomValue, useSetAtom, useStore } from "jotai"
|
||||||
|
import { CloudyIcon, LightbulbOffIcon, type LucideIcon, MoonStarIcon } from "lucide-react"
|
||||||
|
import { useEffect, useRef } from "react"
|
||||||
|
import cn from "./components/lib/cn"
|
||||||
|
import { Tile } from "./components/tile"
|
||||||
|
|
||||||
|
const LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT = 36
|
||||||
|
|
||||||
|
// Store brightness as step (0-43) to match the 44 bars exactly
|
||||||
|
// Step 0 = OFF, Steps 1-43 map to bars 42-0
|
||||||
|
export const brightnessStepAtoms = atom({
|
||||||
|
[ZIGBEE_DEVICE.deskLamp]: atom(0),
|
||||||
|
[ZIGBEE_DEVICE.livingRoomFloorLamp]: atom(0),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const intermediateBrightnessStepAtoms = atom({
|
||||||
|
[ZIGBEE_DEVICE.deskLamp]: atom(-1),
|
||||||
|
[ZIGBEE_DEVICE.livingRoomFloorLamp]: atom(-1),
|
||||||
|
})
|
||||||
|
|
||||||
|
const sceneAtom = atom<string | null>(null)
|
||||||
|
|
||||||
|
const DEVICE_FRIENDLY_NAMES = {
|
||||||
|
[ZIGBEE_DEVICE.deskLamp]: "Desk Lamp",
|
||||||
|
[ZIGBEE_DEVICE.livingRoomFloorLamp]: "Floor Lamp",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type LightSceneConfig = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
icon: LucideIcon
|
||||||
|
deviceStates: Partial<ZigbeeDeviceStates>
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_SCENES: Record<string, LightSceneConfig> = {
|
||||||
|
"lights-off": {
|
||||||
|
id: "lights-off",
|
||||||
|
name: "Lights off",
|
||||||
|
icon: LightbulbOffIcon,
|
||||||
|
deviceStates: {
|
||||||
|
[ZIGBEE_DEVICE.deskLamp]: {
|
||||||
|
state: "OFF",
|
||||||
|
brightness: 0,
|
||||||
|
},
|
||||||
|
[ZIGBEE_DEVICE.livingRoomFloorLamp]: {
|
||||||
|
state: "OFF",
|
||||||
|
brightness: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
evening: {
|
||||||
|
id: "evening",
|
||||||
|
name: "Evening",
|
||||||
|
icon: MoonStarIcon,
|
||||||
|
deviceStates: {
|
||||||
|
[ZIGBEE_DEVICE.deskLamp]: {
|
||||||
|
state: "ON",
|
||||||
|
brightness: 127,
|
||||||
|
},
|
||||||
|
[ZIGBEE_DEVICE.livingRoomFloorLamp]: {
|
||||||
|
state: "ON",
|
||||||
|
brightness: 254,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gloomy: {
|
||||||
|
id: "gloomy",
|
||||||
|
name: "Gloomy",
|
||||||
|
icon: CloudyIcon,
|
||||||
|
deviceStates: {
|
||||||
|
[ZIGBEE_DEVICE.deskLamp]: {
|
||||||
|
state: "ON",
|
||||||
|
brightness: 50,
|
||||||
|
},
|
||||||
|
[ZIGBEE_DEVICE.livingRoomFloorLamp]: {
|
||||||
|
state: "ON",
|
||||||
|
brightness: 128,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const
|
||||||
|
|
||||||
|
// Convert brightness (0-254) to step (0-43)
|
||||||
|
// Step 0 = brightness 0, steps 1-43 map to brightness 1-254
|
||||||
|
export function brightnessToStep(brightness: number): number {
|
||||||
|
if (brightness === 0) return 0
|
||||||
|
// Map brightness 1-254 to steps 1-43
|
||||||
|
return Math.max(1, Math.round((brightness / 254) * (LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert step (0-43) to brightness (0-254)
|
||||||
|
// Step 0 = brightness 0, steps 1-43 map to brightness 1-254
|
||||||
|
export function stepToBrightness(step: number): number {
|
||||||
|
if (step === 0) return 0
|
||||||
|
// Map steps 1-43 to brightness 1-254
|
||||||
|
return Math.max(1, Math.round((step / (LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)) * 254))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LightControlTile({
|
||||||
|
deviceName,
|
||||||
|
className,
|
||||||
|
onRequestBrightnessStepChange,
|
||||||
|
}: { deviceName: ZigbeeDeviceName; className?: string; onRequestBrightnessStepChange: (step: number) => void }) {
|
||||||
|
const currentBrightnessStep = useAtomValue(useAtomValue(brightnessStepAtoms)[deviceName])
|
||||||
|
// Map step to bar index for thumb position
|
||||||
|
// Step 0 = OFF (no thumb shown, set to invalid index)
|
||||||
|
// Step 1-43 map to bars 42-0
|
||||||
|
const initialHighlightIndexStart =
|
||||||
|
currentBrightnessStep === 0
|
||||||
|
? LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1 // No thumb (index out of range, but no bars highlighted)
|
||||||
|
: LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1 - currentBrightnessStep
|
||||||
|
const touchContainerRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const barRefs = useRef<(HTMLDivElement | null)[]>(
|
||||||
|
Array.from({ length: LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT }, () => null),
|
||||||
|
)
|
||||||
|
const setIntermediateBrightnessStep = useSetAtom(useAtomValue(intermediateBrightnessStepAtoms)[deviceName])
|
||||||
|
const setScene = useSetAtom(sceneAtom)
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const brightnessStepAtom = store.get(brightnessStepAtoms)[deviceName]
|
||||||
|
if (store.get(brightnessStepAtom) === currentBrightnessStep) {
|
||||||
|
setIntermediateBrightnessStep(-1)
|
||||||
|
}
|
||||||
|
}, [currentBrightnessStep, deviceName, setIntermediateBrightnessStep, store])
|
||||||
|
|
||||||
|
function requestBrightnessStepChange(step: number) {
|
||||||
|
onRequestBrightnessStepChange(step)
|
||||||
|
setScene(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const bind = useDrag(({ xy: [x], first, last }) => {
|
||||||
|
if (!touchContainerRef.current) return
|
||||||
|
|
||||||
|
if (!first) {
|
||||||
|
touchContainerRef.current.dataset.active = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (last) {
|
||||||
|
delete touchContainerRef.current.dataset.active
|
||||||
|
let thumbIndex = -1
|
||||||
|
for (let i = 0; i < LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT; i++) {
|
||||||
|
const bar = barRefs.current[i]
|
||||||
|
if (!bar) continue
|
||||||
|
|
||||||
|
const barRect = bar.getBoundingClientRect()
|
||||||
|
|
||||||
|
if (x >= barRect.left - 2 && x < barRect.right + 2 && thumbIndex === -1) {
|
||||||
|
thumbIndex = i
|
||||||
|
bar.dataset.thumb = "true"
|
||||||
|
} else {
|
||||||
|
delete bar.dataset.thumb
|
||||||
|
}
|
||||||
|
|
||||||
|
delete bar.dataset.touched
|
||||||
|
delete bar.dataset.touchProximity
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thumbIndex !== -1) {
|
||||||
|
// Map bar index to step: bar 42 -> step 1, bar 0 -> step 43
|
||||||
|
const step = LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1 - thumbIndex
|
||||||
|
requestBrightnessStepChange(step)
|
||||||
|
} else {
|
||||||
|
const firstElement = barRefs.current[barRefs.current.length - 1]
|
||||||
|
const lastElement = barRefs.current[0]
|
||||||
|
if (lastElement && x > lastElement.getBoundingClientRect().right) {
|
||||||
|
lastElement.dataset.thumb = "true"
|
||||||
|
setIntermediateBrightnessStep(LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)
|
||||||
|
if (last) {
|
||||||
|
requestBrightnessStepChange(LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)
|
||||||
|
}
|
||||||
|
} else if (firstElement && x < firstElement.getBoundingClientRect().left) {
|
||||||
|
firstElement.dataset.thumb = "true"
|
||||||
|
setIntermediateBrightnessStep(0)
|
||||||
|
if (last) {
|
||||||
|
requestBrightnessStepChange(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let touchedIndex = -1
|
||||||
|
for (let i = 0; i < LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT; i++) {
|
||||||
|
const bar = barRefs.current[i]
|
||||||
|
if (!bar) continue
|
||||||
|
|
||||||
|
const barRect = bar.getBoundingClientRect()
|
||||||
|
|
||||||
|
delete bar.dataset.thumb
|
||||||
|
|
||||||
|
if (x >= barRect.left - 2 && x < barRect.right + 2 && touchedIndex === -1) {
|
||||||
|
touchedIndex = i
|
||||||
|
|
||||||
|
bar.dataset.touched = "true"
|
||||||
|
bar.dataset.highlighted = "false"
|
||||||
|
delete bar.dataset.touchProximity
|
||||||
|
|
||||||
|
const step = LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - i - 1
|
||||||
|
setIntermediateBrightnessStep(step)
|
||||||
|
|
||||||
|
if (barRefs.current[i - 1]) {
|
||||||
|
barRefs.current[i - 1]!.dataset.touchProximity = "close"
|
||||||
|
}
|
||||||
|
if (barRefs.current[i - 2]) {
|
||||||
|
barRefs.current[i - 2]!.dataset.touchProximity = "medium"
|
||||||
|
}
|
||||||
|
if (barRefs.current[i - 3]) {
|
||||||
|
barRefs.current[i - 3]!.dataset.touchProximity = "far"
|
||||||
|
}
|
||||||
|
} else if (barRect.left < x) {
|
||||||
|
if (bar.dataset.touched === "true") {
|
||||||
|
bar.dataset.prevTouched = "true"
|
||||||
|
} else {
|
||||||
|
delete bar.dataset.prevTouched
|
||||||
|
}
|
||||||
|
bar.dataset.touched = "false"
|
||||||
|
bar.dataset.highlighted = "true"
|
||||||
|
if (touchedIndex >= 0) {
|
||||||
|
const diff = i - touchedIndex
|
||||||
|
if (diff === 1) {
|
||||||
|
bar.dataset.touchProximity = "close"
|
||||||
|
} else if (diff === 2) {
|
||||||
|
bar.dataset.touchProximity = "medium"
|
||||||
|
} else if (diff === 3) {
|
||||||
|
bar.dataset.touchProximity = "far"
|
||||||
|
} else {
|
||||||
|
delete bar.dataset.touchProximity
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete bar.dataset.touchProximity
|
||||||
|
}
|
||||||
|
} else if (barRect.right > x) {
|
||||||
|
bar.dataset.highlighted = "false"
|
||||||
|
bar.dataset.touched = "false"
|
||||||
|
if (touchedIndex >= 0) {
|
||||||
|
const diff = i - touchedIndex
|
||||||
|
if (diff === 1) {
|
||||||
|
bar.dataset.touchProximity = "close"
|
||||||
|
} else if (diff === 2) {
|
||||||
|
bar.dataset.touchProximity = "medium"
|
||||||
|
} else if (diff === 3) {
|
||||||
|
bar.dataset.touchProximity = "far"
|
||||||
|
} else {
|
||||||
|
delete bar.dataset.touchProximity
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete bar.dataset.touchProximity
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bar.dataset.touched = "false"
|
||||||
|
bar.dataset.highlighted = "false"
|
||||||
|
delete bar.dataset.touchProximity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (touchedIndex === -1) {
|
||||||
|
const firstElement = barRefs.current[barRefs.current.length - 1]
|
||||||
|
const lastElement = barRefs.current[0]
|
||||||
|
if (lastElement && x > lastElement.getBoundingClientRect().right) {
|
||||||
|
lastElement.dataset.thumb = "true"
|
||||||
|
setIntermediateBrightnessStep(LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)
|
||||||
|
} else if (firstElement && x < firstElement.getBoundingClientRect().left) {
|
||||||
|
firstElement.dataset.thumb = "true"
|
||||||
|
setIntermediateBrightnessStep(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tile className={cn("h-full flex flex-col justify-start items-start", className)}>
|
||||||
|
<div
|
||||||
|
{...bind()}
|
||||||
|
ref={touchContainerRef}
|
||||||
|
className="group flex-1 flex flex-row-reverse justify-center items-center touch-none gap-x-1 w-full translate-y-4"
|
||||||
|
>
|
||||||
|
{Array.from({ length: LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT }).map((_, index) => {
|
||||||
|
const highlighted = index > initialHighlightIndexStart
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-highlighted={highlighted}
|
||||||
|
data-thumb={index === initialHighlightIndexStart}
|
||||||
|
data-prev-touched={false}
|
||||||
|
data-touched={false}
|
||||||
|
ref={(ref) => {
|
||||||
|
barRefs.current[index] = ref
|
||||||
|
}}
|
||||||
|
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
||||||
|
key={index}
|
||||||
|
className="transition-all transition-75 w-[2px] h-[2px] bg-neutral-400 rounded-full data-[highlighted=true]:h-2 data-[touch-proximity=close]:h-6 data-[touch-proximity=medium]:h-4 data-[touch-proximity=far]:h-2 data-[highlighted=true]:bg-teal-500 data-[touched=true]:h-8 data-[touched=true]:w-1 data-[touched=true]:bg-teal-500 data-[touched=true]:transition-none data-[prev-touched=true]:transition-none data-[thumb=true]:h-8 data-[thumb=true]:bg-teal-500"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="px-4 pb-2 w-full flex flex-row items-center justify-center space-x-2">
|
||||||
|
<p className="tracking-tigher uppercase">{DEVICE_FRIENDLY_NAMES[deviceName]}</p>
|
||||||
|
<BrightnessLevelLabel deviceName={deviceName} />
|
||||||
|
</div>
|
||||||
|
</Tile>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BrightnessLevelLabel({ deviceName }: { deviceName: ZigbeeDeviceName }) {
|
||||||
|
const currentBrightnessStep = useAtomValue(useAtomValue(brightnessStepAtoms)[deviceName])
|
||||||
|
const intermediateBrightnessStep = useAtomValue(useAtomValue(intermediateBrightnessStepAtoms)[deviceName])
|
||||||
|
|
||||||
|
const step = intermediateBrightnessStep === -1 ? currentBrightnessStep : intermediateBrightnessStep
|
||||||
|
|
||||||
|
let label: string
|
||||||
|
if (step === 0) {
|
||||||
|
label = "OFF"
|
||||||
|
} else {
|
||||||
|
// Convert step to percentage: step 1 = ~2%, step 43 = 100%
|
||||||
|
const brightnessPercentage = Math.round((step / (LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)) * 100)
|
||||||
|
label = `${brightnessPercentage}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
"flex-1 text-right font-bold font-mono tracking-tigher",
|
||||||
|
step === 0 ? "text-neutral-400" : "text-teal-500 dark:text-teal-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LightSceneTile({
|
||||||
|
className,
|
||||||
|
onSceneChange,
|
||||||
|
}: { className?: string; onSceneChange: (scene: LightSceneConfig) => void }) {
|
||||||
|
const [activeSceneId, setActiveSceneId] = useAtom(sceneAtom)
|
||||||
|
return (
|
||||||
|
<Tile className={cn("h-full flex flex-col justify-start items-start p-1 gap-1", className)}>
|
||||||
|
{Object.entries(DEFAULT_SCENES).map(([id, { icon: Icon, name }]) => (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setActiveSceneId(id)
|
||||||
|
onSceneChange(DEFAULT_SCENES[id])
|
||||||
|
}}
|
||||||
|
key={id}
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
"w-full gap-2 flex flex-row items-end justify-start h-full border tracking-tigher first:rounded-t-lg last:rounded-b-lg transition-all duration-150 active:transition-none",
|
||||||
|
activeSceneId === id
|
||||||
|
? "p-2 border-teal-500 text-teal-500 dark:text-teal-400 border-2 font-bold"
|
||||||
|
: "p-[9px] text-neutral-400 border-neutral-300 dark:border-neutral-800 active:shadow-inner active:bg-neutral-300 dark:active:bg-teal-500 active:text-neutral-900 font-lighter",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon size={16} strokeWidth={2} />
|
||||||
|
<p className="text-md tracking-none leading-none uppercase">{name}</p>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</Tile>
|
||||||
|
)
|
||||||
|
}
|
||||||
110
apps/dashboard/src/use-auto-theme.ts
Normal file
110
apps/dashboard/src/use-auto-theme.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { useEffect } from "react"
|
||||||
|
|
||||||
|
interface SunTimes {
|
||||||
|
sunrise: Date
|
||||||
|
sunset: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateSunTimes(latitude: number, longitude: number, date: Date = new Date()): SunTimes {
|
||||||
|
const julianDay = getJulianDay(date)
|
||||||
|
const julianCentury = (julianDay - 2451545) / 36525
|
||||||
|
|
||||||
|
const geomMeanLongSun = (280.46646 + julianCentury * (36000.76983 + julianCentury * 0.0003032)) % 360
|
||||||
|
const geomMeanAnomSun = 357.52911 + julianCentury * (35999.05029 - 0.0001537 * julianCentury)
|
||||||
|
|
||||||
|
const eccentEarthOrbit = 0.016708634 - julianCentury * (0.000042037 + 0.0000001267 * julianCentury)
|
||||||
|
|
||||||
|
const sunEqOfCtr =
|
||||||
|
Math.sin(toRadians(geomMeanAnomSun)) * (1.914602 - julianCentury * (0.004817 + 0.000014 * julianCentury)) +
|
||||||
|
Math.sin(toRadians(2 * geomMeanAnomSun)) * (0.019993 - 0.000101 * julianCentury) +
|
||||||
|
Math.sin(toRadians(3 * geomMeanAnomSun)) * 0.000289
|
||||||
|
|
||||||
|
const sunTrueLong = geomMeanLongSun + sunEqOfCtr
|
||||||
|
const sunAppLong = sunTrueLong - 0.00569 - 0.00478 * Math.sin(toRadians(125.04 - 1934.136 * julianCentury))
|
||||||
|
|
||||||
|
const meanObliqEcliptic = 23 + (26 + (21.448 - julianCentury * (46.815 + julianCentury * (0.00059 - julianCentury * 0.001813))) / 60) / 60
|
||||||
|
|
||||||
|
const obliqCorr = meanObliqEcliptic + 0.00256 * Math.cos(toRadians(125.04 - 1934.136 * julianCentury))
|
||||||
|
|
||||||
|
const sunDeclin = toDegrees(Math.asin(Math.sin(toRadians(obliqCorr)) * Math.sin(toRadians(sunAppLong))))
|
||||||
|
|
||||||
|
const varY = Math.tan(toRadians(obliqCorr / 2)) * Math.tan(toRadians(obliqCorr / 2))
|
||||||
|
|
||||||
|
const eqOfTime =
|
||||||
|
4 *
|
||||||
|
toDegrees(
|
||||||
|
varY * Math.sin(2 * toRadians(geomMeanLongSun)) -
|
||||||
|
2 * eccentEarthOrbit * Math.sin(toRadians(geomMeanAnomSun)) +
|
||||||
|
4 * eccentEarthOrbit * varY * Math.sin(toRadians(geomMeanAnomSun)) * Math.cos(2 * toRadians(geomMeanLongSun)) -
|
||||||
|
0.5 * varY * varY * Math.sin(4 * toRadians(geomMeanLongSun)) -
|
||||||
|
1.25 * eccentEarthOrbit * eccentEarthOrbit * Math.sin(2 * toRadians(geomMeanAnomSun)),
|
||||||
|
)
|
||||||
|
|
||||||
|
const haSunrise = toDegrees(Math.acos(Math.cos(toRadians(90.833)) / (Math.cos(toRadians(latitude)) * Math.cos(toRadians(sunDeclin))) - Math.tan(toRadians(latitude)) * Math.tan(toRadians(sunDeclin))))
|
||||||
|
|
||||||
|
const solarNoon = (720 - 4 * longitude - eqOfTime) / 1440
|
||||||
|
const sunriseTime = solarNoon - (haSunrise * 4) / 1440
|
||||||
|
const sunsetTime = solarNoon + (haSunrise * 4) / 1440
|
||||||
|
|
||||||
|
const sunrise = new Date(date)
|
||||||
|
sunrise.setHours(0, 0, 0, 0)
|
||||||
|
sunrise.setMinutes(sunriseTime * 1440)
|
||||||
|
|
||||||
|
const sunset = new Date(date)
|
||||||
|
sunset.setHours(0, 0, 0, 0)
|
||||||
|
sunset.setMinutes(sunsetTime * 1440)
|
||||||
|
|
||||||
|
return { sunrise, sunset }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJulianDay(date: Date): number {
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = date.getMonth() + 1
|
||||||
|
const day = date.getDate()
|
||||||
|
const hour = date.getHours()
|
||||||
|
const minute = date.getMinutes()
|
||||||
|
const second = date.getSeconds()
|
||||||
|
|
||||||
|
let a = Math.floor((14 - month) / 12)
|
||||||
|
let y = year + 4800 - a
|
||||||
|
let m = month + 12 * a - 3
|
||||||
|
|
||||||
|
let jdn = day + Math.floor((153 * m + 2) / 5) + 365 * y + Math.floor(y / 4) - Math.floor(y / 100) + Math.floor(y / 400) - 32045
|
||||||
|
|
||||||
|
return jdn + (hour - 12) / 24 + minute / 1440 + second / 86400
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRadians(degrees: number): number {
|
||||||
|
return (degrees * Math.PI) / 180
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDegrees(radians: number): number {
|
||||||
|
return (radians * 180) / Math.PI
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDarkMode(latitude: number, longitude: number): boolean {
|
||||||
|
const now = new Date()
|
||||||
|
const { sunrise, sunset } = calculateSunTimes(latitude, longitude, now)
|
||||||
|
|
||||||
|
return now < sunrise || now > sunset
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAutoTheme(latitude: number, longitude: number) {
|
||||||
|
useEffect(() => {
|
||||||
|
const updateTheme = () => {
|
||||||
|
const shouldBeDark = isDarkMode(latitude, longitude)
|
||||||
|
|
||||||
|
if (shouldBeDark) {
|
||||||
|
document.documentElement.classList.add("dark")
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove("dark")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTheme()
|
||||||
|
|
||||||
|
const interval = setInterval(updateTheme, 60000)
|
||||||
|
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [latitude, longitude])
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
export default {
|
export default {
|
||||||
|
darkMode: 'class',
|
||||||
content: [
|
content: [
|
||||||
"./index.html",
|
"./index.html",
|
||||||
"./src/**/*.{js,ts,jsx,tsx}",
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
|||||||
1
bun.lock
1
bun.lock
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 0,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "monorepo",
|
"name": "monorepo",
|
||||||
|
|||||||
@@ -12,16 +12,7 @@ export type ZigbeeDeviceStates = {
|
|||||||
}
|
}
|
||||||
[ZIGBEE_DEVICE.livingRoomFloorLamp]: {
|
[ZIGBEE_DEVICE.livingRoomFloorLamp]: {
|
||||||
brightness: number
|
brightness: number
|
||||||
level_config: {
|
|
||||||
on_level: "previous"
|
|
||||||
}
|
|
||||||
linkquality: number
|
|
||||||
state: "ON" | "OFF"
|
state: "ON" | "OFF"
|
||||||
update: {
|
|
||||||
installed_version: number
|
|
||||||
latest_version: number
|
|
||||||
state: "available" | "idle"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user