background support (from metadata). colors, gradients, images. (#168)

This commit is contained in:
Mike Sawka 2024-07-29 11:55:10 -07:00 committed by GitHub
parent 6e75c2cdfd
commit 551802dbd7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 222 additions and 13 deletions

View File

@ -9,10 +9,10 @@ body {
flex-direction: row;
width: 100vw;
height: 100vh;
background-color: var(--main-bg-color);
color: var(--main-text-color);
font: var(--base-font);
overflow: hidden;
background: var(--main-bg-color);
-webkit-font-smoothing: auto;
backface-visibility: hidden;
transform: translateZ(0);
@ -51,6 +51,15 @@ body {
flex-direction: column;
width: 100%;
height: 100%;
.app-background {
pointer-events: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
}
.error-boundary {

View File

@ -8,6 +8,7 @@ import * as services from "@/store/services";
import * as keyutil from "@/util/keyutil";
import * as layoututil from "@/util/layoututil";
import * as util from "@/util/util";
import * as csstree from "css-tree";
import { getLayoutStateAtomForTab, globalLayoutTransformsMap } from "frontend/layout/lib/layoutAtom";
import * as jotai from "jotai";
import * as React from "react";
@ -15,6 +16,8 @@ import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { CenteredDiv } from "./element/quickelems";
import { useWaveObjectValue } from "@/app/store/wos";
import { getWebServerEndpoint } from "@/util/endpoints";
import clsx from "clsx";
import Color from "color";
import "overlayscrollbars/overlayscrollbars.css";
@ -208,7 +211,6 @@ function AppSettingsUpdater() {
const opacity = util.boundNumber(settings?.window?.opacity ?? 0.8, 0, 1);
let baseBgColor = settings?.window?.bgcolor;
console.log("window settings", settings.window);
if (isTransparentOrBlur) {
document.body.classList.add("is-transparent");
const rootStyles = getComputedStyle(document.documentElement);
@ -226,6 +228,76 @@ function AppSettingsUpdater() {
return null;
}
function encodeFileURL(file: string) {
const webEndpoint = getWebServerEndpoint();
return webEndpoint + `/wave/stream-file?path=${encodeURIComponent(file)}&no404=1`;
}
(window as any).csstree = csstree;
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*/;
cssText = cssText.replace(attrRe, "");
const ast = csstree.parse("background: " + cssText, {
context: "declaration",
});
let hasJSUrl = false;
csstree.walk(ast, {
visit: "Url",
enter(node) {
const originalUrl = node.value.trim();
if (originalUrl.startsWith("javascript:")) {
hasJSUrl = true;
return;
}
const newUrl = encodeFileURL(originalUrl);
node.value = newUrl;
},
});
if (hasJSUrl) {
console.log("invalid background, contains a 'javascript' protocol url which is not allowed");
return null;
}
const rtnStyle = csstree.generate(ast);
if (rtnStyle == null) {
return null;
}
return rtnStyle.replace(/^background:\s*/, "");
}
const backgroundAttr = "url(/Users/mike/Downloads/wave-logo_appicon.png) repeat-x fixed";
function AppBackground() {
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);
}
}
return <div className="app-background" style={style} />;
}
const AppInner = () => {
const client = jotai.useAtomValue(atoms.client);
const windowData = jotai.useAtomValue(atoms.waveWindow);
@ -233,6 +305,7 @@ const AppInner = () => {
if (client == null || windowData == null) {
return (
<div className="mainapp">
<AppBackground />
<CenteredDiv>invalid configuration, client or window was not loaded</CenteredDiv>
</div>
);
@ -277,6 +350,7 @@ const AppInner = () => {
const isFullScreen = jotai.useAtomValue(atoms.isFullScreen);
return (
<div className={clsx("mainapp", PLATFORM, { fullscreen: isFullScreen })} onContextMenu={handleContextMenu}>
<AppBackground />
<AppSettingsUpdater />
<DndProvider backend={HTML5Backend}>
<Workspace />

View File

@ -16,7 +16,7 @@
height: 100%;
white-space: nowrap;
border-radius: 6px;
background: rgba(255, 255, 255, 0.05);
background: rgba(255, 255, 255, 0.1);
}
&.animate {

View File

@ -7,7 +7,7 @@ import * as services from "@/store/services";
import * as WOS from "@/store/wos";
import * as React from "react";
import { CenteredDiv, CenteredLoadingDiv } from "@/element/quickelems";
import { CenteredDiv } from "@/element/quickelems";
import { TileLayout } from "frontend/layout/index";
import { getLayoutStateAtomForTab } from "frontend/layout/lib/layoutAtom";
import { useAtomValue } from "jotai";
@ -56,7 +56,11 @@ const TabContent = React.memo(({ tabId }: { tabId: string }) => {
}, []);
if (tabLoading) {
return <CenteredLoadingDiv />;
return (
<div className="tabcontent">
<CenteredDiv>Tab Loading</CenteredDiv>
</div>
);
}
if (!tabData) {

View File

@ -35,6 +35,9 @@ function base64ToArray(b64: string): Uint8Array {
}
function boundNumber(num: number, min: number, max: number): number {
if (num == null || typeof num != "number" || isNaN(num)) {
return null;
}
return Math.min(Math.max(num, min), max);
}

View File

@ -36,6 +36,7 @@
"@storybook/react": "^8.2.6",
"@storybook/react-vite": "^8.2.6",
"@storybook/test": "^8.2.6",
"@types/css-tree": "^2",
"@types/electron": "^1.6.10",
"@types/node": "^20.14.12",
"@types/papaparse": "^5",
@ -84,6 +85,7 @@
"base64-js": "^1.5.1",
"clsx": "^2.1.1",
"color": "^4.2.3",
"css-tree": "^2.3.1",
"dayjs": "^1.11.12",
"electron-updater": "6.3.1",
"html-to-image": "^1.11.11",

View File

@ -307,7 +307,7 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta map[str
if bc.ControllerType == BlockController_Shell {
cmdOpts.Interactive = true
cmdOpts.Login = true
cmdOpts.Cwd, _ = blockMeta["cwd"].(string)
cmdOpts.Cwd, _ = blockMeta["cmd:cwd"].(string)
if cmdOpts.Cwd != "" {
cmdOpts.Cwd = wavebase.ExpandHomeDir(cmdOpts.Cwd)
}
@ -317,8 +317,8 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta map[str
} else {
return fmt.Errorf("missing cmd in block meta")
}
if _, ok := blockMeta["cwd"].(string); ok {
cmdOpts.Cwd = blockMeta["cwd"].(string)
if _, ok := blockMeta["cmd:cwd"].(string); ok {
cmdOpts.Cwd = blockMeta["cmd:cwd"].(string)
if cmdOpts.Cwd != "" {
cmdOpts.Cwd = wavebase.ExpandHomeDir(cmdOpts.Cwd)
}

View File

@ -27,6 +27,8 @@ import (
type WebFnType = func(http.ResponseWriter, *http.Request)
const TransparentGif64 = "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
// Header constants
const (
CacheControlHeaderKey = "Cache-Control"
@ -55,6 +57,45 @@ type WebFnOpts struct {
JsonErrors bool
}
func copyHeaders(dst, src http.Header) {
for key, values := range src {
for _, value := range values {
dst.Add(key, value)
}
}
}
type notFoundBlockingResponseWriter struct {
w http.ResponseWriter
status int
headers http.Header
}
func (rw *notFoundBlockingResponseWriter) Header() http.Header {
return rw.headers
}
func (rw *notFoundBlockingResponseWriter) WriteHeader(status int) {
if status == http.StatusNotFound {
rw.status = status
return
}
rw.status = status
copyHeaders(rw.w.Header(), rw.headers)
rw.w.WriteHeader(status)
}
func (rw *notFoundBlockingResponseWriter) Write(b []byte) (int, error) {
if rw.status == http.StatusNotFound {
// Block the write if it's a 404
return len(b), nil
}
if rw.status == 0 {
rw.WriteHeader(http.StatusOK)
}
return rw.w.Write(b)
}
func handleService(w http.ResponseWriter, r *http.Request) {
bodyData, err := io.ReadAll(r.Body)
if err != nil {
@ -160,14 +201,36 @@ func handleWaveFile(w http.ResponseWriter, r *http.Request) {
}
}
func serveTransparentGIF(w http.ResponseWriter) {
gifBytes, _ := base64.StdEncoding.DecodeString(TransparentGif64)
w.Header().Set("Content-Type", "image/gif")
w.WriteHeader(http.StatusOK)
w.Write(gifBytes)
}
func handleStreamFile(w http.ResponseWriter, r *http.Request) {
fileName := r.URL.Query().Get("path")
if fileName == "" {
http.Error(w, "path is required", http.StatusBadRequest)
return
}
no404 := r.URL.Query().Get("no404")
log.Printf("got no404: %q\n", no404)
if no404 != "" {
log.Printf("streaming file w/no404: %q\n", fileName)
// use the custom response writer
rw := &notFoundBlockingResponseWriter{w: w, headers: http.Header{}}
// Serve the file using http.ServeFile
http.ServeFile(rw, r, fileName)
// if the file was not found, serve the transparent GIF
log.Printf("got streamfile status: %d\n", rw.status)
if rw.status == http.StatusNotFound {
serveTransparentGIF(w)
}
} else {
fileName = wavebase.ExpandHomeDir(fileName)
http.ServeFile(w, r, fileName)
}
}
func WebFnWrap(opts WebFnOpts, fn WebFnType) WebFnType {

View File

@ -28,10 +28,38 @@ const (
MetaKey_CmdRunOnStart = "cmd:runonstart"
MetaKey_CmdClearOnStart = "cmd:clearonstart"
MetaKey_CmdClearOnRestart = "cmd:clearonrestart"
MetaKey_CmdEnv = "env"
MetaKey_CmdCwd = "cwd"
MetaKey_CmdEnv = "cmd:env"
MetaKey_CmdCwd = "cmd:cwd"
)
type MetaType struct {
View string `json:"view,omitempty"`
Controller string `json:"controller,omitempty"`
Title string `json:"title,omitempty"`
File string `json:"file,omitempty"`
Url string `json:"url,omitempty"`
Icon string `json:"icon,omitempty"`
IconColor string `json:"icon:color,omitempty"`
Frame bool `json:"frame,omitempty"`
FrameBorderColor string `json:"frame:bordercolor,omitempty"`
FrameBorderColor_Focused string `json:"frame:bordercolor:focused,omitempty"`
Cmd string `json:"cmd,omitempty"`
CmdInteractive bool `json:"cmd:interactive,omitempty"`
CmdLogin bool `json:"cmd:login,omitempty"`
CmdRunOnStart bool `json:"cmd:runonstart,omitempty"`
CmdClearOnStart bool `json:"cmd:clearonstart,omitempty"`
CmdClearOnRestart bool `json:"cmd:clearonrestart,omitempty"`
CmdEnv map[string]string `json:"cmd:env,omitempty"`
CmdCwd string `json:"cmd:cwd,omitempty"`
Bg string `json:"bg,omitempty"`
BgOpacity float64 `json:"bg:opacity,omitempty"`
BgBlendMode string `json:"bg:blendmode,omitempty"`
}
type UIContext struct {
WindowId string `json:"windowid"`
ActiveTabId string `json:"activetabid"`

View File

@ -3589,6 +3589,13 @@ __metadata:
languageName: node
linkType: hard
"@types/css-tree@npm:^2":
version: 2.3.8
resolution: "@types/css-tree@npm:2.3.8"
checksum: 10c0/fafa7ad516b64481a031aceb3c30762074e1e0bfd67e0f0655e46b8c1b7b3c39660f8285811ca6aac11229ef477c65ca61ee118d2f9264145d5db8fe26f1a721
languageName: node
linkType: hard
"@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.6":
version: 4.1.12
resolution: "@types/debug@npm:4.1.12"
@ -5510,6 +5517,16 @@ __metadata:
languageName: node
linkType: hard
"css-tree@npm:^2.3.1":
version: 2.3.1
resolution: "css-tree@npm:2.3.1"
dependencies:
mdn-data: "npm:2.0.30"
source-map-js: "npm:^1.0.1"
checksum: 10c0/6f8c1a11d5e9b14bf02d10717fc0351b66ba12594166f65abfbd8eb8b5b490dd367f5c7721db241a3c792d935fc6751fbc09f7e1598d421477ad9fadc30f4f24
languageName: node
linkType: hard
"css.escape@npm:^1.5.1":
version: 1.5.1
resolution: "css.escape@npm:1.5.1"
@ -9080,6 +9097,13 @@ __metadata:
languageName: node
linkType: hard
"mdn-data@npm:2.0.30":
version: 2.0.30
resolution: "mdn-data@npm:2.0.30"
checksum: 10c0/a2c472ea16cee3911ae742593715aa4c634eb3d4b9f1e6ada0902aa90df13dcbb7285d19435f3ff213ebaa3b2e0c0265c1eb0e3fb278fda7f8919f046a410cd9
languageName: node
linkType: hard
"media-typer@npm:0.3.0":
version: 0.3.0
resolution: "media-typer@npm:0.3.0"
@ -11434,7 +11458,7 @@ __metadata:
languageName: node
linkType: hard
"source-map-js@npm:^1.2.0":
"source-map-js@npm:^1.0.1, source-map-js@npm:^1.2.0":
version: 1.2.0
resolution: "source-map-js@npm:1.2.0"
checksum: 10c0/7e5f896ac10a3a50fe2898e5009c58ff0dc102dcb056ed27a354623a0ece8954d4b2649e1a1b2b52ef2e161d26f8859c7710350930751640e71e374fe2d321a4
@ -11818,6 +11842,7 @@ __metadata:
"@table-nav/react": "npm:^0.0.7"
"@tanstack/react-table": "npm:^8.19.3"
"@types/color": "npm:^3.0.6"
"@types/css-tree": "npm:^2"
"@types/electron": "npm:^1.6.10"
"@types/node": "npm:^20.14.12"
"@types/papaparse": "npm:^5"
@ -11835,6 +11860,7 @@ __metadata:
base64-js: "npm:^1.5.1"
clsx: "npm:^2.1.1"
color: "npm:^4.2.3"
css-tree: "npm:^2.3.1"
dayjs: "npm:^1.11.12"
electron: "npm:^31.3.0"
electron-builder: "npm:^24.13.3"