Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
15 changes: 0 additions & 15 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

160 changes: 102 additions & 58 deletions src/components/EncodingSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})

Expand Down
45 changes: 34 additions & 11 deletions src/components/MonacoEditor.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -9,11 +9,12 @@ 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
}
let monaco: typeof monacoType
export let monaco: typeof monacoType

export const MonacoEditorLoader = (props: MonacoEditorProps) => {
const { monacoPath } = useCDN()
Expand All @@ -40,11 +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),
})
automaticLayout: true,
}
monacoEditor = monaco.editor.create(monacoEditorDiv!, constructionOptions)
model = monaco.editor.createModel(
props.value,
props.language,
Expand All @@ -54,24 +57,44 @@ export const MonacoEditor = (props: MonacoEditorProps) => {
monacoEditor.onDidChangeModelContent(() => {
props.onChange?.(monacoEditor.getValue())
})
props.onEditorReady?.(monacoEditor)
})
createEffect(() => {
monacoEditor.setValue(props.value)
})
createEffect(
on(
() => props.value,
(value) => {
monacoEditor.setValue(value)
},
{ defer: true },
),
)

createEffect(() => {
monaco.editor.setTheme(props.theme)
monaco.editor.setTheme(props.options?.theme ?? "vs")
})

createEffect(
on(
() => props.language,
(lang) => {
if (lang && model) {
monaco.editor.setModelLanguage(model, lang)
}
},
{ defer: true },
),
)

createEffect(() => {
monacoEditor?.updateOptions({
fontSize: parseInt(local.editor_font_size),
...props.options,
})
})

onCleanup(() => {
model && model.dispose()
monacoEditor && monacoEditor.dispose()
})
return <Box w="$full" h="70vh" ref={monacoEditorDiv!} />
return <Box w="$full" flex={1} minH="60vh" ref={monacoEditorDiv!} />
}
46 changes: 43 additions & 3 deletions src/hooks/useUtil.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,58 @@
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<boolean> {
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<string> => {
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 ""
}
},
isHide: (obj: Obj) => {
const hideFiles = getHideFiles()
for (const reg of hideFiles) {
Expand Down
Loading