mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-20 10:33:41 -05:00
web/settings: embed vs code for editing custom css
Not meant for mobile yet
This commit is contained in:
parent
63798f2298
commit
803505385a
7 changed files with 163 additions and 6 deletions
7
web/package-lock.json
generated
7
web/package-lock.json
generated
|
@ -13,6 +13,7 @@
|
||||||
"blurhash": "^2.0.5",
|
"blurhash": "^2.0.5",
|
||||||
"katex": "^0.16.11",
|
"katex": "^0.16.11",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"monaco-editor": "^0.52.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-blurhash": "^0.3.0",
|
"react-blurhash": "^0.3.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
@ -4301,6 +4302,12 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/monaco-editor": {
|
||||||
|
"version": "0.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz",
|
||||||
|
"integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
"blurhash": "^2.0.5",
|
"blurhash": "^2.0.5",
|
||||||
"katex": "^0.16.11",
|
"katex": "^0.16.11",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"monaco-editor": "^0.52.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-blurhash": "^0.3.0",
|
"react-blurhash": "^0.3.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|
|
@ -21,6 +21,21 @@ export enum PreferenceContext {
|
||||||
RoomDevice = "room_device",
|
RoomDevice = "room_device",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function preferenceContextToInt(context: PreferenceContext): number {
|
||||||
|
switch (context) {
|
||||||
|
case PreferenceContext.Config:
|
||||||
|
return 0
|
||||||
|
case PreferenceContext.Account:
|
||||||
|
return 1
|
||||||
|
case PreferenceContext.Device:
|
||||||
|
return 2
|
||||||
|
case PreferenceContext.RoomAccount:
|
||||||
|
return 3
|
||||||
|
case PreferenceContext.RoomDevice:
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const anyContext = [
|
export const anyContext = [
|
||||||
PreferenceContext.RoomDevice,
|
PreferenceContext.RoomDevice,
|
||||||
PreferenceContext.RoomAccount,
|
PreferenceContext.RoomAccount,
|
||||||
|
|
|
@ -56,6 +56,12 @@ div.settings-view {
|
||||||
font-family: var(--monospace-font-stack);
|
font-family: var(--monospace-font-stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> div.vscode-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
> div.buttons {
|
> div.buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: right;
|
justify-content: right;
|
||||||
|
|
|
@ -13,9 +13,17 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import { use, useCallback, useState } from "react"
|
import { lazy, use, useCallback, useRef, useState } from "react"
|
||||||
|
import Client from "@/api/client.ts"
|
||||||
import { RoomStateStore, usePreferences } from "@/api/statestore"
|
import { RoomStateStore, usePreferences } from "@/api/statestore"
|
||||||
import { Preference, PreferenceContext, PreferenceValueType, Preferences, preferences } from "@/api/types/preferences"
|
import {
|
||||||
|
Preference,
|
||||||
|
PreferenceContext,
|
||||||
|
PreferenceValueType,
|
||||||
|
Preferences,
|
||||||
|
preferenceContextToInt,
|
||||||
|
preferences,
|
||||||
|
} from "@/api/types/preferences"
|
||||||
import useEvent from "@/util/useEvent.ts"
|
import useEvent from "@/util/useEvent.ts"
|
||||||
import ClientContext from "../ClientContext.ts"
|
import ClientContext from "../ClientContext.ts"
|
||||||
import JSONView from "../util/JSONView.tsx"
|
import JSONView from "../util/JSONView.tsx"
|
||||||
|
@ -155,9 +163,24 @@ interface SettingsViewProps {
|
||||||
room: RoomStateStore
|
room: RoomStateStore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getActiveCSSContext(client: Client, room: RoomStateStore): PreferenceContext {
|
||||||
|
if (room.localPreferenceCache.custom_css !== undefined) {
|
||||||
|
return PreferenceContext.RoomDevice
|
||||||
|
} else if (room.serverPreferenceCache.custom_css !== undefined) {
|
||||||
|
return PreferenceContext.RoomAccount
|
||||||
|
} else if (client.store.localPreferenceCache.custom_css !== undefined) {
|
||||||
|
return PreferenceContext.Device
|
||||||
|
} else {
|
||||||
|
return PreferenceContext.Account
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Monaco = lazy(() => import("../util/monaco.tsx"))
|
||||||
|
|
||||||
const CustomCSSInput = ({ setPref, room }: { setPref: SetPrefFunc, room: RoomStateStore }) => {
|
const CustomCSSInput = ({ setPref, room }: { setPref: SetPrefFunc, room: RoomStateStore }) => {
|
||||||
const client = use(ClientContext)!
|
const client = use(ClientContext)!
|
||||||
const [context, setContext] = useState(PreferenceContext.Account)
|
const appliedContext = getActiveCSSContext(client, room)
|
||||||
|
const [context, setContext] = useState(appliedContext)
|
||||||
const getContextText = useCallback((context: PreferenceContext) => {
|
const getContextText = useCallback((context: PreferenceContext) => {
|
||||||
if (context === PreferenceContext.Account) {
|
if (context === PreferenceContext.Account) {
|
||||||
return client.store.serverPreferenceCache.custom_css
|
return client.store.serverPreferenceCache.custom_css
|
||||||
|
@ -180,12 +203,30 @@ const CustomCSSInput = ({ setPref, room }: { setPref: SetPrefFunc, room: RoomSta
|
||||||
setText(evt.target.value)
|
setText(evt.target.value)
|
||||||
}, [])
|
}, [])
|
||||||
const onSave = useEvent(() => {
|
const onSave = useEvent(() => {
|
||||||
|
if (vscodeOpen) {
|
||||||
|
setText(vscodeContentRef.current)
|
||||||
|
setPref(context, "custom_css", vscodeContentRef.current)
|
||||||
|
} else {
|
||||||
setPref(context, "custom_css", text)
|
setPref(context, "custom_css", text)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
const onDelete = useEvent(() => {
|
const onDelete = useEvent(() => {
|
||||||
setPref(context, "custom_css", undefined)
|
setPref(context, "custom_css", undefined)
|
||||||
setText("")
|
setText("")
|
||||||
})
|
})
|
||||||
|
const [vscodeOpen, setVSCodeOpen] = useState(false)
|
||||||
|
const vscodeContentRef = useRef("")
|
||||||
|
const vscodeInitialContentRef = useRef("")
|
||||||
|
const onClickVSCode = useEvent(() => {
|
||||||
|
vscodeContentRef.current = text
|
||||||
|
vscodeInitialContentRef.current = text
|
||||||
|
setVSCodeOpen(true)
|
||||||
|
})
|
||||||
|
const closeVSCode = useCallback(() => {
|
||||||
|
setVSCodeOpen(false)
|
||||||
|
setText(vscodeContentRef.current)
|
||||||
|
vscodeContentRef.current = ""
|
||||||
|
}, [])
|
||||||
return <div className="custom-css-input">
|
return <div className="custom-css-input">
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<h3>Custom CSS</h3>
|
<h3>Custom CSS</h3>
|
||||||
|
@ -195,9 +236,21 @@ const CustomCSSInput = ({ setPref, room }: { setPref: SetPrefFunc, room: RoomSta
|
||||||
<option value={PreferenceContext.RoomAccount}>Room (account)</option>
|
<option value={PreferenceContext.RoomAccount}>Room (account)</option>
|
||||||
<option value={PreferenceContext.RoomDevice}>Room (device)</option>
|
<option value={PreferenceContext.RoomDevice}>Room (device)</option>
|
||||||
</select>
|
</select>
|
||||||
|
{preferenceContextToInt(context) < preferenceContextToInt(appliedContext) &&
|
||||||
|
<span className="warning">
|
||||||
|
⚠️ This context will not be applied, <code>{appliedContext}</code> has content
|
||||||
|
</span>}
|
||||||
</div>
|
</div>
|
||||||
<textarea value={text} onChange={onChangeText}/>
|
{vscodeOpen ? <div className="vscode-wrapper">
|
||||||
|
<Monaco
|
||||||
|
initData={vscodeInitialContentRef.current}
|
||||||
|
onClose={closeVSCode}
|
||||||
|
onSave={onSave}
|
||||||
|
contentRef={vscodeContentRef}
|
||||||
|
/>
|
||||||
|
</div> : <textarea value={text} onChange={onChangeText}/>}
|
||||||
<div className="buttons">
|
<div className="buttons">
|
||||||
|
<button onClick={onClickVSCode}>Open in VS Code</button>
|
||||||
{origText !== undefined && <button className="delete" onClick={onDelete}>Delete</button>}
|
{origText !== undefined && <button className="delete" onClick={onDelete}>Delete</button>}
|
||||||
<button className="save primary-color-button" onClick={onSave} disabled={origText === text}>Save</button>
|
<button className="save primary-color-button" onClick={onSave} disabled={origText === text}>Save</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
72
web/src/ui/util/monaco.tsx
Normal file
72
web/src/ui/util/monaco.tsx
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
// gomuks - A Matrix client written in Go.
|
||||||
|
// Copyright (C) 2024 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
import "monaco-editor/esm/vs/basic-languages/css/css.contribution.js"
|
||||||
|
import "monaco-editor/esm/vs/editor/edcore.main.js"
|
||||||
|
import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js"
|
||||||
|
import CSSWorker from "monaco-editor/esm/vs/language/css/css.worker.js?worker"
|
||||||
|
import "monaco-editor/esm/vs/language/css/monaco.contribution.js"
|
||||||
|
import { RefObject, memo, useLayoutEffect, useRef } from "react"
|
||||||
|
|
||||||
|
window.MonacoEnvironment = {
|
||||||
|
getWorker: function() {
|
||||||
|
return new CSSWorker()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MonacoProps {
|
||||||
|
initData: string
|
||||||
|
onClose: () => void
|
||||||
|
onSave: () => void
|
||||||
|
contentRef: RefObject<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
const Monaco = ({ initData, onClose, onSave, contentRef }: MonacoProps) => {
|
||||||
|
const container = useRef<HTMLDivElement>(null)
|
||||||
|
const editor = useRef<monaco.editor.IStandaloneCodeEditor>(null)
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!container.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const newEditor = monaco.editor.create(container.current, {
|
||||||
|
language: "css",
|
||||||
|
value: initData,
|
||||||
|
fontLigatures: true,
|
||||||
|
fontFamily: `var(--monospace-font-stack)`,
|
||||||
|
theme: window.matchMedia("(prefers-color-scheme: dark)").matches ? "vs-dark" : "vs",
|
||||||
|
})
|
||||||
|
const model = newEditor.getModel()
|
||||||
|
if (!model) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
model.onDidChangeContent(() => contentRef.current = model.getValue(monaco.editor.EndOfLinePreference.LF))
|
||||||
|
newEditor.onKeyDown(evt => {
|
||||||
|
if (evt.keyCode === monaco.KeyCode.Escape) {
|
||||||
|
onClose()
|
||||||
|
} else if (evt.ctrlKey && evt.keyCode === monaco.KeyCode.KeyS) {
|
||||||
|
onSave()
|
||||||
|
evt.preventDefault()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
newEditor.focus()
|
||||||
|
editor.current = newEditor
|
||||||
|
return () => newEditor.dispose()
|
||||||
|
// All props are intentionally immutable
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
return <div style={{ width: "100%", height: "100%" }} ref={container}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(Monaco)
|
|
@ -2,16 +2,19 @@ import react from "@vitejs/plugin-react-swc"
|
||||||
import { defineConfig } from "vite"
|
import { defineConfig } from "vite"
|
||||||
import svgr from "vite-plugin-svgr"
|
import svgr from "vite-plugin-svgr"
|
||||||
|
|
||||||
|
const splitDeps = ["katex", "leaflet", "monaco-editor"]
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: "./",
|
base: "./",
|
||||||
build: {
|
build: {
|
||||||
target: ["esnext", "firefox128", "chrome131", "safari18"],
|
target: ["esnext", "firefox128", "chrome131", "safari18"],
|
||||||
|
chunkSizeWarningLimit: 3500,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
manualChunks: id => {
|
manualChunks: id => {
|
||||||
if (id.includes("wailsio")) {
|
if (id.includes("wailsio")) {
|
||||||
return "wails"
|
return "wails"
|
||||||
} else if (id.includes("node_modules") && !id.includes("katex") && !id.includes("leaflet")) {
|
} else if (id.includes("node_modules") && !splitDeps.some(dep => id.includes(dep))) {
|
||||||
return "vendor"
|
return "vendor"
|
||||||
} else if (id.endsWith("/emoji/data.json")) {
|
} else if (id.endsWith("/emoji/data.json")) {
|
||||||
return "emoji"
|
return "emoji"
|
||||||
|
|
Loading…
Add table
Reference in a new issue