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>
<wallpaper>/media/wallpapers/mesh-gradient-wallpaper-red-purple.png</wallpaper>
</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 { 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";

View file

@ -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 (<div ref={ref} className={classNames.join(" ")}>
{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 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) =>
<Actions avoidTaskBar={false} {...props}>
<Actions avoidTaskbar={false} {...props}>
<ClickAction label={`Open ${APP_NAMES.SETTINGS}`} icon={faCog} onTrigger={() => {
windowsManager.open(APPS.SETTINGS);
}}/>
</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) => {
setShowHome(value);
@ -82,8 +122,8 @@ export const Taskbar = memo(() => {
return (<>
<ModalsView modalsManager={modalsManager} modals={modals}/>
<div
style={{ "--task-bar-height": `${TASK_BAR_HEIGHT}px` }}
className={styles["Task-bar"]}
style={{ "--taskbar-height": `${TASKBAR_HEIGHT}px` }}
className={styles["Taskbar"]}
data-allow-context-menu={true}
onContextMenu={(event) => {
if (event.target.getAttribute("data-allow-context-menu"))
@ -132,9 +172,7 @@ export const Taskbar = memo(() => {
onResize={onUpdate}
ref={ref}
>
{AppsManager.APPLICATIONS.map((app) =>
<AppButton app={app} key={app.id}/>
)}
{apps}
</div>
</div>
<div className={styles["Util-icons"]}>
@ -142,7 +180,7 @@ export const Taskbar = memo(() => {
<Network/>
<Volume/>
<Calendar/>
<button title="View Desktop" id="desktop-button"/>
<button title="Show Desktop" id="desktop-button"/>
</div>
</div>
</>);

View file

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

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 {
width: 100%;
height: calc(100% - var(--task-bar-height));
height: calc(100% - var(--taskbar-height));
border-radius: 0;
transform: none !important;
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>}
*/
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) => {
folder.createFile("desktop", "xml", (file) => {
file.setSource("/config/desktop.xml");
}).createFile("taskbar", "xml", (file) => {
file.setSource("/config/taskbar.xml");
});
})
.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 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
*/