diff --git a/public/config/desktop.xml b/public/config/desktop.xml index 25db787..8baa742 100644 --- a/public/config/desktop.xml +++ b/public/config/desktop.xml @@ -1,5 +1,3 @@ - - /media/wallpapers/mesh-gradient-wallpaper-red-purple.png \ No newline at end of file diff --git a/public/config/taskbar.xml b/public/config/taskbar.xml new file mode 100644 index 0000000..c8f1bce --- /dev/null +++ b/public/config/taskbar.xml @@ -0,0 +1,3 @@ + + terminal,settings,media-viewer,text-editor,file-explorer,wordle,balls,minesweeper + \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index c91b140..96ea759 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,5 @@ import styles from "./App.module.css"; -import { Taskbar } from "./components/task-bar/TaskBar.jsx"; +import { Taskbar } from "./components/taskbar/Taskbar.jsx"; import { WindowsManagerProvider } from "./hooks/windows/windowsManagerContext.js"; import { WindowsView } from "./components/windows/WindowsView.jsx"; import { VirtualRootProvider } from "./hooks/virtual-drive/virtualRootContext.js"; diff --git a/src/components/actions/Actions.jsx b/src/components/actions/Actions.jsx index a0e2121..e351724 100644 --- a/src/components/actions/Actions.jsx +++ b/src/components/actions/Actions.jsx @@ -1,12 +1,12 @@ import { Children, cloneElement, isValidElement, useEffect, useRef, useState } from "react"; import { useShortcuts } from "../../hooks/utils/keyboard.js"; -import style from "./Actions.module.css"; +import styles from "./Actions.module.css"; import { useScreenDimensions } from "../../hooks/utils/screen.js"; -import { TASK_BAR_HEIGHT } from "../../constants/taskBar.js"; +import { TASKBAR_HEIGHT } from "../../constants/taskBar.js"; export const STYLES = { - CONTEXT_MENU: style["Context-menu"], - SHORTCUTS_LISTENER: style["Shortcuts-listener"], + CONTEXT_MENU: styles["Context-menu"], + SHORTCUTS_LISTENER: styles["Shortcuts-listener"], }; /** @@ -16,7 +16,7 @@ export const STYLES = { * @param {Function} props.onAnyTrigger * @param {import("react").ElementType} props.children * @param {*} props.triggerParams - * @param {boolean} props.avoidTaskBar + * @param {boolean} props.avoidTaskbar */ /** @@ -31,7 +31,7 @@ export const STYLES = { * }} * /> */ -export function Actions({ children, className, onAnyTrigger, triggerParams, avoidTaskBar = true }) { +export function Actions({ children, className, onAnyTrigger, triggerParams, avoidTaskbar = true }) { const isListener = (className === STYLES.SHORTCUTS_LISTENER); const ref = useRef(null); @@ -88,8 +88,8 @@ export function Actions({ children, className, onAnyTrigger, triggerParams, avoi const maxX = screenWidth; let maxY = screenHeight; - if (avoidTaskBar) - maxY -= TASK_BAR_HEIGHT; + if (avoidTaskbar) + maxY -= TASKBAR_HEIGHT; const isOverflowingRight = (rect.x + rect.width > maxX); const isOverflowingBottom = (rect.y + rect.height > maxY); @@ -100,20 +100,20 @@ export function Actions({ children, className, onAnyTrigger, triggerParams, avoi setAlignTop(true); setInitiated(true); - }, [alignLeft, avoidTaskBar, screenHeight, screenWidth]); + }, [alignLeft, avoidTaskbar, screenHeight, screenWidth]); if (isListener) return iterateOverChildren(children); - const classNames = [style.Container]; + const classNames = [styles.Container]; if (className != null) classNames.push(className); if (alignLeft) - classNames.push(style["Align-left"]); + classNames.push(styles["Align-left"]); if (alignTop) - classNames.push(style["Align-top"]); + classNames.push(styles["Align-top"]); if (!initiated) - classNames.push(style.Uninitiated); + classNames.push(styles.Uninitiated); return (
{iterateOverChildren(children)} diff --git a/src/components/task-bar/AppButton.jsx b/src/components/task-bar/AppButton.jsx deleted file mode 100644 index 6d1da2b..0000000 --- a/src/components/task-bar/AppButton.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import { memo, useEffect, useState } from "react"; -import Application from "../../features/applications/application.js"; -import { useWindows } from "../../hooks/windows/windowsContext.js"; -import { useWindowsManager } from "../../hooks/windows/windowsManagerContext.js"; -import styles from "./TaskBar.module.css"; -import { ReactSVG } from "react-svg"; - -/** - * @param {object} props - * @param {Application} props.app - */ -export const AppButton = memo(({ app }) => { - const [active, setActive] = useState(false); - const windows = useWindows(); - const windowsManager = useWindowsManager(); - - useEffect(() => { - setActive(windowsManager.isAppActive(app.id)); - }, [app.id, windows, windowsManager]); - - const classNames = [styles["App-icon"]]; - if (active) - classNames.push(styles.Active); - - return ( - - ); -}); \ No newline at end of file diff --git a/src/components/task-bar/TaskBar.jsx b/src/components/taskbar/Taskbar.jsx similarity index 71% rename from src/components/task-bar/TaskBar.jsx rename to src/components/taskbar/Taskbar.jsx index 3d5ae90..7d3a2a3 100644 --- a/src/components/task-bar/TaskBar.jsx +++ b/src/components/taskbar/Taskbar.jsx @@ -1,5 +1,5 @@ -import { memo, useRef, useState } from "react"; -import styles from "./TaskBar.module.css"; +import { memo, useEffect, useMemo, useRef, useState } from "react"; +import styles from "./Taskbar.module.css"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCog, faSearch } from "@fortawesome/free-solid-svg-icons"; import AppsManager from "../../features/applications/applications.js"; @@ -12,7 +12,7 @@ import { Volume } from "./indicators/Volume.jsx"; import { SearchMenu } from "./menus/SearchMenu.jsx"; import { Calendar } from "./indicators/Calendar.jsx"; import { useScrollWithShadow } from "../../hooks/utils/scrollWithShadows.js"; -import { AppButton } from "./AppButton.jsx"; +import { AppButton } from "./app-icon/AppIcon.jsx"; import { useContextMenu } from "../../hooks/modals/contextMenu.js"; import { Actions } from "../actions/Actions.jsx"; import { useModals } from "../../hooks/modals/modals.js"; @@ -20,13 +20,17 @@ import { ClickAction } from "../actions/actions/ClickAction.jsx"; import { APPS, APP_NAMES } from "../../constants/applications.js"; import { useWindowsManager } from "../../hooks/windows/windowsManagerContext.js"; import { ModalsView } from "../modals/ModalsView.jsx"; -import { TASK_BAR_HEIGHT } from "../../constants/taskBar.js"; +import { TASKBAR_HEIGHT } from "../../constants/taskBar.js"; +import { useSettingsManager } from "../../hooks/settings/settingsManagerContext.js"; +import { SettingsManager } from "../../features/settings/settingsManager.js"; +import { useWindows } from "../../hooks/windows/windowsContext.js"; export const Taskbar = memo(() => { + const ref = useRef(null); + const settingsManager = useSettingsManager(); 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, @@ -36,13 +40,49 @@ export const Taskbar = memo(() => { const inputRef = useRef(null); const [modalsManager, modals] = useModals(); const windowsManager = useWindowsManager(); + const windows = useWindows(); const { onContextMenu } = useContextMenu({ modalsManager, Actions: (props) => - + { windowsManager.open(APPS.SETTINGS); }}/> }); + const [pins, setPins] = useState([]); + + const apps = useMemo(() => AppsManager.APPLICATIONS.sort((appA, appB) => { + const indexA = pins.indexOf(appA.id); + const indexB = pins.indexOf(appB.id); + if (indexA < 0 && indexB > 0) { + return 1; + } else if (indexA > 0 && indexB < 0) { + return -1; + } else if (indexA < 0 && indexB < 0) { + return 0; + } else { + return indexA - indexB; + } + }).map((app) => { + const isActive = windows.map((window) => window.app.id).includes(app.id); + const shouldBeShown = (pins.includes(app.id) || isActive); + return (); + }), [modalsManager, pins, windows]); + + useEffect(() => { + (async () => { + const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.taskbar); + settings.get("pins", (pins) => { + setPins(pins.split(",")); + }); + })(); + }, [settingsManager]); const updateShowHome = (value) => { setShowHome(value); @@ -82,8 +122,8 @@ export const Taskbar = memo(() => { return (<>
{ if (event.target.getAttribute("data-allow-context-menu")) @@ -132,9 +172,7 @@ export const Taskbar = memo(() => { onResize={onUpdate} ref={ref} > - {AppsManager.APPLICATIONS.map((app) => - - )} + {apps}
@@ -142,7 +180,7 @@ export const Taskbar = memo(() => { -
); diff --git a/src/components/task-bar/TaskBar.module.css b/src/components/taskbar/Taskbar.module.css similarity index 65% rename from src/components/task-bar/TaskBar.module.css rename to src/components/taskbar/Taskbar.module.css index 4519c6c..2229971 100644 --- a/src/components/task-bar/TaskBar.module.css +++ b/src/components/taskbar/Taskbar.module.css @@ -1,22 +1,22 @@ :root { - --task-bar-height: 3rem; - --task-bar-color: rgba(0, 0, 0, 75%); - --task-bar-button-hover-color: rgba(255, 255, 255, 5%); + --taskbar-height: 3rem; + --taskbar-color: rgba(0, 0, 0, 75%); + --taskbar-button-hover-color: rgba(255, 255, 255, 5%); } -.Task-bar { +.Taskbar { position: fixed; display: flex; bottom: 0; left: 0; width: 100%; - height: var(--task-bar-height); - background-color: var(--task-bar-color); + height: var(--taskbar-height); + background-color: var(--taskbar-color); backdrop-filter: blur(15px); z-index: 10; } -.Task-bar button { +.Taskbar button { display: flex; align-items: center; color: var(--foreground-color-a); @@ -28,9 +28,9 @@ z-index: -1; } -.Task-bar button:hover, -.Task-bar button:focus-visible { - background-color: var(--task-bar-button-hover-color); +.Taskbar button:hover, +.Taskbar button:focus-visible { + background-color: var(--taskbar-button-hover-color); } .Home-container, @@ -94,35 +94,6 @@ width: auto; } -.App-icon { - position: relative; - width: var(--task-bar-height); - height: var(--task-bar-height); - aspect-ratio: 1; -} - -.App-icon::after { - content: ""; - position: absolute; - left: 50%; - bottom: 0; - width: 90%; - height: 0.15rem; - background-color: var(--grey-a); - transition: height 200ms ease-in-out, width 200ms ease-in-out; - transform: translateX(-50%); - -webkit-transform: translateX(-50%); -} - -.App-icon:hover::after, -.App-icon:focus-visible::after { - width: 100%; -} - -.App-icon:not(.Active)::after { - height: 0; -} - .Util-icons { display: flex; gap: 0; diff --git a/src/components/taskbar/app-icon/AppIcon.jsx b/src/components/taskbar/app-icon/AppIcon.jsx new file mode 100644 index 0000000..d197e8c --- /dev/null +++ b/src/components/taskbar/app-icon/AppIcon.jsx @@ -0,0 +1,66 @@ +import { memo } from "react"; +import Application from "../../../features/applications/application.js"; +import { useWindowsManager } from "../../../hooks/windows/windowsManagerContext.js"; +import styles from "./AppIcon.module.css"; +import { ReactSVG } from "react-svg"; +import { useSettingsManager } from "../../../hooks/settings/settingsManagerContext.js"; +import { useContextMenu } from "../../../hooks/modals/contextMenu.js"; +import ModalsManager from "../../../features/modals/modals.js"; +import { Actions } from "../../actions/Actions.jsx"; +import { ClickAction } from "../../actions/actions/ClickAction.jsx"; +import { faThumbTack, faTimes } from "@fortawesome/free-solid-svg-icons"; +import { SettingsManager } from "../../../features/settings/settingsManager.js"; +import { removeFromArray } from "../../../features/utils/array.js"; + +/** + * @param {object} props + * @param {Application} props.app + * @param {ModalsManager} props.modalsManager + * @param {string[]} props.pins + */ +export const AppButton = memo(({ app, modalsManager, pins, active, visible }) => { + const isPinned = pins.includes(app.id); + + const windowsManager = useWindowsManager(); + const settingsManager = useSettingsManager(); + const { onContextMenu } = useContextMenu({ modalsManager, Actions: (props) => + + { + const newPins = [...pins]; + if (isPinned) { + removeFromArray(app.id, pins); + } else { + newPins.push(app.id); + } + + const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.taskbar); + settings.set("pins", newPins.join(",")); + }}/> + {active && { + windowsManager.close(windowsManager.getAppWindowId(app.id)); + }}/>} + + }); + + const classNames = [styles["App-icon"]]; + if (active) + classNames.push(styles.Active); + if (!visible) + classNames.push(styles.Hidden); + + return ( + + ); +}); \ No newline at end of file diff --git a/src/components/taskbar/app-icon/AppIcon.module.css b/src/components/taskbar/app-icon/AppIcon.module.css new file mode 100644 index 0000000..66b8ffa --- /dev/null +++ b/src/components/taskbar/app-icon/AppIcon.module.css @@ -0,0 +1,58 @@ +.App-icon { + opacity: 1; + position: relative; + width: var(--taskbar-height); + height: var(--taskbar-height); + aspect-ratio: 1; + animation: appear 100ms ease-out forwards; +} + +.App-icon.Hidden { + animation: disappear 400ms ease-in-out forwards; + pointer-events: none; +} + +@keyframes appear { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes disappear { + 0% { + opacity: 1; + } + 25% { + opacity: 0; + margin-right: 0; + } + 100% { + opacity: 0; + margin-right: calc(var(--taskbar-height) * -1); + } +} + +.App-icon::after { + content: ""; + position: absolute; + left: 50%; + bottom: 0; + width: 90%; + height: 0.15rem; + background-color: var(--grey-a); + transition: height 200ms ease-in-out, width 200ms ease-in-out; + transform: translateX(-50%); + -webkit-transform: translateX(-50%); +} + +.App-icon:hover::after, +.App-icon:focus-visible::after { + width: 100%; +} + +.App-icon:not(.Active)::after { + height: 0; +} \ No newline at end of file diff --git a/src/components/task-bar/indicators/Battery.jsx b/src/components/taskbar/indicators/Battery.jsx similarity index 100% rename from src/components/task-bar/indicators/Battery.jsx rename to src/components/taskbar/indicators/Battery.jsx diff --git a/src/components/task-bar/indicators/Battery.module.css b/src/components/taskbar/indicators/Battery.module.css similarity index 100% rename from src/components/task-bar/indicators/Battery.module.css rename to src/components/taskbar/indicators/Battery.module.css diff --git a/src/components/task-bar/indicators/Calendar.jsx b/src/components/taskbar/indicators/Calendar.jsx similarity index 100% rename from src/components/task-bar/indicators/Calendar.jsx rename to src/components/taskbar/indicators/Calendar.jsx diff --git a/src/components/task-bar/indicators/Calendar.module.css b/src/components/taskbar/indicators/Calendar.module.css similarity index 100% rename from src/components/task-bar/indicators/Calendar.module.css rename to src/components/taskbar/indicators/Calendar.module.css diff --git a/src/components/task-bar/indicators/Network.jsx b/src/components/taskbar/indicators/Network.jsx similarity index 100% rename from src/components/task-bar/indicators/Network.jsx rename to src/components/taskbar/indicators/Network.jsx diff --git a/src/components/task-bar/indicators/Volume.jsx b/src/components/taskbar/indicators/Volume.jsx similarity index 100% rename from src/components/task-bar/indicators/Volume.jsx rename to src/components/taskbar/indicators/Volume.jsx diff --git a/src/components/task-bar/menus/AppList.module.css b/src/components/taskbar/menus/AppList.module.css similarity index 100% rename from src/components/task-bar/menus/AppList.module.css rename to src/components/taskbar/menus/AppList.module.css diff --git a/src/components/task-bar/menus/HomeMenu.jsx b/src/components/taskbar/menus/HomeMenu.jsx similarity index 100% rename from src/components/task-bar/menus/HomeMenu.jsx rename to src/components/taskbar/menus/HomeMenu.jsx diff --git a/src/components/task-bar/menus/HomeMenu.module.css b/src/components/taskbar/menus/HomeMenu.module.css similarity index 100% rename from src/components/task-bar/menus/HomeMenu.module.css rename to src/components/taskbar/menus/HomeMenu.module.css diff --git a/src/components/task-bar/menus/SearchMenu.jsx b/src/components/taskbar/menus/SearchMenu.jsx similarity index 100% rename from src/components/task-bar/menus/SearchMenu.jsx rename to src/components/taskbar/menus/SearchMenu.jsx diff --git a/src/components/task-bar/menus/SearchMenu.module.css b/src/components/taskbar/menus/SearchMenu.module.css similarity index 100% rename from src/components/task-bar/menus/SearchMenu.module.css rename to src/components/taskbar/menus/SearchMenu.module.css diff --git a/src/components/windows/WindowView.module.css b/src/components/windows/WindowView.module.css index 646e04e..4e65545 100644 --- a/src/components/windows/WindowView.module.css +++ b/src/components/windows/WindowView.module.css @@ -25,7 +25,7 @@ .Window-container.Maximized { width: 100%; - height: calc(100% - var(--task-bar-height)); + height: calc(100% - var(--taskbar-height)); border-radius: 0; transform: none !important; resize: none; diff --git a/src/constants/taskBar.js b/src/constants/taskBar.js index f2adc65..a42f494 100644 --- a/src/constants/taskBar.js +++ b/src/constants/taskBar.js @@ -1 +1 @@ -export const TASK_BAR_HEIGHT = 3 * 16; \ No newline at end of file +export const TASKBAR_HEIGHT = 3 * 16; \ No newline at end of file diff --git a/src/features/settings/settingsManager.js b/src/features/settings/settingsManager.js index 12f335d..1576348 100644 --- a/src/features/settings/settingsManager.js +++ b/src/features/settings/settingsManager.js @@ -6,7 +6,8 @@ export class SettingsManager { * @type {Object} */ static VIRTUAL_PATHS = { - "desktop": "~/.config/desktop.xml" + desktop: "~/.config/desktop.xml", + taskbar: "~/.config/taskbar.xml" }; /** diff --git a/src/features/virtual-drive/virtualRoot.js b/src/features/virtual-drive/virtualRoot.js index a9f40d8..7c2e7c3 100644 --- a/src/features/virtual-drive/virtualRoot.js +++ b/src/features/virtual-drive/virtualRoot.js @@ -55,6 +55,8 @@ export class VirtualRoot extends VirtualFolder { .createFolder(".config", (folder) => { folder.createFile("desktop", "xml", (file) => { file.setSource("/config/desktop.xml"); + }).createFile("taskbar", "xml", (file) => { + file.setSource("/config/taskbar.xml"); }); }) .createFolder("Images", (folder) => { diff --git a/src/features/windows/windows.js b/src/features/windows/windows.js index 10a39b1..b90845c 100644 --- a/src/features/windows/windows.js +++ b/src/features/windows/windows.js @@ -1,4 +1,4 @@ -import { TASK_BAR_HEIGHT } from "../../constants/taskBar.js"; +import { TASKBAR_HEIGHT } from "../../constants/taskBar.js"; import { SCREEN_MARGIN } from "../../constants/windows.js"; import AppsManager from "../applications/applications.js"; import { randomRange } from "../math/random.js"; @@ -26,7 +26,7 @@ export default class WindowsManager { const size = options?.size ?? app.windowOptions?.size ?? new Vector2(700, 400); const position = new Vector2(randomRange(SCREEN_MARGIN, window.innerWidth - size.x - SCREEN_MARGIN), - randomRange(SCREEN_MARGIN, window.innerHeight - size.y - SCREEN_MARGIN - TASK_BAR_HEIGHT)); + randomRange(SCREEN_MARGIN, window.innerHeight - size.y - SCREEN_MARGIN - TASKBAR_HEIGHT)); let id = 0; while (this.windowIds.includes(id.toString())) { @@ -46,6 +46,8 @@ export default class WindowsManager { lastInteraction: new Date().valueOf() }; + app.isActive = true; + this.updateWindows(this.windows); return this.windows[id]; // console.log(this); @@ -72,6 +74,9 @@ export default class WindowsManager { console.warn(`Failed to close window ${windowId}: window not found`); return; } + + const { app } = this.windows[windowId]; + app.isActive = this.isAppActive(app.id); console.info(`Closing window ${windowId}`); delete this.windows[windowId]; @@ -114,6 +119,23 @@ export default class WindowsManager { return active; } + /** + * @param {string} appId + * @returns {string} + */ + getAppWindowId(appId) { + let windowId = null; + + Object.values(this.windows).forEach((window) => { + if (window.app.id === appId) { + windowId = window.id; + return; + } + }); + + return windowId; + } + /** * @param {Function} updateWindows */