diff --git a/src/components/task-bar/AppList.module.css b/src/components/task-bar/AppList.module.css new file mode 100644 index 0000000..84ba1c6 --- /dev/null +++ b/src/components/task-bar/AppList.module.css @@ -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; +} \ No newline at end of file diff --git a/src/components/task-bar/Battery.jsx b/src/components/task-bar/Battery.jsx new file mode 100644 index 0000000..77948f9 --- /dev/null +++ b/src/components/task-bar/Battery.jsx @@ -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 ( + + ); +} \ No newline at end of file diff --git a/src/components/task-bar/Battery.module.css b/src/components/task-bar/Battery.module.css new file mode 100644 index 0000000..cb39076 --- /dev/null +++ b/src/components/task-bar/Battery.module.css @@ -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; +} \ No newline at end of file diff --git a/src/components/task-bar/Calendar.jsx b/src/components/task-bar/Calendar.jsx new file mode 100644 index 0000000..0f50e26 --- /dev/null +++ b/src/components/task-bar/Calendar.jsx @@ -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 ( + + ); +} \ No newline at end of file diff --git a/src/components/task-bar/Calendar.module.css b/src/components/task-bar/Calendar.module.css new file mode 100644 index 0000000..a9012e9 --- /dev/null +++ b/src/components/task-bar/Calendar.module.css @@ -0,0 +1,3 @@ +.Button { + white-space: nowrap; +} \ No newline at end of file diff --git a/src/components/task-bar/HomeMenu.jsx b/src/components/task-bar/HomeMenu.jsx index 4dac8ed..1d7cadf 100644 --- a/src/components/task-bar/HomeMenu.jsx +++ b/src/components/task-bar/HomeMenu.jsx @@ -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 ( -
+

Apps

-
+
{ApplicationsManager.APPLICATIONS.map(({ name, id }) => + ); +} \ No newline at end of file diff --git a/src/components/task-bar/SearchMenu.jsx b/src/components/task-bar/SearchMenu.jsx new file mode 100644 index 0000000..011c8c8 --- /dev/null +++ b/src/components/task-bar/SearchMenu.jsx @@ -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 ( +
+
+
+ {ApplicationsManager.APPLICATIONS.filter(({ name }) => + name.toLowerCase().includes(searchQuery.toLowerCase().trim()) + ).map(({ name, id }) => + + )} +
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/components/task-bar/SearchMenu.module.css b/src/components/task-bar/SearchMenu.module.css new file mode 100644 index 0000000..b8523b3 --- /dev/null +++ b/src/components/task-bar/SearchMenu.module.css @@ -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; +} \ No newline at end of file diff --git a/src/components/task-bar/TaskBar.jsx b/src/components/task-bar/TaskBar.jsx index c521b0a..c76161f 100644 --- a/src/components/task-bar/TaskBar.jsx +++ b/src/components/task-bar/TaskBar.jsx @@ -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 (
-
+
- { setShowHome(false); }}> - - +
- +
+ { updateShowSearch(false); }}> + + + +
+
+
{ApplicationsManager.APPLICATIONS.map((app) => )}
- - - - + + + +
diff --git a/src/components/task-bar/TaskBar.module.css b/src/components/task-bar/TaskBar.module.css index f41a0d6..0c47e76 100644 --- a/src/components/task-bar/TaskBar.module.css +++ b/src/components/task-bar/TaskBar.module.css @@ -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 > * { diff --git a/src/components/task-bar/Volume.jsx b/src/components/task-bar/Volume.jsx new file mode 100644 index 0000000..6694b48 --- /dev/null +++ b/src/components/task-bar/Volume.jsx @@ -0,0 +1,10 @@ +import { faVolumeHigh } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +export function Volume() { + return ( + + ); +} \ No newline at end of file diff --git a/src/hooks/utils/scrollWithShadows.js b/src/hooks/utils/scrollWithShadows.js new file mode 100644 index 0000000..f33ae52 --- /dev/null +++ b/src/hooks/utils/scrollWithShadows.js @@ -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 }; +} \ No newline at end of file