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',
|
name: 'Format Check',
|
||||||
command: 'run 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',
|
name: 'Lint Check',
|
||||||
command: 'run 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',
|
name: 'Type Check',
|
||||||
@@ -56,6 +58,45 @@ function commandExists(command) {
|
|||||||
return !result.error && result.status === 0
|
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() {
|
function printDivider() {
|
||||||
console.log('========================================')
|
console.log('========================================')
|
||||||
}
|
}
|
||||||
@@ -73,7 +114,7 @@ printDivider()
|
|||||||
|
|
||||||
for (const check of checks) {
|
for (const check of checks) {
|
||||||
console.log(`\n[pre-commit] ${check.name}`)
|
console.log(`\n[pre-commit] ${check.name}`)
|
||||||
const result = spawnCommand(runner.run(check.command))
|
let result = spawnCommand(runner.run(check.command))
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
console.error(`\n[pre-commit] Failed to run "${check.command}".`)
|
console.error(`\n[pre-commit] Failed to run "${check.command}".`)
|
||||||
@@ -81,6 +122,34 @@ for (const check of checks) {
|
|||||||
process.exit(1)
|
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) {
|
if (result.status !== 0) {
|
||||||
console.error(`\n[pre-commit] ${check.name.toUpperCase()} FAILED`)
|
console.error(`\n[pre-commit] ${check.name.toUpperCase()} FAILED`)
|
||||||
console.error(`[pre-commit] ${check.help}`)
|
console.error(`[pre-commit] ${check.help}`)
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
|
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 { useTranslation } from 'react-i18next'
|
||||||
import { toast } from '@renderer/components/base/toast'
|
import { toast } from '@renderer/components/base/toast'
|
||||||
import { previewPlugin, installPlugin } from '@renderer/utils/ipc'
|
import { previewPlugin, installPlugin } from '@renderer/utils/ipc'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void
|
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 {
|
function abToBase64(buf: ArrayBuffer): string {
|
||||||
let binary = ''
|
let binary = ''
|
||||||
const bytes = new Uint8Array(buf)
|
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 { t } = useTranslation()
|
||||||
const fileInput = useRef<HTMLInputElement>(null)
|
const fileInput = useRef<HTMLInputElement>(null)
|
||||||
const [fileName, setFileName] = useState('')
|
const [fileName, setFileName] = useState('')
|
||||||
@@ -31,18 +34,29 @@ const PluginInstallModal: React.FC<Props> = ({ onClose }) => {
|
|||||||
const [preview, setPreview] = useState<IPluginDescriptorPreview | null>(null)
|
const [preview, setPreview] = useState<IPluginDescriptorPreview | null>(null)
|
||||||
const [busy, setBusy] = useState(false)
|
const [busy, setBusy] = useState(false)
|
||||||
|
|
||||||
const onPickFile = async (e: React.ChangeEvent<HTMLInputElement>): Promise<void> => {
|
// file -> base64, or null if rejected/unreadable (toasts here)
|
||||||
const f = e.target.files?.[0]
|
const loadFile = async (f: File): Promise<string | null> => {
|
||||||
if (!f) return
|
if (f.size > MAX_CPX_BYTES) {
|
||||||
setFileName(f.name)
|
toast.error(t('plugins.fileTooLarge'))
|
||||||
setFileB64(abToBase64(await f.arrayBuffer()))
|
return null
|
||||||
setPreview(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)
|
setBusy(true)
|
||||||
try {
|
try {
|
||||||
setPreview(await previewPlugin(fileB64))
|
setPreview(await previewPlugin(b64))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e instanceof Error ? e.message : ''
|
const msg = e instanceof Error ? e.message : ''
|
||||||
toast.error(msg.includes('v1') ? t('plugins.outdatedFile') : t('plugins.previewFailed'))
|
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> => {
|
const doInstall = async (): Promise<void> => {
|
||||||
setBusy(true)
|
setBusy(true)
|
||||||
try {
|
try {
|
||||||
@@ -78,7 +110,7 @@ const PluginInstallModal: React.FC<Props> = ({ onClose }) => {
|
|||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={onPickFile}
|
onChange={onPickFile}
|
||||||
/>
|
/>
|
||||||
<Button variant="flat" onPress={() => fileInput.current?.click()}>
|
<Button variant="flat" isDisabled={busy} onPress={() => fileInput.current?.click()}>
|
||||||
{fileName || t('plugins.chooseFile')}
|
{fileName || t('plugins.chooseFile')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -836,6 +836,7 @@
|
|||||||
"installed": "Installed",
|
"installed": "Installed",
|
||||||
"installFailed": "Install failed",
|
"installFailed": "Install failed",
|
||||||
"previewFailed": "Invalid plugin file",
|
"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": "نصب شد",
|
"installed": "نصب شد",
|
||||||
"installFailed": "نصب ناموفق بود",
|
"installFailed": "نصب ناموفق بود",
|
||||||
"previewFailed": "فایل افزونه نامعتبر است",
|
"previewFailed": "فایل افزونه نامعتبر است",
|
||||||
"outdatedFile": "قالب فایل افزونه قدیمی است؛ لطفاً نسخه جدید را از سرویسدهنده دریافت کنید"
|
"outdatedFile": "قالب فایل افزونه قدیمی است؛ لطفاً نسخه جدید را از سرویسدهنده دریافت کنید",
|
||||||
|
"fileTooLarge": "فایل افزونه بیش از حد بزرگ است"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -808,6 +808,7 @@
|
|||||||
"installed": "Установлено",
|
"installed": "Установлено",
|
||||||
"installFailed": "Ошибка установки",
|
"installFailed": "Ошибка установки",
|
||||||
"previewFailed": "Недействительный файл плагина",
|
"previewFailed": "Недействительный файл плагина",
|
||||||
"outdatedFile": "Формат файла плагина устарел; получите новый у провайдера"
|
"outdatedFile": "Формат файла плагина устарел; получите новый у провайдера",
|
||||||
|
"fileTooLarge": "Файл плагина слишком большой"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -832,6 +832,7 @@
|
|||||||
"installed": "已安装",
|
"installed": "已安装",
|
||||||
"installFailed": "安装失败",
|
"installFailed": "安装失败",
|
||||||
"previewFailed": "插件文件无效",
|
"previewFailed": "插件文件无效",
|
||||||
"outdatedFile": "该插件文件格式已过期,请从机场重新获取"
|
"outdatedFile": "该插件文件格式已过期,请从机场重新获取",
|
||||||
|
"fileTooLarge": "插件文件过大"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -832,6 +832,7 @@
|
|||||||
"installed": "已安裝",
|
"installed": "已安裝",
|
||||||
"installFailed": "安裝失敗",
|
"installFailed": "安裝失敗",
|
||||||
"previewFailed": "外掛檔案無效",
|
"previewFailed": "外掛檔案無效",
|
||||||
"outdatedFile": "該外掛檔案格式已過期,請從機場重新取得"
|
"outdatedFile": "該外掛檔案格式已過期,請從機場重新取得",
|
||||||
|
"fileTooLarge": "外掛檔案過大"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ const Profiles: React.FC = () => {
|
|||||||
const [, setNow] = useState(new Date())
|
const [, setNow] = useState(new Date())
|
||||||
const { pluginConfig, mutatePluginConfig } = usePluginConfig()
|
const { pluginConfig, mutatePluginConfig } = usePluginConfig()
|
||||||
const [showPluginImport, setShowPluginImport] = useState(false)
|
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 isUrlEmpty = url.trim() === ''
|
||||||
const sensors = useSensors(useSensor(PointerSensor))
|
const sensors = useSensors(useSensor(PointerSensor))
|
||||||
const { data: subs = [], mutate: mutateSubs } = useSWR(
|
const { data: subs = [], mutate: mutateSubs } = useSWR(
|
||||||
@@ -218,7 +221,8 @@ const Profiles: React.FC = () => {
|
|||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
if (event.dataTransfer?.files) {
|
if (event.dataTransfer?.files) {
|
||||||
const file = event.dataTransfer.files[0]
|
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 {
|
try {
|
||||||
const path = window.api.webUtils.getPathForFile(file)
|
const path = window.api.webUtils.getPathForFile(file)
|
||||||
const content = await readTextFile(path)
|
const content = await readTextFile(path)
|
||||||
@@ -226,7 +230,12 @@ const Profiles: React.FC = () => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(String(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'))
|
toast.warning(tRef.current('profiles.error.unsupportedFileType'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -535,8 +544,11 @@ const Profiles: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
{showPluginImport && (
|
{showPluginImport && (
|
||||||
<PluginInstallModal
|
<PluginInstallModal
|
||||||
|
key={pluginDropSeq}
|
||||||
|
initialFile={pluginDropFile ?? undefined}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowPluginImport(false)
|
setShowPluginImport(false)
|
||||||
|
setPluginDropFile(null)
|
||||||
mutatePluginConfig()
|
mutatePluginConfig()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user