Updated taskbar + added search menu

This commit is contained in:
Prozilla 2023-08-02 21:25:18 +02:00
parent 27a0c480ad
commit 1499e4f178
No known key found for this signature in database
GPG key ID: 5858DFE71CAF31EE
14 changed files with 530 additions and 79 deletions

View file

@ -0,0 +1,25 @@
.App-list {
--scrollbar-color: rgba(0, 0, 0, 25%);
overflow-y: auto;
max-height: 100%;
}
.App-button {
display: flex;
gap: 1rem;
width: 100%;
padding: 0.25rem 0.5rem;
border-radius: 0.5rem;
}
.App-button > div,
.App-button > div > div,
.App-button > div > div > svg {
width: 2rem;
height: 2rem;
}
.App-button > p {
margin: 0;
white-space: nowrap;
}

View file

@ -0,0 +1,69 @@
import { faBatteryEmpty, faBatteryFull, faBatteryHalf, faBatteryQuarter, faBatteryThreeQuarters, faMinus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useEffect, useState } from "react";
import styles from "./Battery.module.css";
export function Battery() {
const [isCharging, setIsCharging] = useState(false);
const [percentage, setPercentage] = useState(100);
// const [chargingTime, setChargingTime] = useState(0);
// const [dischargingTime, setDischargingTime] = useState(0);
useEffect(() => {
navigator.getBattery().then((battery) => {
const updateIsCharging = () => {
setIsCharging(battery.charging);
};
const updatePercentage = () => {
setPercentage(battery.level * 100);
};
// const updateChargingTime = () => {
// setChargingTime(battery.chargingTime);
// };
// const updateDischargingTime = () => {
// setDischargingTime(battery.dischargingTime);
// };
updateIsCharging();
updatePercentage();
// updateChargingTime();
// updateDischargingTime();
battery.addEventListener("chargingchange", updateIsCharging);
battery.addEventListener("levelchange", updatePercentage);
// battery.addEventListener("chargingtimechange", updateChargingTime);
// battery.addEventListener("dischargingtimechange", updateDischargingTime);
return () => {
battery.removeEventListener("chargingchange", updateIsCharging);
battery.removeEventListener("levelchange", updatePercentage);
// battery.removeEventListener("chargingtimechange", updateChargingTime);
// battery.removeEventListener("dischargingtimechange", updateDischargingTime);
}
});
});
let icon = faBatteryFull;
if (percentage < 25) {
icon = faBatteryEmpty;
} else if (percentage < 50) {
icon = faBatteryQuarter;
} else if (percentage < 75) {
icon = faBatteryHalf;
} else if (percentage < 100) {
icon = faBatteryThreeQuarters;
}
return (
<button className={styles.Button} title="Battery">
{!isCharging
? <FontAwesomeIcon className={styles["Charging-indicator"]} icon={faMinus}/>
: null
}
<FontAwesomeIcon icon={icon}/>
</button>
);
}

View file

@ -0,0 +1,13 @@
.Button {
position: relative;
}
.Charging-indicator {
--outline-color: var(--background-color-c);
--outline-width: 2px;
position: absolute;
top: 0.55rem;
right: 0.15rem;
height: 0.7rem !important;
}

View file

@ -0,0 +1,28 @@
import { useEffect, useState } from "react";
import styles from "./Calendar.module.css";
export function Calendar() {
const [date, setDate] = useState(new Date());
useEffect(() => {
setInterval(() => {
setDate(new Date());
}, 30000);
}, []);
return (
<button className={styles.Button} title="Date & Time" style={{ userSelect: "none" }}>
{date.toLocaleString("en-US", {
hour: "numeric",
minute: "numeric",
hour12: false,
})}
<br/>
{date.toLocaleDateString("en-GB", {
day: "numeric",
month: "short",
year: "numeric",
})}
</button>
);
}

View file

@ -0,0 +1,3 @@
.Button {
white-space: nowrap;
}

View file

