From ba3e7a6e613f7c575f101e5f2711397091638fe6 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Wed, 17 Jun 2026 00:14:59 +0800 Subject: [PATCH 01/18] feat(monaco): add more elements Signed-off-by: MadDogOwner --- src/components/MonacoEditor.tsx | 19 +- src/pages/home/previews/text-editor.tsx | 259 +++++++++++++++++++++--- 2 files changed, 254 insertions(+), 24 deletions(-) diff --git a/src/components/MonacoEditor.tsx b/src/components/MonacoEditor.tsx index eeb9cc1df..2ccfb2ecf 100644 --- a/src/components/MonacoEditor.tsx +++ b/src/components/MonacoEditor.tsx @@ -12,6 +12,7 @@ export interface MonacoEditorProps { theme: "vs" | "vs-dark" path?: string language?: string + onEditorReady?: (editor: monacoType.editor.IStandaloneCodeEditor) => void } let monaco: typeof monacoType @@ -44,6 +45,21 @@ export const MonacoEditor = (props: MonacoEditorProps) => { value: props.value, theme: props.theme, fontSize: parseInt(local.editor_font_size), + minimap: { enabled: true }, + lineNumbers: "on", + scrollBeyondLastLine: false, + smoothScrolling: true, + cursorBlinking: "smooth", + cursorSmoothCaretAnimation: "on", + bracketPairColorization: { enabled: true }, + automaticLayout: true, + padding: { top: 8 }, + wordWrap: "off", + renderLineHighlight: "all", + scrollbar: { + verticalScrollbarSize: 10, + horizontalScrollbarSize: 10, + }, }) model = monaco.editor.createModel( props.value, @@ -54,6 +70,7 @@ export const MonacoEditor = (props: MonacoEditorProps) => { monacoEditor.onDidChangeModelContent(() => { props.onChange?.(monacoEditor.getValue()) }) + props.onEditorReady?.(monacoEditor) }) createEffect(() => { monacoEditor.setValue(props.value) @@ -73,5 +90,5 @@ export const MonacoEditor = (props: MonacoEditorProps) => { model && model.dispose() monacoEditor && monacoEditor.dispose() }) - return + return } diff --git a/src/pages/home/previews/text-editor.tsx b/src/pages/home/previews/text-editor.tsx index 1074bebf3..c702808d4 100644 --- a/src/pages/home/previews/text-editor.tsx +++ b/src/pages/home/previews/text-editor.tsx @@ -1,12 +1,24 @@ -import { Button, useColorMode, VStack } from "@hope-ui/solid" +import { + Box, + Button, + HStack, + IconButton, + useColorMode, + VStack, +} from "@hope-ui/solid" import { createEffect, createMemo, createSignal, on, Show } from "solid-js" import { EncodingSelect, MaybeLoading } from "~/components" import { MonacoEditorLoader } from "~/components/MonacoEditor" import { useFetch, useFetchText, useParseText, useRouter, useT } from "~/hooks" -import { objStore, userCan } from "~/store" +import { objStore, setLocal, userCan } from "~/store" +import { local } from "~/store" import { PEmptyResp } from "~/types" import { handleResp, notify, r } from "~/utils" import { createShortcut } from "@solid-primitives/keyboard" +import { BiRegularRedo, BiRegularUndo } from "solid-icons/bi" +import { TbDeviceFloppy, TbTextWrap, TbTextWrapDisabled } from "solid-icons/tb" +import { FaSolidMinus, FaSolidPlus } from "solid-icons/fa" +import type * as monacoType from "monaco-editor/esm/vs/editor/editor.api.js" function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { const { colorMode } = useColorMode() @@ -18,6 +30,29 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { const [encoding, setEncoding] = createSignal("utf-8") const [value, setValue] = createSignal(text(encoding())) const t = useT() + + // Editor instance reference + const [editor, setEditor] = + createSignal() + + // Track modified state + const [modified, setModified] = createSignal(false) + const [cursorLine, setCursorLine] = createSignal(1) + const [cursorColumn, setCursorColumn] = createSignal(1) + const [wordCount, setWordCount] = createSignal(0) + const [language, setLanguage] = createSignal("") + const [wordWrap, setWordWrap] = createSignal(false) + + // Save on Ctrl+S / Cmd+S + createShortcut(["Control", "S"], (e: KeyboardEvent | null) => { + e?.preventDefault() + onSave() + }) + createShortcut(["Meta", "S"], (e: KeyboardEvent | null) => { + e?.preventDefault() + onSave() + }) + const [loading, save] = useFetch( (): PEmptyResp => r.put("/fs/put", value(), { @@ -27,9 +62,11 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { }, }), ) + createEffect( on(encoding, (v) => { setValue(text(v)) + setModified(false) }), ) @@ -37,43 +74,219 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { const resp = await save() handleResp(resp, () => { notify.success(t("global.save_success")) + setModified(false) + }) + } + + function onEditorReady(ed: monacoType.editor.IStandaloneCodeEditor) { + setEditor(ed) + + // Track cursor position + ed.onDidChangeCursorPosition((e) => { + setCursorLine(e.position.lineNumber) + setCursorColumn(e.position.column) }) + + // Track content changes for modified state + let savedVersionId = ed.getModel()?.getAlternativeVersionId() ?? 0 + ed.onDidChangeModelContent(() => { + const currentVersionId = ed.getModel()?.getAlternativeVersionId() ?? 0 + setModified(currentVersionId !== savedVersionId) + updateWordCount(ed) + }) + + // Initial word count + updateWordCount(ed) + + // Detect language from model + const lang = ed.getModel()?.getLanguageId() ?? "" + setLanguage(lang) + } + + function updateWordCount(ed: monacoType.editor.IStandaloneCodeEditor) { + const content = ed.getValue() + if (!content) { + setWordCount(0) + return + } + const trimmed = content.trim() + setWordCount(trimmed ? trimmed.split(/\s+/).length : 0) } - createShortcut(["Control", "S"], onSave) + function undo() { + editor()?.trigger("toolbar", "undo", null) + } + + function redo() { + editor()?.trigger("toolbar", "redo", null) + } + + function toggleWordWrap() { + const next = !wordWrap() + setWordWrap(next) + editor()?.updateOptions({ wordWrap: next ? "on" : "off" }) + } + + function changeFontSize(delta: number) { + const current = parseInt(local.editor_font_size) || 14 + const next = Math.max(8, Math.min(40, current + delta)) + setLocal("editor_font_size", String(next)) + } return ( - - - + {/* Toolbar */} + + + + + + } + size="sm" + variant="ghost" + onClick={undo} + title={`${t("global.undo")} (Ctrl+Z)`} + /> + } + size="sm" + variant="ghost" + onClick={redo} + title={`${t("global.redo")} (Ctrl+Y)`} /> - + + + + : } + size="sm" + variant="ghost" + onClick={toggleWordWrap} + title={t("global.wrap")} + color={wordWrap() ? "$info11" : undefined} + /> + + + } + size="sm" + variant="ghost" + onClick={() => changeFontSize(-1)} + title={t("global.font_size")} + /> + + {local.editor_font_size} + + } + size="sm" + variant="ghost" + onClick={() => changeFontSize(1)} + title={t("global.font_size")} + /> + + + + + + + + + + {/* Editor */} { - setValue(value) - }} + onChange={(val) => setValue(val)} + onEditorReady={onEditorReady} /> - - - + + Ln {cursorLine()}, Col {cursorColumn()} + + {wordCount()} words + + + {language()} + + {encoding()} + + + ● {t("global.modified")} + + + ) } -// TODO add encoding select const TextEditor = () => { const [content] = useFetchText() return ( From 6fc3f4ef2a6e623a00353c583126fec9b75d71fa Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Wed, 17 Jun 2026 00:28:29 +0800 Subject: [PATCH 02/18] feat(monaco): add webpage fullscreen Signed-off-by: MadDogOwner --- src/pages/home/previews/text-editor.tsx | 57 ++++++++++++++++++++----- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/src/pages/home/previews/text-editor.tsx b/src/pages/home/previews/text-editor.tsx index c702808d4..23ae7921e 100644 --- a/src/pages/home/previews/text-editor.tsx +++ b/src/pages/home/previews/text-editor.tsx @@ -18,6 +18,7 @@ import { createShortcut } from "@solid-primitives/keyboard" import { BiRegularRedo, BiRegularUndo } from "solid-icons/bi" import { TbDeviceFloppy, TbTextWrap, TbTextWrapDisabled } from "solid-icons/tb" import { FaSolidMinus, FaSolidPlus } from "solid-icons/fa" +import { AiOutlineFullscreen, AiOutlineFullscreenExit } from "solid-icons/ai" import type * as monacoType from "monaco-editor/esm/vs/editor/editor.api.js" function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { @@ -42,6 +43,7 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { const [wordCount, setWordCount] = createSignal(0) const [language, setLanguage] = createSignal("") const [wordWrap, setWordWrap] = createSignal(false) + const [fullscreen, setFullscreen] = createSignal(false) // Save on Ctrl+S / Cmd+S createShortcut(["Control", "S"], (e: KeyboardEvent | null) => { @@ -52,6 +54,10 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { e?.preventDefault() onSave() }) + // Escape to exit fullscreen + createShortcut(["Escape"], () => { + if (fullscreen()) setFullscreen(false) + }) const [loading, save] = useFetch( (): PEmptyResp => @@ -134,7 +140,17 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { } return ( - + {/* Toolbar */} changeFontSize(1)} title={t("global.font_size")} /> - - - - - - + + + + : + } + size="sm" + variant="ghost" + onClick={() => setFullscreen(!fullscreen())} + title="Fullscreen (Esc)" + color={fullscreen() ? "$info11" : undefined} + /> + + + + + + {/* Editor */} From cfc12b2d6ea58aa5d2acf347d24d07f6fc9b6849 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Wed, 17 Jun 2026 00:41:36 +0800 Subject: [PATCH 03/18] feat(monaco): finetune layout Signed-off-by: MadDogOwner --- src/pages/home/previews/text-editor.tsx | 36 +++++++------------------ 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/src/pages/home/previews/text-editor.tsx b/src/pages/home/previews/text-editor.tsx index 23ae7921e..67d501a8f 100644 --- a/src/pages/home/previews/text-editor.tsx +++ b/src/pages/home/previews/text-editor.tsx @@ -162,33 +162,15 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { overflowX="auto" flexShrink={0} > - } > - - + {t("global.save")} + - + } From 329215591c7511cd61906990fb29c53ccdcc785e Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Wed, 17 Jun 2026 01:09:07 +0800 Subject: [PATCH 04/18] perf: optimize encoding detection with BOM detection and confidence filtering - Skip string inputs (TextEncoder always produces UTF-8) - Add BOM detection for UTF-8/UTF-16 LE/BE (fast and accurate) - Filter chardet results by minimum confidence threshold (30%) - Hoist encodingLabels to module scope with Set for O(1) lookup --- src/components/EncodingSelect.tsx | 160 +++++++++++++++++++----------- 1 file changed, 102 insertions(+), 58 deletions(-) diff --git a/src/components/EncodingSelect.tsx b/src/components/EncodingSelect.tsx index ae79f1ccb..d4d85c4b9 100644 --- a/src/components/EncodingSelect.tsx +++ b/src/components/EncodingSelect.tsx @@ -3,70 +3,114 @@ import { SelectWrapper } from "./Base" import chardet from "chardet" import { createEffect } from "solid-js" +const MIN_CONFIDENCE = 30 + +const encodingLabels = [ + "utf-8", + "gbk", + "gb18030", + "ibm866", + "iso-8859-2", + "iso-8859-3", + "iso-8859-4", + "iso-8859-5", + "iso-8859-6", + "iso-8859-7", + "iso-8859-8", + "iso-8859-8i", + "iso-8859-10", + "iso-8859-13", + "iso-8859-14", + "iso-8859-15", + "iso-8859-16", + "koi8-r", + "koi8-u", + "macintosh", + "windows-874", + "windows-1250", + "windows-1251", + "windows-1252", + "windows-1253", + "windows-1254", + "windows-1255", + "windows-1256", + "windows-1257", + "windows-1258", + "x-mac-cyrillic", + "big5", + "euc-jp", + "iso-2022-jp", + "shift_jis", + "euc-kr", + "iso-2022-kr", + "utf-16be", + "utf-16le", + "x-user-defined", + "iso-2022-cn", +] + +const encodingLabelSet = new Set(encodingLabels) + +/** + * Detect encoding from BOM (Byte Order Mark). + * Returns the detected encoding or undefined if no BOM found. + */ +function detectBOM(buffer: Uint8Array): string | undefined { + if ( + buffer.length >= 3 && + buffer[0] === 0xef && + buffer[1] === 0xbb && + buffer[2] === 0xbf + ) { + return "utf-8" + } + if (buffer.length >= 2) { + if (buffer[0] === 0xff && buffer[1] === 0xfe) return "utf-16le" + if (buffer[0] === 0xfe && buffer[1] === 0xff) return "utf-16be" + } + return undefined +} + +/** + * Detect encoding using chardet with confidence filtering. + * Returns the best match or undefined if no confident match found. + */ +function detectByChardet(buffer: Uint8Array): string | undefined { + const results = chardet.analyse(buffer) + for (const result of results) { + if (result.confidence >= MIN_CONFIDENCE) { + const label = result.name.toLowerCase() + if (encodingLabelSet.has(label)) { + return label + } + } + } + return undefined +} + export function EncodingSelect(props: { encoding: string setEncoding: (encoding: string) => void referenceText?: string | ArrayBuffer }) { - const encodingLabels = [ - "utf-8", - "gbk", - "gb18030", - "ibm866", - "iso-8859-2", - "iso-8859-3", - "iso-8859-4", - "iso-8859-5", - "iso-8859-6", - "iso-8859-7", - "iso-8859-8", - "iso-8859-8i", - "iso-8859-10", - "iso-8859-13", - "iso-8859-14", - "iso-8859-15", - "iso-8859-16", - "koi8-r", - "koi8-u", - "macintosh", - "windows-874", - "windows-1250", - "windows-1251", - "windows-1252", - "windows-1253", - "windows-1254", - "windows-1255", - "windows-1256", - "windows-1257", - "windows-1258", - "x-mac-cyrillic", - "big5", - "euc-jp", - "iso-2022-jp", - "shift_jis", - "euc-kr", - "iso-2022-kr", - "utf-16be", - "utf-16le", - "x-user-defined", - "iso-2022-cn", - ] - createEffect(() => { - if (props.referenceText) { - let buffer: Uint8Array - if (typeof props.referenceText === "string") { - buffer = new TextEncoder().encode(props.referenceText) - } else { - buffer = new Uint8Array(props.referenceText) - } - for (let encoding of chardet.analyse(buffer)) { - const encodingLabel = encoding.name.toLowerCase() - if (encodingLabels.includes(encodingLabel)) { - props.setEncoding(encodingLabel) - return - } - } + const data = props.referenceText + // Skip detection for strings - they are already decoded (TextEncoder always produces UTF-8) + if (!data || typeof data === "string") return + + const buffer = new Uint8Array(data) + + // 1. Try BOM detection first (fast and accurate) + const bomEncoding = detectBOM(buffer) + if (bomEncoding) { + props.setEncoding(bomEncoding) + return + } + + // 2. Fall back to chardet with confidence filtering + const detected = detectByChardet(buffer) + if (detected) { + props.setEncoding(detected) } }) From 30de012628c05fb617bb368a714b5f9fc5a82129 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Wed, 17 Jun 2026 01:15:02 +0800 Subject: [PATCH 05/18] fix: MonacoEditor falsely reports modified state on open Use on() with defer:true to skip the initial setValue call, preventing a false onDidChangeModelContent trigger that increments the version ID. --- src/components/MonacoEditor.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/MonacoEditor.tsx b/src/components/MonacoEditor.tsx index 2ccfb2ecf..beed0d78f 100644 --- a/src/components/MonacoEditor.tsx +++ b/src/components/MonacoEditor.tsx @@ -1,5 +1,5 @@ import { Box } from "@hope-ui/solid" -import { createEffect, createSignal, onCleanup, onMount } from "solid-js" +import { createEffect, createSignal, on, onCleanup, onMount } from "solid-js" import { MaybeLoading } from "./FullLoading" import loader from "@monaco-editor/loader" import { useCDN } from "~/hooks" @@ -72,9 +72,15 @@ export const MonacoEditor = (props: MonacoEditorProps) => { }) props.onEditorReady?.(monacoEditor) }) - createEffect(() => { - monacoEditor.setValue(props.value) - }) + createEffect( + on( + () => props.value, + (value) => { + monacoEditor.setValue(value) + }, + { defer: true }, + ), + ) createEffect(() => { monaco.editor.setTheme(props.theme) From 6ca526a857651224edd6bd1179ffe888b32c82a7 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Wed, 17 Jun 2026 01:20:56 +0800 Subject: [PATCH 06/18] feat: add exit confirmation for unsaved changes in text editor --- src/lang/en/global.json | 1 + src/pages/home/previews/text-editor.tsx | 30 ++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/lang/en/global.json b/src/lang/en/global.json index 7561b2da5..7456d56b1 100644 --- a/src/lang/en/global.json +++ b/src/lang/en/global.json @@ -28,6 +28,7 @@ "close": "Close", "no_support_now": "Not currently supported", "empty_input": "Please enter", + "unsaved_changes_confirm": "You have unsaved changes. Are you sure you want to leave?", "invalid_filename_chars": "Filename cannot contain: / \\ ? < > * : | \"", "invalid_regex": "Invalid regular expression", "name": "Name", diff --git a/src/pages/home/previews/text-editor.tsx b/src/pages/home/previews/text-editor.tsx index 67d501a8f..86defdc4e 100644 --- a/src/pages/home/previews/text-editor.tsx +++ b/src/pages/home/previews/text-editor.tsx @@ -6,7 +6,15 @@ import { useColorMode, VStack, } from "@hope-ui/solid" -import { createEffect, createMemo, createSignal, on, Show } from "solid-js" +import { + createEffect, + createMemo, + createSignal, + on, + onCleanup, + Show, +} from "solid-js" +import { useBeforeLeave } from "@solidjs/router" import { EncodingSelect, MaybeLoading } from "~/components" import { MonacoEditorLoader } from "~/components/MonacoEditor" import { useFetch, useFetchText, useParseText, useRouter, useT } from "~/hooks" @@ -45,6 +53,26 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { const [wordWrap, setWordWrap] = createSignal(false) const [fullscreen, setFullscreen] = createSignal(false) + // Warn on browser close/refresh when there are unsaved changes + const beforeUnloadHandler = (e: BeforeUnloadEvent) => { + if (modified()) { + e.preventDefault() + } + } + window.addEventListener("beforeunload", beforeUnloadHandler) + onCleanup(() => + window.removeEventListener("beforeunload", beforeUnloadHandler), + ) + + // Warn on in-app navigation when there are unsaved changes + useBeforeLeave((e) => { + if (modified()) { + if (!window.confirm(t("global.unsaved_changes_confirm"))) { + e.preventDefault() + } + } + }) + // Save on Ctrl+S / Cmd+S createShortcut(["Control", "S"], (e: KeyboardEvent | null) => { e?.preventDefault() From 0184e1779a8b012bcf350d60deb72e4d0a0516b0 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Wed, 17 Jun 2026 11:11:32 +0800 Subject: [PATCH 07/18] feat: add language switch Signed-off-by: MadDogOwner --- src/components/MonacoEditor.tsx | 14 ++- src/pages/home/previews/text-editor.tsx | 111 +++++++++++++++++++++++- 2 files changed, 121 insertions(+), 4 deletions(-) diff --git a/src/components/MonacoEditor.tsx b/src/components/MonacoEditor.tsx index beed0d78f..8a71f2d84 100644 --- a/src/components/MonacoEditor.tsx +++ b/src/components/MonacoEditor.tsx @@ -14,7 +14,7 @@ export interface MonacoEditorProps { language?: string onEditorReady?: (editor: monacoType.editor.IStandaloneCodeEditor) => void } -let monaco: typeof monacoType +export let monaco: typeof monacoType export const MonacoEditorLoader = (props: MonacoEditorProps) => { const { monacoPath } = useCDN() @@ -86,6 +86,18 @@ export const MonacoEditor = (props: MonacoEditorProps) => { monaco.editor.setTheme(props.theme) }) + createEffect( + on( + () => props.language, + (lang) => { + if (lang && model) { + monaco.editor.setModelLanguage(model, lang) + } + }, + { defer: true }, + ), + ) + createEffect(() => { monacoEditor?.updateOptions({ fontSize: parseInt(local.editor_font_size), diff --git a/src/pages/home/previews/text-editor.tsx b/src/pages/home/previews/text-editor.tsx index 86defdc4e..960cc7606 100644 --- a/src/pages/home/previews/text-editor.tsx +++ b/src/pages/home/previews/text-editor.tsx @@ -3,6 +3,11 @@ import { Button, HStack, IconButton, + Input, + Popover, + PopoverBody, + PopoverContent, + PopoverTrigger, useColorMode, VStack, } from "@hope-ui/solid" @@ -10,13 +15,14 @@ import { createEffect, createMemo, createSignal, + For, on, onCleanup, Show, } from "solid-js" import { useBeforeLeave } from "@solidjs/router" import { EncodingSelect, MaybeLoading } from "~/components" -import { MonacoEditorLoader } from "~/components/MonacoEditor" +import { MonacoEditorLoader, monaco } from "~/components/MonacoEditor" import { useFetch, useFetchText, useParseText, useRouter, useT } from "~/hooks" import { objStore, setLocal, userCan } from "~/store" import { local } from "~/store" @@ -24,11 +30,21 @@ import { PEmptyResp } from "~/types" import { handleResp, notify, r } from "~/utils" import { createShortcut } from "@solid-primitives/keyboard" import { BiRegularRedo, BiRegularUndo } from "solid-icons/bi" -import { TbDeviceFloppy, TbTextWrap, TbTextWrapDisabled } from "solid-icons/tb" +import { + TbBraces, + TbDeviceFloppy, + TbTextWrap, + TbTextWrapDisabled, +} from "solid-icons/tb" import { FaSolidMinus, FaSolidPlus } from "solid-icons/fa" import { AiOutlineFullscreen, AiOutlineFullscreenExit } from "solid-icons/ai" import type * as monacoType from "monaco-editor/esm/vs/editor/editor.api.js" +interface LanguageOption { + id: string + aliases?: string[] +} + function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { const { colorMode } = useColorMode() const theme = createMemo(() => { @@ -50,8 +66,25 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { const [cursorColumn, setCursorColumn] = createSignal(1) const [wordCount, setWordCount] = createSignal(0) const [language, setLanguage] = createSignal("") + const [languageOptions, setLanguageOptions] = createSignal( + [], + ) const [wordWrap, setWordWrap] = createSignal(false) const [fullscreen, setFullscreen] = createSignal(false) + const [langSearch, setLangSearch] = createSignal("") + const filteredLanguages = createMemo(() => { + const s = langSearch().toLowerCase() + if (!s) return languageOptions() + return languageOptions().filter( + (l) => + l.id.toLowerCase().includes(s) || + l.aliases?.some((a) => a.toLowerCase().includes(s)), + ) + }) + const languageDisplayName = createMemo(() => { + const lang = languageOptions().find((l) => l.id === language()) + return lang?.aliases?.[0] || language() + }) // Warn on browser close/refresh when there are unsaved changes const beforeUnloadHandler = (e: BeforeUnloadEvent) => { @@ -135,6 +168,12 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { // Detect language from model const lang = ed.getModel()?.getLanguageId() ?? "" setLanguage(lang) + + // Populate language options from Monaco + if (monaco?.languages?.getLanguages) { + const langs = monaco.languages.getLanguages() as LanguageOption[] + setLanguageOptions(langs.sort((a, b) => a.id.localeCompare(b.id))) + } } function updateWordCount(ed: monacoType.editor.IStandaloneCodeEditor) { @@ -296,6 +335,7 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { setValue(val)} onEditorReady={onEditorReady} @@ -319,7 +359,72 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { {wordCount()} words - {language()} + setLangSearch("")}> + + + + {languageDisplayName()} + + + + + setLangSearch(e.currentTarget.value)} + mb="$2" + autofocus + /> + + + {(lang) => ( + { + setLanguage(lang.id) + setLangSearch("") + }} + > + {lang.aliases?.[0] || lang.id} + + + ({lang.id}) + + + + )} + + + + + {encoding()} From 5ec26b870155d26a95efd2279e5480ad30934a94 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Wed, 17 Jun 2026 11:25:47 +0800 Subject: [PATCH 08/18] feat: support close minimap Signed-off-by: MadDogOwner --- src/components/MonacoEditor.tsx | 26 ++++-------- src/pages/home/previews/text-editor.tsx | 54 +++++++++++++------------ 2 files changed, 36 insertions(+), 44 deletions(-) diff --git a/src/components/MonacoEditor.tsx b/src/components/MonacoEditor.tsx index 8a71f2d84..715814ee5 100644 --- a/src/components/MonacoEditor.tsx +++ b/src/components/MonacoEditor.tsx @@ -9,9 +9,9 @@ import { local } from "~/store" export interface MonacoEditorProps { value: string onChange?: (value: string) => void - theme: "vs" | "vs-dark" path?: string language?: string + options?: monacoType.editor.IStandaloneEditorConstructionOptions onEditorReady?: (editor: monacoType.editor.IStandaloneCodeEditor) => void } export let monaco: typeof monacoType @@ -41,26 +41,13 @@ export const MonacoEditor = (props: MonacoEditorProps) => { let model: monacoType.editor.ITextModel onMount(() => { - monacoEditor = monaco.editor.create(monacoEditorDiv!, { + const constructionOptions = { + ...props.options, value: props.value, - theme: props.theme, fontSize: parseInt(local.editor_font_size), - minimap: { enabled: true }, - lineNumbers: "on", - scrollBeyondLastLine: false, - smoothScrolling: true, - cursorBlinking: "smooth", - cursorSmoothCaretAnimation: "on", - bracketPairColorization: { enabled: true }, automaticLayout: true, - padding: { top: 8 }, - wordWrap: "off", - renderLineHighlight: "all", - scrollbar: { - verticalScrollbarSize: 10, - horizontalScrollbarSize: 10, - }, - }) + } + monacoEditor = monaco.editor.create(monacoEditorDiv!, constructionOptions) model = monaco.editor.createModel( props.value, props.language, @@ -83,7 +70,7 @@ export const MonacoEditor = (props: MonacoEditorProps) => { ) createEffect(() => { - monaco.editor.setTheme(props.theme) + monaco.editor.setTheme(props.options?.theme ?? "vs") }) createEffect( @@ -101,6 +88,7 @@ export const MonacoEditor = (props: MonacoEditorProps) => { createEffect(() => { monacoEditor?.updateOptions({ fontSize: parseInt(local.editor_font_size), + ...props.options, }) }) diff --git a/src/pages/home/previews/text-editor.tsx b/src/pages/home/previews/text-editor.tsx index 960cc7606..fa058b3d5 100644 --- a/src/pages/home/previews/text-editor.tsx +++ b/src/pages/home/previews/text-editor.tsx @@ -24,7 +24,7 @@ import { useBeforeLeave } from "@solidjs/router" import { EncodingSelect, MaybeLoading } from "~/components" import { MonacoEditorLoader, monaco } from "~/components/MonacoEditor" import { useFetch, useFetchText, useParseText, useRouter, useT } from "~/hooks" -import { objStore, setLocal, userCan } from "~/store" +import { objStore, setLocal } from "~/store" import { local } from "~/store" import { PEmptyResp } from "~/types" import { handleResp, notify, r } from "~/utils" @@ -35,6 +35,8 @@ import { TbDeviceFloppy, TbTextWrap, TbTextWrapDisabled, + TbMap, + TbMapOff, } from "solid-icons/tb" import { FaSolidMinus, FaSolidPlus } from "solid-icons/fa" import { AiOutlineFullscreen, AiOutlineFullscreenExit } from "solid-icons/ai" @@ -70,6 +72,7 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { [], ) const [wordWrap, setWordWrap] = createSignal(false) + const [minimap, setMinimap] = createSignal(true) const [fullscreen, setFullscreen] = createSignal(false) const [langSearch, setLangSearch] = createSignal("") const filteredLanguages = createMemo(() => { @@ -195,9 +198,7 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { } function toggleWordWrap() { - const next = !wordWrap() - setWordWrap(next) - editor()?.updateOptions({ wordWrap: next ? "on" : "off" }) + setWordWrap(!wordWrap()) } function changeFontSize(delta: number) { @@ -256,12 +257,7 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { title={`${t("global.redo")} (Ctrl+Y)`} /> - + + : } + size="sm" + variant="ghost" + onClick={() => setMinimap(!minimap())} + title="Minimap" + color={minimap() ? "$info11" : undefined} + /> + + + - - setValue(val)} onEditorReady={onEditorReady} /> @@ -353,6 +358,11 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { color="$neutral11" flexShrink={0} > + + + ● {t("global.modified")} + + Ln {cursorLine()}, Col {cursorColumn()} @@ -374,7 +384,7 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { {languageDisplayName()} - + - {encoding()} - - - ● {t("global.modified")} - - ) From c1f4042bf82a0213d84b25e174bd03703104ee59 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Fri, 26 Jun 2026 15:23:02 +0800 Subject: [PATCH 09/18] fix: remove untranslated modified Signed-off-by: MadDogOwner --- src/pages/home/previews/text-editor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/previews/text-editor.tsx b/src/pages/home/previews/text-editor.tsx index fa058b3d5..da8005c22 100644 --- a/src/pages/home/previews/text-editor.tsx +++ b/src/pages/home/previews/text-editor.tsx @@ -360,7 +360,7 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { > - ● {t("global.modified")} + ● From 4528e1b211eaf704e77e1347fb2e23fd9ca1435e Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Fri, 26 Jun 2026 15:38:56 +0800 Subject: [PATCH 10/18] refactor: reuse StreamUpload form home/uploads/stream Signed-off-by: MadDogOwner --- src/pages/home/previews/text-editor.tsx | 33 ++++++++++++------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/pages/home/previews/text-editor.tsx b/src/pages/home/previews/text-editor.tsx index da8005c22..2c0608fce 100644 --- a/src/pages/home/previews/text-editor.tsx +++ b/src/pages/home/previews/text-editor.tsx @@ -23,11 +23,11 @@ import { import { useBeforeLeave } from "@solidjs/router" import { EncodingSelect, MaybeLoading } from "~/components" import { MonacoEditorLoader, monaco } from "~/components/MonacoEditor" -import { useFetch, useFetchText, useParseText, useRouter, useT } from "~/hooks" +import { useFetchText, useParseText, useRouter, useT } from "~/hooks" import { objStore, setLocal } from "~/store" import { local } from "~/store" -import { PEmptyResp } from "~/types" -import { handleResp, notify, r } from "~/utils" +import { notify } from "~/utils" +import { StreamUpload } from "~/pages/home/uploads/stream" import { createShortcut } from "@solid-primitives/keyboard" import { BiRegularRedo, BiRegularUndo } from "solid-icons/bi" import { @@ -74,6 +74,7 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { const [wordWrap, setWordWrap] = createSignal(false) const [minimap, setMinimap] = createSignal(true) const [fullscreen, setFullscreen] = createSignal(false) + const [saving, setSaving] = createSignal(false) const [langSearch, setLangSearch] = createSignal("") const filteredLanguages = createMemo(() => { const s = langSearch().toLowerCase() @@ -123,16 +124,6 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { if (fullscreen()) setFullscreen(false) }) - const [loading, save] = useFetch( - (): PEmptyResp => - r.put("/fs/put", value(), { - headers: { - "File-Path": encodeURIComponent(pathname()), - "Content-Type": props.contentType || "text/plain", - }, - }), - ) - createEffect( on(encoding, (v) => { setValue(text(v)) @@ -141,11 +132,19 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { ) async function onSave() { - const resp = await save() - handleResp(resp, () => { + setSaving(true) + try { + const file = new File([value()], objStore.obj.name, { + type: props.contentType || "text/plain", + }) + await StreamUpload(pathname(), file, () => {}, false, true, false) notify.success(t("global.save_success")) setModified(false) - }) + } catch (e: any) { + notify.error(e.message) + } finally { + setSaving(false) + } } function onEditorReady(ed: monacoType.editor.IStandaloneCodeEditor) { @@ -233,7 +232,7 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { + + - } - size="sm" - variant="ghost" - onClick={undo} - title={`${t("global.undo")} (Ctrl+Z)`} - /> - } - size="sm" - variant="ghost" - onClick={redo} - title={`${t("global.redo")} (Ctrl+Y)`} - /> + } + size="sm" + variant="ghost" + onClick={undo} + title={`${t("global.undo")} (Ctrl+Z)`} + /> + } + size="sm" + variant="ghost" + onClick={redo} + title={`${t("global.redo")} (Ctrl+Y)`} + /> - + + setValue(val)} onEditorReady={onEditorReady} From d36085a22fa27b7e75cdb8f3ccd7560ef1008ca9 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Fri, 26 Jun 2026 16:28:00 +0800 Subject: [PATCH 13/18] style: revert MonacoEditor minimum height to 60vh Signed-off-by: MadDogOwner --- src/components/MonacoEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MonacoEditor.tsx b/src/components/MonacoEditor.tsx index 715814ee5..d704a359f 100644 --- a/src/components/MonacoEditor.tsx +++ b/src/components/MonacoEditor.tsx @@ -96,5 +96,5 @@ export const MonacoEditor = (props: MonacoEditorProps) => { model && model.dispose() monacoEditor && monacoEditor.dispose() }) - return + return } From 4c3c3d5b4719bac5be78e994b22319eb2c90b8aa Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Fri, 26 Jun 2026 16:52:00 +0800 Subject: [PATCH 14/18] refactor: implement copy-to-clipboard Signed-off-by: MadDogOwner --- package.json | 1 - pnpm-lock.yaml | 15 ------- src/hooks/useUtil.ts | 41 +++++++++++++++++-- src/lang/en/global.json | 3 ++ src/pages/home/previews/text-editor.tsx | 52 ++++++++++++++++++++++++- 5 files changed, 92 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index f3befb288..544e2f2b6 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,6 @@ "bencode": "^4.0.0", "chardet": "^2.1.1", "clsx": "^2.1.1", - "copy-to-clipboard": "^3.3.3", "crypto-js": "^4.2.0", "docx-preview": "^0.3.7", "handlebars": "^4.7.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad0444211..238b059c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,9 +71,6 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 - copy-to-clipboard: - specifier: ^3.3.3 - version: 3.3.3 crypto-js: specifier: ^4.2.0 version: 4.2.0 @@ -1962,9 +1959,6 @@ packages: convert-string@0.1.0: resolution: {integrity: sha512-1KX9ESmtl8xpT2LN2tFnKSbV4NiarbVi8DVb39ZriijvtTklyrT+4dT1wsGMHKD3CJUjXgvJzstm9qL9ICojGA==} - copy-to-clipboard@3.3.3: - resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} - core-js-compat@3.48.0: resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==} @@ -3389,9 +3383,6 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - toggle-selection@1.0.6: - resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} - tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -5595,10 +5586,6 @@ snapshots: convert-string@0.1.0: {} - copy-to-clipboard@3.3.3: - dependencies: - toggle-selection: 1.0.6 - core-js-compat@3.48.0: dependencies: browserslist: 4.28.1 @@ -7278,8 +7265,6 @@ snapshots: dependencies: is-number: 7.0.0 - toggle-selection@1.0.6: {} - tr46@0.0.3: {} trim-lines@3.0.1: {} diff --git a/src/hooks/useUtil.ts b/src/hooks/useUtil.ts index 696073f93..40c54b079 100644 --- a/src/hooks/useUtil.ts +++ b/src/hooks/useUtil.ts @@ -1,18 +1,53 @@ -import copy from "copy-to-clipboard" import { createResource } from "solid-js" import { getHideFiles, objStore } from "~/store" import { Obj } from "~/types" import { decodeText, fetchText, notify, pathJoin } from "~/utils" import { useT, useLink, useRouter } from "." +async function checkClipboardPermission( + mode: "clipboard-read" | "clipboard-write", +): Promise { + try { + const status = await navigator.permissions.query({ + name: mode as PermissionName, + }) + return status.state === "granted" || status.state === "prompt" + } catch { + // Safari does not support permissions.query for clipboard; + // assume permission is available if navigator.clipboard exists. + return !!navigator.clipboard + } +} + export const useUtil = () => { const t = useT() const { pathname } = useRouter() return { - copy: (text: string) => { - copy(text) + copy: async (text: string) => { + try { + if (await checkClipboardPermission("clipboard-write")) { + await navigator.clipboard.writeText(text) + } else { + throw new Error("permission denied") + } + } catch { + const ta = document.createElement("textarea") + ta.value = text + ta.style.position = "fixed" + ta.style.opacity = "0" + document.body.appendChild(ta) + ta.select() + document.execCommand("copy") + document.body.removeChild(ta) + } notify.success(t("global.copied")) }, + paste: async (): Promise => { + if (!(await checkClipboardPermission("clipboard-read"))) { + throw new Error(t("global.clipboard_denied")) + } + return navigator.clipboard.readText() + }, isHide: (obj: Obj) => { const hideFiles = getHideFiles() for (const reg of hideFiles) { diff --git a/src/lang/en/global.json b/src/lang/en/global.json index 7456d56b1..f6c5e1985 100644 --- a/src/lang/en/global.json +++ b/src/lang/en/global.json @@ -5,7 +5,10 @@ "delete": "Delete", "save": "Save", "update": "Update", + "copy": "Copy", + "paste": "Paste", "copied": "Copied", + "clipboard_denied": "Clipboard access denied. Please allow clipboard permission in your browser settings.", "delete_success": "Deleted Successfully", "save_success": "Saved Successfully", "update_success": "Updated Successfully", diff --git a/src/pages/home/previews/text-editor.tsx b/src/pages/home/previews/text-editor.tsx index d3cad2e24..5ccfd8f3f 100644 --- a/src/pages/home/previews/text-editor.tsx +++ b/src/pages/home/previews/text-editor.tsx @@ -23,7 +23,7 @@ import { import { useBeforeLeave } from "@solidjs/router" import { EncodingSelect, MaybeLoading } from "~/components" import { MonacoEditorLoader, monaco } from "~/components/MonacoEditor" -import { useFetchText, useParseText, useRouter, useT } from "~/hooks" +import { useFetchText, useParseText, useRouter, useT, useUtil } from "~/hooks" import { objStore, setLocal, userCan } from "~/store" import { local } from "~/store" import { notify } from "~/utils" @@ -32,6 +32,8 @@ import { createShortcut } from "@solid-primitives/keyboard" import { BiRegularRedo, BiRegularUndo } from "solid-icons/bi" import { TbBraces, + TbClipboardText, + TbCopy, TbDeviceFloppy, TbTextWrap, TbTextWrapDisabled, @@ -57,6 +59,7 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { const [encoding, setEncoding] = createSignal("utf-8") const [value, setValue] = createSignal(text(encoding())) const t = useT() + const { copy, paste } = useUtil() // Editor instance reference const [editor, setEditor] = @@ -209,6 +212,32 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { editor()?.trigger("toolbar", "redo", null) } + async function copyToClipboard() { + const ed = editor() + if (!ed) return + const sel = ed.getSelection() + const selection = sel ? (ed.getModel()?.getValueInRange(sel) ?? "") : "" + const text = selection || ed.getValue() + await copy(text) + } + + async function pasteFromClipboard() { + const ed = editor() + if (!ed) return + + const text = await paste() + if (!text) return + const selection = ed.getSelection() + if (selection) { + ed.executeEdits("paste", [ + { range: selection, text, forceMoveMarkers: true }, + ]) + } else { + ed.trigger("keyboard", "paste", { text }) + } + ed.focus() + } + function toggleWordWrap() { const next = !wordWrap() setWordWrap(next) @@ -275,6 +304,27 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { + } + size="sm" + variant="ghost" + onClick={copyToClipboard} + title={t("global.copy")} + /> + + } + size="sm" + variant="ghost" + onClick={pasteFromClipboard} + title={t("global.paste")} + /> + + + + : } From d46e3ab550a8470c854e74da6f3c06a2cedf44e0 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Fri, 26 Jun 2026 17:17:38 +0800 Subject: [PATCH 15/18] chore: adjust button properties Signed-off-by: MadDogOwner --- src/hooks/useUtil.ts | 11 ++++++--- src/pages/home/previews/text-editor.tsx | 30 ++++++++++++------------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/hooks/useUtil.ts b/src/hooks/useUtil.ts index 40c54b079..7c60c6500 100644 --- a/src/hooks/useUtil.ts +++ b/src/hooks/useUtil.ts @@ -43,10 +43,15 @@ export const useUtil = () => { notify.success(t("global.copied")) }, paste: async (): Promise => { - if (!(await checkClipboardPermission("clipboard-read"))) { - throw new Error(t("global.clipboard_denied")) + try { + if (!(await checkClipboardPermission("clipboard-read"))) { + throw new Error(t("global.clipboard_denied")) + } + return navigator.clipboard.readText() + } catch (e: any) { + notify.error(e.message || t("global.clipboard_denied")) + return "" } - return navigator.clipboard.readText() }, isHide: (obj: Obj) => { const hideFiles = getHideFiles() diff --git a/src/pages/home/previews/text-editor.tsx b/src/pages/home/previews/text-editor.tsx index 5ccfd8f3f..9091922bb 100644 --- a/src/pages/home/previews/text-editor.tsx +++ b/src/pages/home/previews/text-editor.tsx @@ -1,6 +1,7 @@ import { Box, Button, + ButtonGroup, HStack, IconButton, Input, @@ -275,11 +276,13 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { > @@ -351,33 +354,28 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { - + } - size="sm" - variant="ghost" onClick={() => changeFontSize(-1)} title={t("global.font_size")} /> - + } - size="sm" - variant="ghost" onClick={() => changeFontSize(1)} title={t("global.font_size")} /> - + Date: Fri, 26 Jun 2026 17:34:43 +0800 Subject: [PATCH 16/18] style: use ButtonGroup in LocalSettings Signed-off-by: MadDogOwner --- src/pages/home/toolbar/LocalSettings.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/home/toolbar/LocalSettings.tsx b/src/pages/home/toolbar/LocalSettings.tsx index 4779ce50b..84bebce37 100644 --- a/src/pages/home/toolbar/LocalSettings.tsx +++ b/src/pages/home/toolbar/LocalSettings.tsx @@ -24,6 +24,7 @@ import { VStack, Switch as HopeSwitch, IconButton, + ButtonGroup, } from "@hope-ui/solid" import { For, Match, onCleanup, Switch } from "solid-js" import { FaSolidMinus, FaSolidPlus } from "solid-icons/fa" @@ -83,7 +84,7 @@ function LocalSettingEdit(props: LocalSetting) { /> - + } @@ -100,6 +101,7 @@ function LocalSettingEdit(props: LocalSetting) { onInput={(e) => { setLocal(props.key, e.currentTarget.value) }} + borderRadius="$none" style={{ "-moz-appearance": "textfield", // @ts-ignore @@ -115,7 +117,7 @@ function LocalSettingEdit(props: LocalSetting) { setLocal(props.key, (parseInt(local[props.key]) + 1).toString()) }} /> - + From 84e0ea93dedaf58059d81cd11da61218ca0329f7 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Sat, 27 Jun 2026 16:37:31 +0800 Subject: [PATCH 17/18] style: integrate BoxWithFullScreen component and clean up fullscreen logic in TextEditor Signed-off-by: MadDogOwner --- src/pages/home/previews/text-editor.tsx | 56 +++++++++---------------- 1 file changed, 20 insertions(+), 36 deletions(-) diff --git a/src/pages/home/previews/text-editor.tsx b/src/pages/home/previews/text-editor.tsx index 9091922bb..64e04d2eb 100644 --- a/src/pages/home/previews/text-editor.tsx +++ b/src/pages/home/previews/text-editor.tsx @@ -22,7 +22,7 @@ import { Show, } from "solid-js" import { useBeforeLeave } from "@solidjs/router" -import { EncodingSelect, MaybeLoading } from "~/components" +import { BoxWithFullScreen, EncodingSelect, MaybeLoading } from "~/components" import { MonacoEditorLoader, monaco } from "~/components/MonacoEditor" import { useFetchText, useParseText, useRouter, useT, useUtil } from "~/hooks" import { objStore, setLocal, userCan } from "~/store" @@ -42,7 +42,6 @@ import { TbMapOff, } from "solid-icons/tb" import { FaSolidMinus, FaSolidPlus } from "solid-icons/fa" -import { AiOutlineFullscreen, AiOutlineFullscreenExit } from "solid-icons/ai" import type * as monacoType from "monaco-editor/esm/vs/editor/editor.api.js" interface LanguageOption { @@ -79,7 +78,6 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { local.editor_word_wrap === "true", ) const [minimap, setMinimap] = createSignal(local.editor_minimap !== "false") - const [fullscreen, setFullscreen] = createSignal(false) const [saving, setSaving] = createSignal(false) const [langSearch, setLangSearch] = createSignal("") const filteredLanguages = createMemo(() => { @@ -136,11 +134,6 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { }) } - // Escape to exit fullscreen - createShortcut(["Escape"], () => { - if (fullscreen()) setFullscreen(false) - }) - createEffect( on(encoding, (v) => { setValue(text(v)) @@ -254,14 +247,12 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { return ( {/* Toolbar */} - : - } - size="sm" - variant="ghost" - onClick={() => setFullscreen(!fullscreen())} - title="Fullscreen (Esc)" - color={fullscreen() ? "$info11" : undefined} - /> - - - - ● - - - - Ln {cursorLine()}, Col {cursorColumn()} - - {wordCount()} words - setLangSearch("")}> + + + Ln {cursorLine()}, Col {cursorColumn()} + + {wordCount()} words + + + ● + + ) @@ -513,9 +492,14 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { const TextEditor = () => { const [content] = useFetchText() return ( - - - + + + + + ) } From d964121a055b15ae97ccba92d696f77d75c3efff Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Sat, 27 Jun 2026 16:49:40 +0800 Subject: [PATCH 18/18] feat: add tooltip Signed-off-by: MadDogOwner --- src/lang/en/global.json | 4 + src/pages/home/previews/text-editor.tsx | 165 +++++++++++++----------- 2 files changed, 95 insertions(+), 74 deletions(-) diff --git a/src/lang/en/global.json b/src/lang/en/global.json index f6c5e1985..a622d2a21 100644 --- a/src/lang/en/global.json +++ b/src/lang/en/global.json @@ -5,6 +5,10 @@ "delete": "Delete", "save": "Save", "update": "Update", + "undo": "Undo", + "redo": "Redo", + "increase": "Increase", + "decrease": "Decrease", "copy": "Copy", "paste": "Paste", "copied": "Copied", diff --git a/src/pages/home/previews/text-editor.tsx b/src/pages/home/previews/text-editor.tsx index 64e04d2eb..6ec762671 100644 --- a/src/pages/home/previews/text-editor.tsx +++ b/src/pages/home/previews/text-editor.tsx @@ -9,6 +9,7 @@ import { PopoverBody, PopoverContent, PopoverTrigger, + Tooltip, useColorMode, VStack, } from "@hope-ui/solid" @@ -266,82 +267,90 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { flexShrink={0} > - + + + + + + } + size="sm" + variant="ghost" + onClick={undo} + /> + + + } + size="sm" + variant="ghost" + onClick={redo} + /> + + + + + } + aria-label={t("global.copy")} + icon={} size="sm" variant="ghost" - onClick={undo} - title={`${t("global.undo")} (Ctrl+Z)`} + onClick={copyToClipboard} /> + + + + } + size="sm" + variant="ghost" + onClick={pasteFromClipboard} + /> + + + + + + } + aria-label={t("home.local_settings.editor_word_wrap")} + icon={wordWrap() ? : } size="sm" variant="ghost" - onClick={redo} - title={`${t("global.redo")} (Ctrl+Y)`} + onClick={toggleWordWrap} + color={wordWrap() ? "$info11" : undefined} /> + - - - - } - size="sm" - variant="ghost" - onClick={copyToClipboard} - title={t("global.copy")} - /> - + } + aria-label={t("home.local_settings.editor_minimap")} + icon={minimap() ? : } size="sm" variant="ghost" - onClick={pasteFromClipboard} - title={t("global.paste")} + onClick={() => { + const next = !minimap() + setMinimap(next) + setLocal("editor_minimap", String(next)) + }} + color={minimap() ? "$info11" : undefined} /> - - - - - : } - size="sm" - variant="ghost" - onClick={toggleWordWrap} - title={t("global.wrap")} - color={wordWrap() ? "$info11" : undefined} - /> - - : } - size="sm" - variant="ghost" - onClick={() => { - const next = !minimap() - setMinimap(next) - setLocal("editor_minimap", String(next)) - }} - title="Minimap" - color={minimap() ? "$info11" : undefined} - /> + @@ -351,21 +360,29 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) { attached display={{ "@initial": "none", "@sm": "flex" }} > - } - onClick={() => changeFontSize(-1)} - title={t("global.font_size")} - /> + + } + onClick={() => changeFontSize(-1)} + /> + - } - onClick={() => changeFontSize(1)} - title={t("global.font_size")} - /> + + } + onClick={() => changeFontSize(1)} + /> +