Skip to content

Commit

Permalink
Separate AppBackground into its own file (#1053)
Browse files Browse the repository at this point in the history
Setup for command palette. I'm moving AppBackground to its own file and
simplifying the imports
  • Loading branch information
esimkowitz authored Oct 17, 2024
1 parent 88445b2 commit 7628e66
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 146 deletions.
123 changes: 123 additions & 0 deletions frontend/app/app-bg.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { getWebServerEndpoint } from "@/util/endpoints";
import * as util from "@/util/util";
import useResizeObserver from "@react-hook/resize-observer";
import { generate as generateCSS, parse as parseCSS, walk as walkCSS } from "css-tree";
import { useAtomValue } from "jotai";
import { CSSProperties, useCallback, useLayoutEffect, useRef } from "react";
import { debounce } from "throttle-debounce";
import { atoms, getApi, PLATFORM, WOS } from "./store/global";
import { useWaveObjectValue } from "./store/wos";

function encodeFileURL(file: string) {
const webEndpoint = getWebServerEndpoint();
return webEndpoint + `/wave/stream-file?path=${encodeURIComponent(file)}&no404=1`;
}

function processBackgroundUrls(cssText: string): string {
if (util.isBlank(cssText)) {
return null;
}
cssText = cssText.trim();
if (cssText.endsWith(";")) {
cssText = cssText.slice(0, -1);
}
const attrRe = /^background(-image)?\s*:\s*/i;
cssText = cssText.replace(attrRe, "");
const ast = parseCSS("background: " + cssText, {
context: "declaration",
});
let hasUnsafeUrl = false;
walkCSS(ast, {
visit: "Url",
enter(node) {
const originalUrl = node.value.trim();
if (
originalUrl.startsWith("http:") ||
originalUrl.startsWith("https:") ||
originalUrl.startsWith("data:")
) {
return;
}
// allow file:/// urls (if they are absolute)
if (originalUrl.startsWith("file://")) {
const path = originalUrl.slice(7);
if (!path.startsWith("/")) {
console.log(`Invalid background, contains a non-absolute file URL: ${originalUrl}`);
hasUnsafeUrl = true;
return;
}
const newUrl = encodeFileURL(path);
node.value = newUrl;
return;
}
// allow absolute paths
if (originalUrl.startsWith("/") || originalUrl.startsWith("~/")) {
const newUrl = encodeFileURL(originalUrl);
node.value = newUrl;
return;
}
hasUnsafeUrl = true;
console.log(`Invalid background, contains an unsafe URL scheme: ${originalUrl}`);
},
});
if (hasUnsafeUrl) {
return null;
}
const rtnStyle = generateCSS(ast);
if (rtnStyle == null) {
return null;
}
return rtnStyle.replace(/^background:\s*/, "");
}

export function AppBackground() {
const bgRef = useRef<HTMLDivElement>(null);
const tabId = useAtomValue(atoms.activeTabId);
const [tabData] = useWaveObjectValue<Tab>(WOS.makeORef("tab", tabId));
const bgAttr = tabData?.meta?.bg;
const style: CSSProperties = {};
if (!util.isBlank(bgAttr)) {
try {
const processedBg = processBackgroundUrls(bgAttr);
if (!util.isBlank(processedBg)) {
const opacity = util.boundNumber(tabData?.meta?.["bg:opacity"], 0, 1) ?? 0.5;
style.opacity = opacity;
style.background = processedBg;
const blendMode = tabData?.meta?.["bg:blendmode"];
if (!util.isBlank(blendMode)) {
style.backgroundBlendMode = blendMode;
}
}
} catch (e) {
console.error("error processing background", e);
}
}
const getAvgColor = useCallback(
debounce(30, () => {
if (
bgRef.current &&
PLATFORM !== "darwin" &&
bgRef.current &&
"windowControlsOverlay" in window.navigator
) {
const titlebarRect: Dimensions = (window.navigator.windowControlsOverlay as any).getTitlebarAreaRect();
const bgRect = bgRef.current.getBoundingClientRect();
if (titlebarRect && bgRect) {
const windowControlsLeft = titlebarRect.width - titlebarRect.height;
const windowControlsRect: Dimensions = {
top: titlebarRect.top,
left: windowControlsLeft,
height: titlebarRect.height,
width: bgRect.width - bgRect.left - windowControlsLeft,
};
getApi().updateWindowControlsOverlay(windowControlsRect);
}
}
}),
[bgRef, style]
);
useLayoutEffect(getAvgColor, [getAvgColor]);
useResizeObserver(bgRef, getAvgColor);

return <div ref={bgRef} className="app-background" style={style} />;
}
164 changes: 18 additions & 146 deletions frontend/app/app.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,29 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import { useWaveObjectValue } from "@/app/store/wos";
import { Workspace } from "@/app/workspace/workspace";
import { ContextMenuModel } from "@/store/contextmenu";
import {
PLATFORM,
WOS,
atoms,
createBlock,
getApi,
globalStore,
removeFlashError,
useSettingsPrefixAtom,
} from "@/store/global";
import { PLATFORM, atoms, createBlock, globalStore, removeFlashError, useSettingsPrefixAtom } from "@/store/global";
import { appHandleKeyDown } from "@/store/keymodel";
import { getWebServerEndpoint } from "@/util/endpoints";
import { getElemAsStr } from "@/util/focusutil";
import * as keyutil from "@/util/keyutil";
import * as util from "@/util/util";
import useResizeObserver from "@react-hook/resize-observer";
import clsx from "clsx";
import Color from "color";
import * as csstree from "css-tree";
import debug from "debug";
import * as jotai from "jotai";
import { Provider, useAtomValue } from "jotai";
import "overlayscrollbars/overlayscrollbars.css";
import * as React from "react";
import { Fragment, useEffect, useState } from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { debounce } from "throttle-debounce";
import { AppBackground } from "./app-bg";
import "./app.less";
import { CenteredDiv } from "./element/quickelems";

const dlog = debug("wave:app");
const focusLog = debug("wave:focus");

const App = () => {
let Provider = jotai.Provider;
return (
<Provider store={globalStore}>
<AppInner />
Expand Down Expand Up @@ -123,8 +109,8 @@ async function handleContextMenu(e: React.MouseEvent<HTMLDivElement>) {

function AppSettingsUpdater() {
const windowSettingsAtom = useSettingsPrefixAtom("window");
const windowSettings = jotai.useAtomValue(windowSettingsAtom);
React.useEffect(() => {
const windowSettings = useAtomValue(windowSettingsAtom);
useEffect(() => {
const isTransparentOrBlur =
(windowSettings?.["window:transparent"] || windowSettings?.["window:blur"]) ?? false;
const opacity = util.boundNumber(windowSettings?.["window:opacity"] ?? 0.8, 0, 1);
Expand Down Expand Up @@ -163,7 +149,7 @@ function AppFocusHandler() {
return null;

// for debugging
React.useEffect(() => {
useEffect(() => {
document.addEventListener("focusin", appFocusIn);
document.addEventListener("focusout", appFocusOut);
document.addEventListener("selectionchange", appSelectionChange);
Expand All @@ -183,122 +169,8 @@ function AppFocusHandler() {
return null;
}

function encodeFileURL(file: string) {
const webEndpoint = getWebServerEndpoint();
return webEndpoint + `/wave/stream-file?path=${encodeURIComponent(file)}&no404=1`;
}

function processBackgroundUrls(cssText: string): string {
if (util.isBlank(cssText)) {
return null;
}
cssText = cssText.trim();
if (cssText.endsWith(";")) {
cssText = cssText.slice(0, -1);
}
const attrRe = /^background(-image)?\s*:\s*/i;
cssText = cssText.replace(attrRe, "");
const ast = csstree.parse("background: " + cssText, {
context: "declaration",
});
let hasUnsafeUrl = false;
csstree.walk(ast, {
visit: "Url",
enter(node) {
const originalUrl = node.value.trim();
if (
originalUrl.startsWith("http:") ||
originalUrl.startsWith("https:") ||
originalUrl.startsWith("data:")
) {
return;
}
// allow file:/// urls (if they are absolute)
if (originalUrl.startsWith("file://")) {
const path = originalUrl.slice(7);
if (!path.startsWith("/")) {
console.log(`Invalid background, contains a non-absolute file URL: ${originalUrl}`);
hasUnsafeUrl = true;
return;
}
const newUrl = encodeFileURL(path);
node.value = newUrl;
return;
}
// allow absolute paths
if (originalUrl.startsWith("/") || originalUrl.startsWith("~/")) {
const newUrl = encodeFileURL(originalUrl);
node.value = newUrl;
return;
}
hasUnsafeUrl = true;
console.log(`Invalid background, contains an unsafe URL scheme: ${originalUrl}`);
},
});
if (hasUnsafeUrl) {
return null;
}
const rtnStyle = csstree.generate(ast);
if (rtnStyle == null) {
return null;
}
return rtnStyle.replace(/^background:\s*/, "");
}

function AppBackground() {
const bgRef = React.useRef<HTMLDivElement>(null);
const tabId = jotai.useAtomValue(atoms.activeTabId);
const [tabData] = useWaveObjectValue<Tab>(WOS.makeORef("tab", tabId));
const bgAttr = tabData?.meta?.bg;
const style: React.CSSProperties = {};
if (!util.isBlank(bgAttr)) {
try {
const processedBg = processBackgroundUrls(bgAttr);
if (!util.isBlank(processedBg)) {
const opacity = util.boundNumber(tabData?.meta?.["bg:opacity"], 0, 1) ?? 0.5;
style.opacity = opacity;
style.background = processedBg;
const blendMode = tabData?.meta?.["bg:blendmode"];
if (!util.isBlank(blendMode)) {
style.backgroundBlendMode = blendMode;
}
}
} catch (e) {
console.error("error processing background", e);
}
}
const getAvgColor = React.useCallback(
debounce(30, () => {
if (
bgRef.current &&
PLATFORM !== "darwin" &&
bgRef.current &&
"windowControlsOverlay" in window.navigator
) {
const titlebarRect: Dimensions = (window.navigator.windowControlsOverlay as any).getTitlebarAreaRect();
const bgRect = bgRef.current.getBoundingClientRect();
if (titlebarRect && bgRect) {
const windowControlsLeft = titlebarRect.width - titlebarRect.height;
const windowControlsRect: Dimensions = {
top: titlebarRect.top,
left: windowControlsLeft,
height: titlebarRect.height,
width: bgRect.width - bgRect.left - windowControlsLeft,
};
getApi().updateWindowControlsOverlay(windowControlsRect);
}
}
}),
[bgRef, style]
);
React.useLayoutEffect(getAvgColor, [getAvgColor]);
useResizeObserver(bgRef, getAvgColor);

return <div ref={bgRef} className="app-background" style={style} />;
}

const AppKeyHandlers = () => {
React.useEffect(() => {
useEffect(() => {
const staticKeyDownHandler = keyutil.keydownWrapper(appHandleKeyDown);
document.addEventListener("keydown", staticKeyDownHandler);

Expand All @@ -310,11 +182,11 @@ const AppKeyHandlers = () => {
};

const FlashError = () => {
const flashErrors = jotai.useAtomValue(atoms.flashErrors);
const [hoveredId, setHoveredId] = React.useState<string>(null);
const [ticker, setTicker] = React.useState<number>(0);
const flashErrors = useAtomValue(atoms.flashErrors);
const [hoveredId, setHoveredId] = useState<string>(null);
const [ticker, setTicker] = useState<number>(0);

React.useEffect(() => {
useEffect(() => {
if (flashErrors.length == 0 || hoveredId != null) {
return;
}
Expand Down Expand Up @@ -351,10 +223,10 @@ const FlashError = () => {

function convertNewlinesToBreaks(text) {
return text.split("\n").map((part, index) => (
<React.Fragment key={index}>
<Fragment key={index}>
{part}
<br />
</React.Fragment>
</Fragment>
));
}

Expand Down Expand Up @@ -382,10 +254,10 @@ const FlashError = () => {
};

const AppInner = () => {
const prefersReducedMotion = jotai.useAtomValue(atoms.prefersReducedMotionAtom);
const client = jotai.useAtomValue(atoms.client);
const windowData = jotai.useAtomValue(atoms.waveWindow);
const isFullScreen = jotai.useAtomValue(atoms.isFullScreen);
const prefersReducedMotion = useAtomValue(atoms.prefersReducedMotionAtom);
const client = useAtomValue(atoms.client);
const windowData = useAtomValue(atoms.waveWindow);
const isFullScreen = useAtomValue(atoms.isFullScreen);

if (client == null || windowData == null) {
return (
Expand Down

0 comments on commit 7628e66

Please sign in to comment.