Updated taskbar

This commit is contained in:
Prozilla 2023-12-13 21:06:06 +01:00
parent 5263cfd2b6
commit 861851872a
No known key found for this signature in database
GPG key ID: 5858DFE71CAF31EE
25 changed files with 231 additions and 108 deletions

View file

@ -1,5 +1,3 @@
<?xml version="1.0"?>
<!DOCTYPE xml>
<options> <options>
<wallpaper>/media/wallpapers/mesh-gradient-wallpaper-red-purple.png</wallpaper> <wallpaper>/media/wallpapers/mesh-gradient-wallpaper-red-purple.png</wallpaper>
</options> </options>

View file

@ -0,0 +1,3 @@
<options>
<pins>terminal,settings,media-viewer,text-editor,file-explorer,wordle,balls,minesweeper</pins>
</options>

View file

@ -1,5 +1,5 @@
import styles from "./App.module.css"; 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 { WindowsManagerProvider } from "./hooks/windows/windowsManagerContext.js";
import { WindowsView } from "./components/windows/WindowsView.jsx"; import { WindowsView } from "./components/windows/WindowsView.jsx";
import { VirtualRootProvider } from "./hooks/virtual-drive/virtualRootContext.js"; import { VirtualRootProvider } from "./hooks/virtual-drive/virtualRootContext.js";

View file

