mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/mihomo-party-org/clash-party.git
synced 2026-09-20 08:03:39 +08:00
feat: plugin file drag & drop
This commit is contained in:
@@ -22,12 +22,14 @@ const checks = [
|
||||
{
|
||||
name: 'Format Check',
|
||||
command: 'run format:check',
|
||||
help: 'Formatting issues were reported above. Review the listed files and fix them before committing again.'
|
||||
fixCommand: 'run format',
|
||||
help: 'Formatting issues were reported above and could not be auto-fixed. Review the listed files and fix them before committing again.'
|
||||
},
|
||||
{
|
||||
name: 'Lint Check',
|
||||
command: 'run lint:check',
|
||||
help: 'Lint errors were reported above. Review them and fix the affected code before committing again.'
|
||||
fixCommand: 'run lint',
|
||||
help: 'Lint errors were reported above and could not be auto-fixed. Review them and fix the affected code before committing again.'
|
||||
},
|
||||
{
|
||||
name: 'Type Check',
|
||||
@@ -56,6 +58,45 @@ function commandExists(command) {
|
||||
return !result.error && result.status === 0
|
||||
}
|
||||
|
||||
function gitCommand(args, { stdio = 'inherit' } = {}) {
|
||||
return spawnSync('git', args, {
|
||||
cwd: process.cwd(),
|
||||
stdio
|
||||
})
|
||||
}
|
||||
|
||||
function getStagedFiles() {
|
||||
const result = gitCommand(['diff', '--name-only', '--cached', '--diff-filter=ACMR', '-z'], {
|
||||
stdio: 'pipe'
|
||||
})
|
||||
|
||||
if (result.error || result.status !== 0) {
|
||||
console.error('[pre-commit] Failed to read staged files for auto-fix restaging.')
|
||||
if (result.error) {
|
||||
console.error(result.error.message)
|
||||
}
|
||||
process.exit(result.status ?? 1)
|
||||
}
|
||||
|
||||
return result.stdout.toString('utf8').split('\0').filter(Boolean)
|
||||
}
|
||||
|
||||
function restageFiles(files) {
|
||||
if (files.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const result = gitCommand(['add', '--', ...files])
|
||||
|
||||
if (result.error || result.status !== 0) {
|
||||
console.error('[pre-commit] Auto-fix completed, but failed to restage fixed files.')
|
||||
if (result.error) {
|
||||
console.error(result.error.message)
|
||||
}
|
||||
process.exit(result.status ?? 1)
|
||||
}
|
||||
}
|
||||
|
||||
function printDivider() {
|
||||
console.log('========================================')
|
||||
}
|
||||
@@ -73,7 +114,7 @@ printDivider()
|
||||
|
||||
for (const check of checks) {
|
||||
console.log(`\n[pre-commit] ${check.name}`)
|
||||
const result = spawnCommand(runner.run(check.command))
|
||||
let result = spawnCommand(runner.run(check.command))
|
||||
|
||||
if (result.error) {
|
||||
console.error(`\n[pre-commit] Failed to run "${check.command}".`)
|
||||
@@ -81,6 +122,34 @@ for (const check of checks) {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (result.status !== 0 && check.fixCommand) {
|
||||
console.log(`\n[pre-commit] ${check.name} failed. Running auto-fix...`)
|
||||
const fixResult = spawnCommand(runner.run(check.fixCommand))
|
||||
|
||||
if (fixResult.error) {
|
||||
console.error(`\n[pre-commit] Failed to run "${check.fixCommand}".`)
|
||||
console.error(fixResult.error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (fixResult.status !== 0) {
|
||||
console.error(`\n[pre-commit] Auto-fix command "${check.fixCommand}" failed.`)
|
||||
console.error(`[pre-commit] ${check.help}`)
|
||||
console.error('[pre-commit] Commit aborted.')
|
||||
process.exit(fixResult.status ?? 1)
|
||||
}
|
||||
|
||||
restageFiles(getStagedFiles())
|
||||
console.log(`[pre-commit] Auto-fix completed. Re-running ${check.name}.`)
|
||||
result = spawnCommand(runner.run(check.command))
|
||||
|
||||
if (result.error) {
|
||||
console.error(`\n[pre-commit] Failed to run "${check.command}".`)
|
||||
console.error(result.error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
console.error(`\n[pre-commit] ${check.name.toUpperCase()} FAILED`)
|
||||
console.error(`[pre-commit] ${check.help}`)
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from '@renderer/components/base/toast'
|
||||
import { previewPlugin, installPlugin } from '@renderer/utils/ipc'
|
||||
|
||||
interface Props {
|
||||
onClose: () => void
|
||||
initialFile?: File // dropped file: auto-load + preview on open
|
||||
}
|
||||
|
||||
const MAX_CPX_BYTES = 10 * 1024 * 1024 // guard against a huge mis-dropped file freezing the renderer
|
||||
|
||||
function abToBase64(buf: ArrayBuffer): string {
|
||||
let binary = ''
|
||||
const bytes = new Uint8Array(buf)
|
||||
@@ -23,7 +26,7 @@ function hostOf(url: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
const PluginInstallModal: React.FC<Props> = ({ onClose }) => {
|
||||
const PluginInstallModal: React.FC<Props> = ({ onClose, initialFile }) => {
|
||||
const { t } = useTranslation()
|
||||
const fileInput = useRef<HTMLInputElement>(null)
|
||||
const [fileName, setFileName] = useState('')
|
||||
@@ -31,18 +34,29 @@ const PluginInstallModal: React.FC<Props> = ({ onClose }) => {
|
||||
const [preview, setPreview] = useState<IPluginDescriptorPreview | null>(null)
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
const onPickFile = async (e: React.ChangeEvent<HTMLInputElement>): Promise<void> => {
|
||||
const f = e.target.files?.[0]
|
||||
if (!f) return
|
||||
setFileName(f.name)
|
||||
setFileB64(abToBase64(await f.arrayBuffer()))
|
||||
setPreview(null)
|
||||
// file -> base64, or null if rejected/unreadable (toasts here)
|
||||
const loadFile = async (f: File): Promise<string | null> => {
|
||||
if (f.size > MAX_CPX_BYTES) {
|
||||
toast.error(t('plugins.fileTooLarge'))
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const b64 = abToBase64(await f.arrayBuffer())
|
||||
setFileName(f.name)
|
||||
setFileB64(b64)
|
||||
setPreview(null)
|
||||
return b64
|
||||
} catch {
|
||||
toast.error(t('plugins.previewFailed'))
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const doPreview = async (): Promise<void> => {
|
||||
// preview by explicit b64 (state may not be flushed yet)
|
||||
const previewB64 = async (b64: string): Promise<void> => {
|
||||
setBusy(true)
|
||||
try {
|
||||
setPreview(await previewPlugin(fileB64))
|
||||
setPreview(await previewPlugin(b64))
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : ''
|
||||
toast.error(msg.includes('v1') ? t('plugins.outdatedFile') : t('plugins.previewFailed'))
|
||||
@@ -51,6 +65,24 @@ const PluginInstallModal: React.FC<Props> = ({ onClose }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const onPickFile = async (e: React.ChangeEvent<HTMLInputElement>): Promise<void> => {
|
||||
const f = e.target.files?.[0]
|
||||
if (!f) return
|
||||
await loadFile(f)
|
||||
}
|
||||
|
||||
const doPreview = async (): Promise<void> => {
|
||||
await previewB64(fileB64)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialFile) return
|
||||
loadFile(initialFile).then((b64) => {
|
||||
if (b64) previewB64(b64)
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const doInstall = async (): Promise<void> => {
|
||||
setBusy(true)
|
||||
try {
|
||||
@@ -78,7 +110,7 @@ const PluginInstallModal: React.FC<Props> = ({ onClose }) => {
|
||||
className="hidden"
|
||||
onChange={onPickFile}
|
||||
/>
|
||||
<Button variant="flat" onPress={() => fileInput.current?.click()}>
|
||||
<Button variant="flat" isDisabled={busy} onPress={() => fileInput.current?.click()}>
|
||||
{fileName || t('plugins.chooseFile')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -836,6 +836,7 @@
|
||||
"installed": "Installed",
|
||||
"installFailed": "Install failed",
|
||||
"previewFailed": "Invalid plugin file",
|
||||
"outdatedFile": "This plugin file format is outdated; please obtain the new one from your provider"
|
||||
"outdatedFile": "This plugin file format is outdated; please obtain the new one from your provider",
|
||||
"fileTooLarge": "Plugin file is too large"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -800,6 +800,7 @@
|
||||
"installed": "نصب شد",
|
||||
"installFailed": "نصب ناموفق بود",
|
||||
"previewFailed": "فایل افزونه نامعتبر است",
|
||||
"outdatedFile": "قالب فایل افزونه قدیمی است؛ لطفاً نسخه جدید را از سرویسدهنده دریافت کنید"
|
||||
"outdatedFile": "قالب فایل افزونه قدیمی است؛ لطفاً نسخه جدید را از سرویسدهنده دریافت کنید",
|
||||
"fileTooLarge": "فایل افزونه بیش از حد بزرگ است"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -808,6 +808,7 @@
|
||||
"installed": "Установлено",
|
||||
"installFailed": "Ошибка установки",
|
||||
"previewFailed": "Недействительный файл плагина",
|
||||
"outdatedFile": "Формат файла плагина устарел; получите новый у провайдера"
|
||||
"outdatedFile": "Формат файла плагина устарел; получите новый у провайдера",
|
||||
"fileTooLarge": "Файл плагина слишком большой"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -832,6 +832,7 @@
|
||||
"installed": "已安装",
|
||||
"installFailed": "安装失败",
|
||||
"previewFailed": "插件文件无效",
|
||||
"outdatedFile": "该插件文件格式已过期,请从机场重新获取"
|
||||
"outdatedFile": "该插件文件格式已过期,请从机场重新获取",
|
||||
"fileTooLarge": "插件文件过大"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -832,6 +832,7 @@
|
||||
"installed": "已安裝",
|
||||
"installFailed": "安裝失敗",
|
||||
"previewFailed": "外掛檔案無效",
|
||||
"outdatedFile": "該外掛檔案格式已過期,請從機場重新取得"
|
||||
"outdatedFile": "該外掛檔案格式已過期,請從機場重新取得",
|
||||
"fileTooLarge": "外掛檔案過大"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,9 @@ const Profiles: React.FC = () => {
|
||||
const [, setNow] = useState(new Date())
|
||||
const { pluginConfig, mutatePluginConfig } = usePluginConfig()
|
||||
const [showPluginImport, setShowPluginImport] = useState(false)
|
||||
const [pluginDropFile, setPluginDropFile] = useState<File | null>(null)
|
||||
// bump per .cpx drop -> remount modal so it loads the new file even when open
|
||||
const [pluginDropSeq, setPluginDropSeq] = useState(0)
|
||||
const isUrlEmpty = url.trim() === ''
|
||||
const sensors = useSensors(useSensor(PointerSensor))
|
||||
const { data: subs = [], mutate: mutateSubs } = useSWR(
|
||||
@@ -218,7 +221,8 @@ const Profiles: React.FC = () => {
|
||||
event.stopPropagation()
|
||||
if (event.dataTransfer?.files) {
|
||||
const file = event.dataTransfer.files[0]
|
||||
if (file.name.endsWith('.yml') || file.name.endsWith('.yaml')) {
|
||||
const name = file?.name.toLowerCase() ?? ''
|
||||
if (name.endsWith('.yml') || name.endsWith('.yaml')) {
|
||||
try {
|
||||
const path = window.api.webUtils.getPathForFile(file)
|
||||
const content = await readTextFile(path)
|
||||
@@ -226,7 +230,12 @@ const Profiles: React.FC = () => {
|
||||
} catch (e) {
|
||||
toast.error(String(e))
|
||||
}
|
||||
} else {
|
||||
} else if (name.endsWith('.cpx')) {
|
||||
// .cpx -> plugin install modal (preview + confirm)
|
||||
setPluginDropFile(file)
|
||||
setPluginDropSeq((n) => n + 1)
|
||||
setShowPluginImport(true)
|
||||
} else if (file) {
|
||||
toast.warning(tRef.current('profiles.error.unsupportedFileType'))
|
||||
}
|
||||
}
|
||||
@@ -535,8 +544,11 @@ const Profiles: React.FC = () => {
|
||||
)}
|
||||
{showPluginImport && (
|
||||
<PluginInstallModal
|
||||
key={pluginDropSeq}
|
||||
initialFile={pluginDropFile ?? undefined}
|
||||
onClose={() => {
|
||||
setShowPluginImport(false)
|
||||
setPluginDropFile(null)
|
||||
mutatePluginConfig()
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user