@ -1,5 +1,6 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import styles from "./HomeMenu.module.css";
import appStyles from "./AppList.module.css";
import { faCircleInfo, faFileLines, faGear, faImage, faPowerOff } from "@fortawesome/free-solid-svg-icons";
import { useWindowsManager } from "../../hooks/windows/WindowsManagerContext.js";
import ApplicationsManager from "../../features/applications/applications.js";
@ -8,7 +9,7 @@ import { closeTab } from "../../features/utils/browser.js";
import { useKeyboardListener } from "../../hooks/utils/keyboard.js";
import { useVirtualRoot } from "../../hooks/virtual-drive/VirtualRootContext.js";
export function HomeMenu({ active, setActive }) {
export function HomeMenu({ active, setActive, search }) {
const windowsManager = useWindowsManager();
const virtualRoot = useVirtualRoot();
@ -23,6 +24,10 @@ export function HomeMenu({ active, setActive }) {
onlyAltKey = true;
} else {
onlyAltKey = false;
if (active && event.key.length === 1) {
search(event.key);
}
}
}
@ -39,7 +44,7 @@ export function HomeMenu({ active, setActive }) {
useKeyboardListener({ onKeyDown, onKeyUp });
return (
<div className={classNames.join(" ")} onKeyDown={onKeyDown}>
<div className={classNames.join(" ")}>
<div className={styles["Container-inner"]}>
<div className={styles.Buttons}>
<button title="Power" onClick={() => { closeTab(); }}>
@ -72,11 +77,11 @@ export function HomeMenu({ active, setActive }) {
</div>
<div className={styles.Apps}>
<h2>Apps</h2>
<div className={styles["App-list"]}>
<div className={appStyles["App-list"]}>
{ApplicationsManager.APPLICATIONS.map(({ name, id }) =>
<button
key={id}
className={styles["App-button"]}
className={appStyles["App-button"]}
onClick={() => {
setActive(false);
windowsManager.open(id);

View file

@ -14,6 +14,11 @@
transition: opacity 200ms ease-in-out, transform 200ms ease-in-out;
}
.Container-outer:not(.Active) {
pointer-events: none;
}
.Container-outer:not(.Active) .Container-inner {
opacity: 0;
transform: translateY(100px);
@ -35,6 +40,8 @@
flex-direction: column-reverse;
align-items: center;
padding: 0.25rem;
margin-top: auto;
z-index: -1;
}
.Buttons > button {
@ -47,10 +54,9 @@
}
.Apps {
--scrollbar-color: rgba(0, 0, 0, 25%);
display: flex;
flex-direction: column;
width: 100%;
width: 100% !important;
max-height: 20rem;
padding: 1rem;
padding-right: 0.25rem;
@ -63,29 +69,4 @@
font-size: 1rem;
margin: 0;
margin-bottom: 1rem;
}
.App-list {
overflow: auto;
max-height: 100%;
}
.App-list > button {
display: flex;
gap: 1rem;
width: 100%;
padding: 0.25rem 0.5rem;
border-radius: 0.5rem;
}
.App-list > button > div,
.App-list > button > div > div,
.App-list > button > div > div > svg {
width: 2rem;
height: 2rem;
}
.App-list > button > p {
margin: 0;
white-space: nowrap;
}

View file

@ -0,0 +1,10 @@
import { faWifi } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export function Network() {
return (
<button title="Wifi">
<FontAwesomeIcon icon={faWifi}/>
</button>
);
}

View file

@ -0,0 +1,57 @@
import styles from "./SearchMenu.module.css";
import appStyles from "./AppList.module.css";
import ApplicationsManager from "../../features/applications/applications.js";
import { useWindowsManager } from "../../hooks/windows/WindowsManagerContext.js";
import { ReactSVG } from "react-svg";
import { useEffect } from "react";
export function SearchMenu({ active, setActive, searchQuery, setSearchQuery, inputRef }) {
const windowsManager = useWindowsManager();
useEffect(() => {
if (inputRef.current)
inputRef.current.focus();
}, [inputRef]);
const onChange = (event) => {
const value = event.target.value;
setSearchQuery(value);
}
const classNames = [styles["Container-outer"]];
if (active)
classNames.push(styles.Active);
return (
<div className={classNames.join(" ")}>
<div className={styles["Container-inner"]}>
<div className={appStyles["App-list"]}>
{ApplicationsManager.APPLICATIONS.filter(({ name }) =>
name.toLowerCase().includes(searchQuery.toLowerCase().trim())
).map(({ name, id }) =>
<button
key={id}
className={appStyles["App-button"]}
onClick={() => {
setActive(false);
windowsManager.open(id);
}}
title={name}
>
<ReactSVG src={`${process.env.PUBLIC_URL}/media/applications/icons/${id}.svg`}/>
<p>{name}</p>
</button>
)}
</div>
<input
ref={inputRef}
className={styles.Input}
value={searchQuery}
onChange={onChange}
spellCheck={false}
autoFocus
/>
</div>
</div>
);
}

View file

@ -0,0 +1,50 @@
.Container-outer {
position: absolute;
left: 0;
bottom: 100%;
height: auto !important;
overflow: hidden;
}
.Container-inner {
opacity: 1;
background-color: rgba(25, 25, 25, 75%);
backdrop-filter: blur(15px);
transform: none;
transition: opacity 200ms ease-in-out, transform 200ms ease-in-out;
}
.Container-outer:not(.Active) {
pointer-events: none;
}
.Container-outer:not(.Active) .Container-inner {
opacity: 0;
transform: translateY(100px);
}
.Container-inner {
display: flex;
flex-direction: column;
min-width: 13rem;
max-width: 29rem;
max-height: 20rem;
padding: 0.5rem;
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
overflow: hidden;
resize: horizontal;
}
.Input {
width: 100%;
padding: 0.25rem 0.5rem;
color: var(--foreground-color-a);
background-color: rgba(0, 0, 0, 25%);
border: none;
border-radius: 0.5rem;
outline: none;
font-family: inherit;
font-size: inherit;
}

View file

@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import styles from "./TaskBar.module.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBatteryFull, faSearch, faVolumeHigh, faWifi } from "@fortawesome/free-solid-svg-icons";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import ApplicationsManager from "../../features/applications/applications.js";
import { useWindows } from "../../hooks/windows/WindowsContext.js";
import { useWindowsManager } from "../../hooks/windows/WindowsManagerContext.js";
@ -10,6 +10,12 @@ import { ReactSVG } from "react-svg";
import Application from "../../features/applications/application.js";
import { HomeMenu } from "./HomeMenu.jsx";
import OutsideClickListener from "../../hooks/utils/outsideClick.js";
import { Battery } from "./Battery.jsx";
import { Network } from "./Network.jsx";
import { Volume } from "./Volume.jsx";
import { SearchMenu } from "./SearchMenu.jsx";
import { Calendar } from "./Calendar.jsx";
import { useScrollWithShadow } from "../../hooks/utils/scrollWithShadows.js";
/**
* @param {Object} props
@ -41,56 +47,99 @@ function AppButton({ app }) {
}
export function Taskbar() {
const [date, setDate] = useState(new Date());
const [showHome, setShowHome] = useState(false);
const [showSearch, setShowSearch] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const ref = useRef(null);
const { boxShadow, onUpdate } = useScrollWithShadow({
ref,
shadow: {
offset: 20,
blurRadius: 10,
spreadRadius: -10,
color: {
a: 25
}
}
});
const inputRef = useRef(null);
useEffect(() => {
setInterval(() => {
setDate(new Date());
}, 30000);
}, []);
const updateShowHome = (value) => {
setShowHome(value);
if (value) {
setShowSearch(false);
}
};
const updateShowSearch = (value) => {
setShowSearch(value);
if (value) {
if (searchQuery !== "") {
setSearchQuery("");
}
setShowHome(false);
if (inputRef.current) {
inputRef.current.focus();
}
} else {
setTimeout(() => {
if (!showSearch) {
setSearchQuery("");
}
}, 200);
}
};
const search = (query) => {
updateShowSearch(true);
}
return (
<div className={styles["Task-bar"]}>
<div className={styles["Program-icons"]}>
<div className={styles["Menu-icons"]}>
<div className={styles["Home-container"]}>
<OutsideClickListener onOutsideClick={() => { setShowHome(false); }}>
<button title="Home" className={styles["Home-button"]} onClick={() => { setShowHome(!showHome); }}>
<OutsideClickListener onOutsideClick={() => { updateShowHome(false); }}>
<button title="Home"
className={`${styles["Menu-button"]} ${styles["Home-button"]}`}
onClick={() => { updateShowHome(!showHome); }}
>
<ReactSVG src={process.env.PUBLIC_URL + "/media/logo.svg"}/>
</button>
<HomeMenu active={showHome} setActive={setShowHome}/>
<HomeMenu active={showHome} setActive={updateShowHome} search={search}/>
</OutsideClickListener>
</div>
<button title="Search">
<FontAwesomeIcon icon={faSearch}/>
</button>
<div className={styles["Search-container"]}>
<OutsideClickListener onOutsideClick={() => { updateShowSearch(false); }}>
<button title="Search"
className={`${styles["Menu-button"]} ${styles["Search-button"]}`}
onClick={() => { updateShowSearch(!showSearch); }}
>
<FontAwesomeIcon icon={faSearch}/>
</button>
<SearchMenu
active={showSearch}
setActive={updateShowSearch}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
inputRef={inputRef}
/>
</OutsideClickListener>
</div>
</div>
<div className={styles["App-icons"]} onScroll={onUpdate} onResize={onUpdate} ref={ref} style={{ boxShadow }}>
{ApplicationsManager.APPLICATIONS.map((app) =>
<AppButton app={app} key={app.id}/>
)}
</div>
<div className={styles["Util-icons"]}>
<button title="Battery">
<FontAwesomeIcon icon={faBatteryFull}/>
</button>
<button title="Wifi">
<FontAwesomeIcon icon={faWifi}/>
</button>
<button title="Volume">
<FontAwesomeIcon icon={faVolumeHigh}/>
</button>
<button title="Date & Time" style={{ userSelect: "none" }}>
{date.toLocaleString("en-US", {
hour: "numeric",
minute: "numeric",
hour12: false,
})}
<br/>
{date.toLocaleDateString("en-GB", {
day: "numeric",
month: "short",
year: "numeric",
})}
</button>
<Battery/>
<Network/>
<Volume/>
<Calendar/>
<button title="View Desktop" id="desktop-button"/>
</div>
</div>

View file

@ -7,7 +7,6 @@
.Task-bar {
position: fixed;
display: flex;
justify-content: space-between;
bottom: 0;
left: 0;
width: 100%;
@ -26,13 +25,15 @@
border: none;
outline: none;
transition: background-color 100ms ease-in-out;
z-index: -1;
}
.Task-bar button:hover {
background-color: var(--task-bar-button-hover-color);
}
.Home-container {
.Home-container,
.Search-container {
position: relative;
padding: 0 !important;
}
@ -42,27 +43,37 @@
filter: none;
}
.Program-icons {
.Menu-icons,
.App-icons {
display: flex;
align-items: center;
height: 100%;
z-index: -1;
}
.Program-icons > *, .Home-button {
.App-icons {
overflow-x: auto;
}
.App-icons::-webkit-scrollbar {
display: none;
}
.App-icons > *,
.Menu-button {
height: 100%;
padding: 0.75rem;
}
.Program-icons > * > svg {
.App-icons > * > svg,
.Menu-button > svg {
height: 1.25rem;
}
.Program-icons > * > div,
.Program-icons > * > div > div,
.Program-icons > * > div > div > svg,
.Home-button > div,
.Home-button > div > div,
.Home-button > div > div > svg {
.Menu-icons div,
.Menu-icons div > svg,
.App-icons div,
.App-icons div > svg {
height: 100%;
width: auto;
}
@ -97,6 +108,9 @@
justify-content: flex-end;
align-items: center;
height: 100%;
margin-left: auto;
padding-left: 0.5rem;
z-index: -1;
}
.Util-icons > * {

View file

@ -0,0 +1,10 @@
import { faVolumeHigh } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export function Volume() {
return (
<button title="Volume">
<FontAwesomeIcon icon={faVolumeHigh}/>
</button>
);
}

View file

@ -0,0 +1,137 @@
/**
* https://medium.com/dfind-consulting/react-scroll-hook-with-shadows-9ba2d47ae32
*/
// eslint-disable-next-line no-unused-vars
import React, { useCallback, useEffect, useRef } from "react";
import { useState } from "react";
/**
* @param {Object} options
* @param {React.ElementRef} options.ref
* @param {Boolean=true} options.horizontal
* @param {Boolean=true} options.dynamicOffset
* @param {Number=1} options.dynamicOffsetFactor
* @param {Object} options.shadow
* @param {Number=8} options.shadow.offset
* @param {Number=5} options.shadow.blurRadius
* @param {Number=-5} options.shadow.spreadRadius
* @param {Object} options.shadow.color
* @param {Number=0} options.shadow.color.r
* @param {Number=0} options.shadow.color.g
* @param {Number=0} options.shadow.color.b
* @param {Number=50} options.shadow.color.a
*/
export function useScrollWithShadow(options) {
const [initiated, setInitiated] = useState(false);
const [scrollStart, setScrollStart] = useState(0);
const [scrollLength, setScrollLength] = useState(0);
const [clientLength, setClientLength] = useState(0);
if (options == null)
options = {};
if (options.shadow == null)
options.shadow = {};
if (options.shadow.color == null)
options.shadow.color = {};
const {
ref,
horizontal = true,
dynamicOffset = true,
dynamicOffsetFactor = 3,
shadow: {
offset = 8,
blurRadius = 5,
spreadRadius = -5,
color: {
r = 0,
g = 0,
b = 0,
a = 50
}
}
} = options;
const updateValues = useCallback((element) => {
if (!element)
return;
console.log(element);
setScrollStart(horizontal ? element.scrollLeft : element.scrollTop);
setScrollLength(horizontal ? element.scrollWidth : element.scrollHeight);
setClientLength(horizontal ? element.clientWidth : element.clientHeight);
}, [horizontal]);
const onUpdate = (event) => {
console.log(event);
updateValues(event.target);
};
useEffect(() => {
const onResize = () => {
updateValues(ref.current);
};
if (ref.current && !initiated) {
console.log(ref.current);
setInitiated(true);
updateValues(ref.current);
}
window.addEventListener("resize", onResize);
return () => {
window.removeEventListener("resize", onResize);
}
}, [ref, updateValues, initiated]);
const getBoxShadow = () => {
const startDistance = scrollStart;
const endDistance = scrollLength - scrollStart - clientLength;
const isStart = startDistance === 0;
const isEnd = endDistance === 0;
const isBetween = scrollStart > 0 && clientLength < scrollLength - scrollStart;
let startOffset = offset;
let endOffset = offset;
if (dynamicOffset) {
startOffset = startDistance * dynamicOffsetFactor - offset;
endOffset = endDistance * dynamicOffsetFactor - offset;
if (startOffset > offset) {
startOffset = offset;
} else if (startOffset < 0) {
startOffset = 0;
}
if (endOffset > offset) {
endOffset = offset;
} else if (endOffset < 0) {
endOffset = 0;
}
}
const shadowStartOffset = horizontal ? `${startOffset}px 0` : `0 ${startOffset}px`;
const shadowEndOffset = horizontal ? `-${endOffset}px 0` : `0 -${endOffset}px`;
const start = `inset ${shadowStartOffset} ${blurRadius}px ${spreadRadius}px rgba(${r}, ${g}, ${b}, ${a}%)`;
const end = `inset ${shadowEndOffset} ${blurRadius}px ${spreadRadius}px rgba(${r}, ${g}, ${b}, ${a}%)`;
let boxShadow = "none";
if (isStart) {
boxShadow = end;
} else if (isBetween) {
boxShadow = `${start}, ${end}`;
} else if (isEnd) {
boxShadow = start;
}
return boxShadow;
};
return { boxShadow: getBoxShadow(), onUpdate };
}