mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-19 18:13: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",
|
||||
"katex": "^0.16.11",
|
||||
"leaflet": "^1.9.4",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"react": "^19.0.0",
|
||||
"react-blurhash": "^0.3.0",
|
||||
"react-dom": "^19.0.0",
|
||||
|
@ -4301,6 +4302,12 @@
|
|||
"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": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"blurhash": "^2.0.5",
|
||||
"katex": "^0.16.11",
|
||||
"leaflet": "^1.9.4",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"react": "^19.0.0",
|
||||
"react-blurhash": "^0.3.0",
|
||||
"react-dom": "^19.0.0",
|
||||
|
|
|
@ -21,6 +21,21 @@ export enum PreferenceContext {
|
|||
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 = [
|
||||
PreferenceContext.RoomDevice,
|
||||
PreferenceContext.RoomAccount,
|
||||
|
|
|
@ -56,6 +56,12 @@ div.settings-view {
|
|||
font-family: var(--monospace-font-stack);
|
||||
}
|
||||
|
||||
> div.vscode-wrapper {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
> div.buttons {
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
|
|
|
@ -13,9 +13,17 @@
|
|||
//
|
||||
// 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 { 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 { 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 ClientContext from "../ClientContext.ts"
|
||||
import JSONView from "../util/JSONView.tsx"
|
||||
|
@ -155,9 +163,24 @@ interface SettingsViewProps {
|
|||
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 client = use(ClientContext)!
|
||||
const [context, setContext] = useState(PreferenceContext.Account)
|
||||
const appliedContext = getActiveCSSContext(client, room)
|
||||
const [context, setContext] = useState(appliedContext)
|
||||
const getContextText = useCallback((context: PreferenceContext) => {
|
||||
if (context === PreferenceContext.Account) {
|
||||
return client.store.serverPreferenceCache.custom_css
|
||||
|
@ -180,12 +203,30 @@ const CustomCSSInput = ({ setPref, room }: { setPref: SetPrefFunc, room: RoomSta
|
|||
setText(evt.target.value)
|
||||
}, [])
|
||||
const onSave = useEvent(() => {
|
||||
if (vscodeOpen) {
|
||||
setText(vscodeContentRef.current)
|
||||
setPref(context, "custom_css", vscodeContentRef.current)
|
||||
} else {
|
||||
setPref(context, "custom_css", text)
|
||||
}
|
||||
})
|
||||
const onDelete = useEvent(() => {
|
||||
setPref(context, "custom_css", undefined)
|
||||
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">
|
||||
<div className="header">
|
||||
<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.RoomDevice}>Room (device)</option>
|
||||
</select>
|
||||
{preferenceContextToInt(context) < preferenceContextToInt(appliedContext) &&
|
||||
<span className="warning">
|
||||
⚠️ This context will not be applied, <code>{appliedContext}</code> has content
|
||||
</span>}
|
||||
</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">
|
||||
<button onClick={onClickVSCode}>Open in VS Code</button>
|
||||
{origText !== undefined && <button className="delete" onClick={onDelete}>Delete</button>}
|
||||
<button className="save primary-color-button" onClick={onSave} disabled={origText === text}>Save</button>
|
||||
</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 svgr from "vite-plugin-svgr"
|
||||
|
||||
const splitDeps = ["katex", "leaflet", "monaco-editor"]
|
||||
|
||||
export default defineConfig({
|
||||
base: "./",
|
||||
build: {
|
||||
target: ["esnext", "firefox128", "chrome131", "safari18"],
|
||||
chunkSizeWarningLimit: 3500,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: id => {
|
||||
if (id.includes("wailsio")) {
|
||||
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"
|
||||
} else if (id.endsWith("/emoji/data.json")) {
|
||||
return "emoji"
|
||||
|
|
Loading…
Add table
Reference in a new issue