@ -1,12 +1,12 @@
import { Children, cloneElement, isValidElement, useEffect, useRef, useState } from "react"; import { Children, cloneElement, isValidElement, useEffect, useRef, useState } from "react";
import { useShortcuts } from "../../hooks/utils/keyboard.js"; 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 { useScreenDimensions } from "../../hooks/utils/screen.js";
import { TASK_BAR_HEIGHT } from "../../constants/taskBar.js"; import { TASKBAR_HEIGHT } from "../../constants/taskBar.js";
export const STYLES = { export const STYLES = {
CONTEXT_MENU: style["Context-menu"], CONTEXT_MENU: styles["Context-menu"],
SHORTCUTS_LISTENER: style["Shortcuts-listener"], SHORTCUTS_LISTENER: styles["Shortcuts-listener"],
}; };
/** /**
@ -16,7 +16,7 @@ export const STYLES = {
* @param {Function} props.onAnyTrigger * @param {Function} props.onAnyTrigger
* @param {import("react").ElementType} props.children * @param {import("react").ElementType} props.children
* @param {*} props.triggerParams * @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 isListener = (className === STYLES.SHORTCUTS_LISTENER);
const ref = useRef(null); const ref = useRef(null);
@ -88,8 +88,8 @@ export function Actions({ children, className, onAnyTrigger, triggerParams, avoi
const maxX = screenWidth; const maxX = screenWidth;
let maxY = screenHeight; let maxY = screenHeight;
if (avoidTaskBar) if (avoidTaskbar)
maxY -= TASK_BAR_HEIGHT; maxY -= TASKBAR_HEIGHT;
const isOverflowingRight = (rect.x + rect.width > maxX); const isOverflowingRight = (rect.x + rect.width > maxX);
const isOverflowingBottom = (rect.y + rect.height > maxY); const isOverflowingBottom = (rect.y + rect.height > maxY);
@ -100,20 +100,20 @@ export function Actions({ children, className, onAnyTrigger, triggerParams, avoi
setAlignTop(true); setAlignTop(true);
setInitiated(true); setInitiated(true);
}, [alignLeft, avoidTaskBar, screenHeight, screenWidth]); }, [alignLeft, avoidTaskbar, screenHeight, screenWidth]);
if (isListener) if (isListener)
return iterateOverChildren(children); return iterateOverChildren(children);
const classNames = [style.Container]; const classNames = [styles.Container];
if (className != null) if (className != null)
classNames.push(className); classNames.push(className);
if (alignLeft) if (alignLeft)
classNames.push(style["Align-left"]); classNames.push(styles["Align-left"]);
if (alignTop) if (alignTop)
classNames.push(style["Align-top"]); classNames.push(styles["Align-top"]);
if (!initiated) if (!initiated)
classNames.push(style.Uninitiated); classNames.push(styles.Uninitiated);
return (<div ref={ref} className={classNames.join(" ")}> return (<div ref={ref} className={classNames.join(" ")}>
{iterateOverChildren(children)} {iterateOverChildren(children)}

View file

@ -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 (
<button
key={app.id}
tabIndex={0}
className={classNames.join(" ")}
onClick={() => { windowsManager.open(app.id); }}
title={app.name}
>
<ReactSVG src={`${process.env.PUBLIC_URL}/media/applications/icons/${app.id}.svg`}/>
</button>
);
});

View file

@ -1,5 +1,5 @@
import { memo, useRef, useState } from "react"; import { memo, useEffect, useMemo, useRef, useState } from "react";
import styles from "./TaskBar.module.css"; import styles from "./Taskbar.module.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCog, faSearch } from "@fortawesome/free-solid-svg-icons"; import { faCog, faSearch } from "@fortawesome/free-solid-svg-icons";
import AppsManager from "../../features/applications/applications.js"; import AppsManager from "../../features/applications/applications.js";
@ -12,7 +12,7 @@ import { Volume } from "./indicators/Volume.jsx";
import { SearchMenu } from "./menus/SearchMenu.jsx"; import { SearchMenu } from "./menus/SearchMenu.jsx";
import { Calendar } from "./indicators/Calendar.jsx"; import { Calendar } from "./indicators/Calendar.jsx";
import { useScrollWithShadow } from "../../hooks/utils/scrollWithShadows.js"; 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 { useContextMenu } from "../../hooks/modals/contextMenu.js";
import { Actions } from "../actions/Actions.jsx"; import { Actions } from "../actions/Actions.jsx";
import { useModals } from "../../hooks/modals/modals.js"; 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 { APPS, APP_NAMES } from "../../constants/applications.js";
import { useWindowsManager } from "../../hooks/windows/windowsManagerContext.js"; import { useWindowsManager } from "../../hooks/windows/windowsManagerContext.js";
import { ModalsView } from "../modals/ModalsView.jsx"; 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(() => { export const Taskbar = memo(() => {
const ref = useRef(null);
const settingsManager = useSettingsManager();
const [showHome, setShowHome] = useState(false); const [showHome, setShowHome] = useState(false);
const [showSearch, setShowSearch] = useState(false); const [showSearch, setShowSearch] = useState(false);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const ref = useRef(null);
const { boxShadow, onUpdate } = useScrollWithShadow({ ref, shadow: { const { boxShadow, onUpdate } = useScrollWithShadow({ ref, shadow: {
offset: 20, offset: 20,
blurRadius: 10, blurRadius: 10,
@ -36,13 +40,49 @@ export const Taskbar = memo(() => {
const inputRef = useRef(null); const inputRef = useRef(null);
const [modalsManager, modals] = useModals(); const [modalsManager, modals] = useModals();
const windowsManager = useWindowsManager(); const windowsManager = useWindowsManager();
const windows = useWindows();
const { onContextMenu } = useContextMenu({ modalsManager, Actions: (props) => const { onContextMenu } = useContextMenu({ modalsManager, Actions: (props) =>
<Actions avoidTaskBar={false} {...props}> <Actions avoidTaskbar={false} {...props}>
<ClickAction label={`Open ${APP_NAMES.SETTINGS}`} icon={faCog} onTrigger={() => { <ClickAction label={`Open ${APP_NAMES.SETTINGS}`} icon={faCog} onTrigger={() => {
windowsManager.open(APPS.SETTINGS); windowsManager.open(APPS.SETTINGS);
}}/> }}/>
</Actions> </Actions>
}); });
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 (<AppButton
modalsManager={modalsManager}
pins={pins}
app={app}
key={app.id}
active={isActive}
visible={shouldBeShown}
/>);
}), [modalsManager, pins, windows]);
useEffect(() => {
(async () => {
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.taskbar);
settings.get("pins", (pins) => {
setPins(pins.split(","));
});
})();
}, [settingsManager]);
const updateShowHome = (value) => { const updateShowHome = (value) => {
setShowHome(value); setShowHome(value);
@ -82,8 +122,8 @@ export const Taskbar = memo(() => {
return (<> return (<>
<ModalsView modalsManager={modalsManager} modals={modals}/> <ModalsView modalsManager={modalsManager} modals={modals}/>
<div <div
style={{ "--task-bar-height": `${TASK_BAR_HEIGHT}px` }} style={{ "--taskbar-height": `${TASKBAR_HEIGHT}px` }}
className={styles["Task-bar"]} className={styles["Taskbar"]}
data-allow-context-menu={true} data-allow-context-menu={true}
onContextMenu={(event) => { onContextMenu={(event) => {
if (event.target.getAttribute("data-allow-context-menu")) if (event.target.getAttribute("data-allow-context-menu"))
@ -132,9 +172,7 @@ export const Taskbar = memo(() => {
onResize={onUpdate} onResize={onUpdate}
ref={ref} ref={ref}
> >
{AppsManager.APPLICATIONS.map((app) => {apps}
<AppButton app={app} key={app.id}/>
)}
</div> </div>
</div> </div>
<div className={styles["Util-icons"]}> <div className={styles["Util-icons"]}>
@ -142,7 +180,7 @@ export const Taskbar = memo(() => {
<Network/> <Network/>
<Volume/> <Volume/>
<Calendar/> <Calendar/>
<button title="View Desktop" id="desktop-button"/> <button title="Show Desktop" id="desktop-button"/>
</div> </div>
</div> </div>
</>); </>);

View file

@ -1,22 +1,22 @@
:root { :root {
--task-bar-height: 3rem; --taskbar-height: 3rem;
--task-bar-color: rgba(0, 0, 0, 75%); --taskbar-color: rgba(0, 0, 0, 75%);
--task-bar-button-hover-color: rgba(255, 255, 255, 5%); --taskbar-button-hover-color: rgba(255, 255, 255, 5%);
} }
.Task-bar { .Taskbar {
position: fixed; position: fixed;
display: flex; display: flex;
bottom: 0; bottom: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: var(--task-bar-height); height: var(--taskbar-height);
background-color: var(--task-bar-color); background-color: var(--taskbar-color);
backdrop-filter: blur(15px); backdrop-filter: blur(15px);
z-index: 10; z-index: 10;
} }
.Task-bar button { .Taskbar button {
display: flex; display: flex;
align-items: center; align-items: center;
color: var(--foreground-color-a); color: var(--foreground-color-a);
@ -28,9 +28,9 @@
z-index: -1; z-index: -1;
} }
.Task-bar button:hover, .Taskbar button:hover,
.Task-bar button:focus-visible { .Taskbar button:focus-visible {
background-color: var(--task-bar-button-hover-color); background-color: var(--taskbar-button-hover-color);
} }
.Home-container, .Home-container,
@ -94,35 +94,6 @@
width: auto; 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 { .Util-icons {
display: flex; display: flex;
gap: 0; gap: 0;

View file

@ -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) =>
<Actions avoidTaskbar={false} {...props}>
<ClickAction label={isPinned ? "Unpin from taskbar" : "Pin to taskbar"} icon={faThumbTack} onTrigger={() => {
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 && <ClickAction label="Close window" icon={faTimes} onTrigger={() => {
windowsManager.close(windowsManager.getAppWindowId(app.id));
}}/>}
</Actions>
});
const classNames = [styles["App-icon"]];
if (active)
classNames.push(styles.Active);
if (!visible)
classNames.push(styles.Hidden);
return (
<button
key={app.id}
tabIndex={0}
className={classNames.join(" ")}
onClick={() => { windowsManager.open(app.id); }}
onContextMenu={(event) => {
if (visible)
onContextMenu(event);
}}
title={app.name}
>
<ReactSVG src={`${process.env.PUBLIC_URL}/media/applications/icons/${app.id}.svg`}/>
</button>
);
});

View file

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

View file

@ -25,7 +25,7 @@
.Window-container.Maximized { .Window-container.Maximized {
width: 100%; width: 100%;
height: calc(100% - var(--task-bar-height)); height: calc(100% - var(--taskbar-height));
border-radius: 0; border-radius: 0;
transform: none !important; transform: none !important;
resize: none; resize: none;

View file

@ -1 +1 @@
export const TASK_BAR_HEIGHT = 3 * 16; export const TASKBAR_HEIGHT = 3 * 16;

View file

@ -6,7 +6,8 @@ export class SettingsManager {
* @type {Object<string, string>} * @type {Object<string, string>}
*/ */
static VIRTUAL_PATHS = { static VIRTUAL_PATHS = {
"desktop": "~/.config/desktop.xml" desktop: "~/.config/desktop.xml",
taskbar: "~/.config/taskbar.xml"
}; };
/** /**

View file

@ -55,6 +55,8 @@ export class VirtualRoot extends VirtualFolder {
.createFolder(".config", (folder) => { .createFolder(".config", (folder) => {
folder.createFile("desktop", "xml", (file) => { folder.createFile("desktop", "xml", (file) => {
file.setSource("/config/desktop.xml"); file.setSource("/config/desktop.xml");
}).createFile("taskbar", "xml", (file) => {
file.setSource("/config/taskbar.xml");
}); });
}) })
.createFolder("Images", (folder) => { .createFolder("Images", (folder) => {

View file

@ -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 { SCREEN_MARGIN } from "../../constants/windows.js";
import AppsManager from "../applications/applications.js"; import AppsManager from "../applications/applications.js";
import { randomRange } from "../math/random.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 size = options?.size ?? app.windowOptions?.size ?? new Vector2(700, 400);
const position = new Vector2(randomRange(SCREEN_MARGIN, window.innerWidth - size.x - SCREEN_MARGIN), 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; let id = 0;
while (this.windowIds.includes(id.toString())) { while (this.windowIds.includes(id.toString())) {
@ -46,6 +46,8 @@ export default class WindowsManager {
lastInteraction: new Date().valueOf() lastInteraction: new Date().valueOf()
}; };
app.isActive = true;
this.updateWindows(this.windows); this.updateWindows(this.windows);
return this.windows[id]; return this.windows[id];
// console.log(this); // console.log(this);
@ -73,6 +75,9 @@ export default class WindowsManager {
return; return;
} }
const { app } = this.windows[windowId];
app.isActive = this.isAppActive(app.id);
console.info(`Closing window ${windowId}`); console.info(`Closing window ${windowId}`);
delete this.windows[windowId]; delete this.windows[windowId];
@ -114,6 +119,23 @@ export default class WindowsManager {
return active; 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 * @param {Function} updateWindows
*/ */