mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
background support (from metadata). colors, gradients, images. (#168)
This commit is contained in:
parent
6e75c2cdfd
commit
551802dbd7
@ -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 {
|
||||
|
@ -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 />
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,15 +201,37 @@ 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 := ¬FoundBlockingResponseWriter{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 {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -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"`
|
||||
|
28
yarn.lock
28
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user