mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-07-12 16:11:27 +08:00
430
src/renderer/pages/ScriptPage.vue
Normal file
430
src/renderer/pages/ScriptPage.vue
Normal file
@@ -0,0 +1,430 @@
|
||||
<template>
|
||||
<div class="relative flex h-full w-full items-center justify-center">
|
||||
<div class="relative z-1 flex h-full w-full flex-col items-center justify-start gap-4 rounded-xl border-none p-4">
|
||||
<div
|
||||
class="flex w-full items-center justify-between gap-4 overflow-visible rounded-2xl border border-border-secondary px-6 py-2 shadow-md max-md:items-stretch max-md:p-5"
|
||||
>
|
||||
<div class="flex flex-1 flex-wrap items-center gap-4 p-1">
|
||||
<FileCode :size="24" class="text-accent" />
|
||||
<div>
|
||||
<h1 class="m-0 text-2xl font-semibold tracking-tight text-main">{{ t('pages.scripts.title') }}</h1>
|
||||
<p class="m-0 text-sm text-secondary">{{ t('pages.scripts.description') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3 overflow-visible">
|
||||
<div class="flex max-w-[220px] min-w-[180px] flex-1 flex-col gap-1">
|
||||
<MultiSelect
|
||||
v-model:choosed="choosedCat"
|
||||
:zero-placeholder="t('pages.scripts.chooseScriptType')"
|
||||
:all-list="supportedScriptCategories"
|
||||
/>
|
||||
</div>
|
||||
<CustomButton
|
||||
type="primary"
|
||||
:icon="FolderOpen"
|
||||
:text="t('pages.scripts.openScriptFolder')"
|
||||
@click="handleOpenScriptFolder"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plugin Grid -->
|
||||
<div
|
||||
class="relative flex h-full w-full flex-1 items-center justify-center overflow-hidden rounded-2xl border border-border-secondary p-4 shadow-md"
|
||||
>
|
||||
<div class="no-scrollbar h-full w-full overflow-auto rounded-sm">
|
||||
<div class="grid w-full grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-5 border-none p-1 max-md:gap-4">
|
||||
<div
|
||||
v-for="(item, index) in scriptsList"
|
||||
:key="item.fileName + index"
|
||||
class="group/config-card relative flex min-h-[160px] cursor-pointer flex-col gap-6 overflow-hidden rounded-xl border border-border-secondary p-5 shadow-sm transition-all duration-fast ease-apple hover:border-2 hover:border-accent hover:shadow-md [.disabled]:opacity-80"
|
||||
:class="{
|
||||
disabled:
|
||||
!item.enabled && item.category !== 'manualTrigger' && item.category !== 'uploader.advancedplist',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="absolute right-1 bottom-0 flex h-[15px] w-auto items-center rounded-md bg-accent/70 px-2 py-1 text-xs font-semibold text-white"
|
||||
>
|
||||
{{ supportedScriptCategories.find(cat => cat.type === item.category)?.name || item.category }}
|
||||
</div>
|
||||
<div class="relative z-1 flex flex-1 items-start justify-between">
|
||||
<div
|
||||
class="peer flex h-[40px] w-[40px] items-center justify-center rounded-lg border border-border-secondary text-accent transition-all duration-fast ease-apple group-hover/config-card:scale-105 [.is-active]:border-none [.is-active]:bg-accent [.is-active]:text-white"
|
||||
:class="{ 'is-active': item.enabled }"
|
||||
>
|
||||
<FileCode :size="20" />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-1.5 transition-all duration-fast ease-apple">
|
||||
<button
|
||||
class="action-btn"
|
||||
:title="t('pages.scripts.editScript')"
|
||||
@click.stop="openEditPage(item.filePath)"
|
||||
>
|
||||
<Pencil :size="14" />
|
||||
</button>
|
||||
<button
|
||||
class="action-btn danger"
|
||||
:title="t('pages.scripts.deleteScript')"
|
||||
@click.stop="() => deleteConfig(item.filePath)"
|
||||
>
|
||||
<Trash2 :size="14" />
|
||||
</button>
|
||||
<button
|
||||
v-if="item.category === 'manualTrigger'"
|
||||
class="action-btn bg-accent/50 text-white!"
|
||||
:title="t('pages.scripts.runScript')"
|
||||
@click.stop="runScript(item.filePath)"
|
||||
>
|
||||
<Play :size="14" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="item.category !== 'manualTrigger' && item.category !== 'uploader.advancedplist'"
|
||||
class="action-btn"
|
||||
:title="item.enabled ? t('pages.scripts.disableScript') : t('pages.scripts.enableScript')"
|
||||
@click.stop="toggleScript(item.filePath)"
|
||||
>
|
||||
<template v-if="!item.enabled">
|
||||
<CheckCircle2 :size="14" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<XIcon :size="14" />
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative z-1 flex-1">
|
||||
<div class="mx-0 mt-0 mb-2 flex items-center text-base font-semibold tracking-tight text-main">
|
||||
{{ item.fileName }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3 flex items-center gap-1.5 text-xs text-tertiary">
|
||||
<div class="flex items-center gap-1">
|
||||
<Clock :size="12" />
|
||||
<span>{{ formatDate(item.mtimeMs) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="item.enabled"
|
||||
class="inline-flex items-center gap-1.5 rounded-2xl bg-accent/40 px-3 py-1.5 text-xs font-medium text-white transition-all duration-fast ease-standard"
|
||||
>
|
||||
<CheckCircle2 :size="15" />
|
||||
<span>{{ t('pages.scripts.enabled') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="inline-flex items-center gap-1.5 rounded-2xl px-3 py-1.5 text-xs font-medium text-tertiary transition-all duration-fast ease-standard group-hover/config-card:bg-accent/10"
|
||||
>
|
||||
<span>{{ t('pages.scripts.disabled') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
key="add-new"
|
||||
class="group/new relative flex min-h-[180px] cursor-pointer flex-col items-center justify-center gap-6 overflow-hidden rounded-xl border-2 border-dashed border-border p-5 shadow-sm transition-all duration-fast ease-apple hover:border-solid hover:border-accent hover:bg-surface hover:shadow-md"
|
||||
@click="openNewScriptsNameDialog"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-3 transition-all duration-fast ease-apple">
|
||||
<div
|
||||
class="flex h-[56px] w-[56px] items-center justify-center rounded-xl border-2 border-dashed border-border text-tertiary transition-all duration-fast ease-apple group-hover/new:scale-105 group-hover/new:border-solid group-hover/new:border-accent group-hover/new:bg-accent/5 group-hover/new:text-accent"
|
||||
>
|
||||
<Plus :size="24" />
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<span class="text-base font-semibold text-secondary">{{ t('pages.scripts.addNew') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CustomModal v-if="editorVisible" v-model:visible="editorVisible" :title="t('common.edit')">
|
||||
<Editor v-model="editorContent" language="javascript" />
|
||||
<template #footer>
|
||||
<CustomButton type="secondary" :text="t('common.cancel')" @click="editorVisible = false" />
|
||||
<CustomButton type="primary" :text="t('common.save')" @click="saveEditorContent" />
|
||||
</template>
|
||||
</CustomModal>
|
||||
|
||||
<CustomModal
|
||||
v-if="newScriptNameVisible"
|
||||
v-model:visible="newScriptNameVisible"
|
||||
:title="t('pages.scripts.addNew')"
|
||||
height="auto"
|
||||
width="600px"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center gap-4 bg-bg-secondary p-6">
|
||||
<SettingCard class="w-full">
|
||||
<SingleSelect
|
||||
v-model="newScriptCategory"
|
||||
:title="t('pages.scripts.selectScriptType')"
|
||||
:key-list="supportedScriptCategories.map(cat => cat.type)"
|
||||
:fronticon="false"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
{{
|
||||
supportedScriptCategories.find(cat => cat.type === item)
|
||||
? supportedScriptCategories.find(cat => cat.type === item)?.name
|
||||
: item
|
||||
}}
|
||||
</template>
|
||||
</SingleSelect>
|
||||
</SettingCard>
|
||||
<SettingCard class="w-full">
|
||||
<CustomInput
|
||||
v-model="newScriptName"
|
||||
:title="t('pages.scripts.pleaseEnterScriptName')"
|
||||
placeholder="test.js"
|
||||
/>
|
||||
</SettingCard>
|
||||
</div>
|
||||
<template #footer>
|
||||
<CustomButton type="secondary" :text="t('common.cancel')" @click="newScriptNameVisible = false" />
|
||||
<CustomButton type="primary" :text="t('common.confirm')" @click="handleNewScriptNameConfirm" />
|
||||
</template>
|
||||
</CustomModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import dayjs from 'dayjs'
|
||||
import { CheckCircle2, Clock, FileCode, FolderOpen, Pencil, Play, Plus, Trash2, XIcon } from 'lucide-vue-next'
|
||||
import { computed, onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import CustomButton from '@/components/common/CustomButton.vue'
|
||||
import CustomInput from '@/components/common/CustomInput.vue'
|
||||
import CustomModal from '@/components/common/CustomModal.vue'
|
||||
import MultiSelect from '@/components/common/MultiSelect.vue'
|
||||
import SettingCard from '@/components/common/SettingCard.vue'
|
||||
import SingleSelect from '@/components/common/SingleSelect.vue'
|
||||
import Editor from '@/components/Editor.vue'
|
||||
import useConfirm from '@/hooks/useConfirm'
|
||||
import useMessage from '@/hooks/useMessage'
|
||||
import { getRawData } from '@/utils/common'
|
||||
import { configPaths } from '@/utils/configPaths'
|
||||
import { getConfig, saveConfig } from '@/utils/dataSender'
|
||||
import { II18nLanguage, IRPCActionType } from '@/utils/enum'
|
||||
import { defaultScriptTemplate, defaultScriptTemplateEn } from '@/utils/static'
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
const { confirm } = useConfirm()
|
||||
const scriptsMap = ref<Record<string, any>>({})
|
||||
const choosedCat = ref<string[]>([])
|
||||
const scriptsList = ref<IStringKeyMap[]>([])
|
||||
const editorVisible = ref(false)
|
||||
const editorContent = ref('')
|
||||
const editingScriptName = ref<string[]>([])
|
||||
const newScriptNameVisible = ref(false)
|
||||
const newScriptName = ref('')
|
||||
const newScriptCategory = ref('manualTrigger')
|
||||
|
||||
const supportedScriptCategories = [
|
||||
{ type: 'onSoftwareOpen', name: t('pages.scripts.scriptsTypes.onSoftwareOpen') },
|
||||
{ type: 'onSoftwareClose', name: t('pages.scripts.scriptsTypes.onSoftwareClose') },
|
||||
{ type: 'preProcess', name: t('pages.scripts.scriptsTypes.preProcess') },
|
||||
{ type: 'beforeTransform', name: t('pages.scripts.scriptsTypes.beforeTransform') },
|
||||
{ type: 'transform', name: t('pages.scripts.scriptsTypes.transform') },
|
||||
{ type: 'beforeUpload', name: t('pages.scripts.scriptsTypes.beforeUpload') },
|
||||
{ type: 'upload', name: t('pages.scripts.scriptsTypes.upload') },
|
||||
{ type: 'afterUpload', name: t('pages.scripts.scriptsTypes.afterUpload') },
|
||||
{ type: 'onUploadSuccess', name: t('pages.scripts.scriptsTypes.onUploadSuccess') },
|
||||
{ type: 'onGalleryRemove', name: t('pages.scripts.scriptsTypes.onGalleryRemove') },
|
||||
{ type: 'manualTrigger', name: t('pages.scripts.scriptsTypes.manualTrigger') },
|
||||
{ type: 'uploader.advancedplist', name: t('pages.scripts.scriptsTypes.uploader.advancedplist') },
|
||||
]
|
||||
|
||||
const existingPathsSet = computed(() => {
|
||||
return new Set(scriptsList.value.map(item => item.filePath.join('/')))
|
||||
})
|
||||
|
||||
watch(scriptsMap, async () => {
|
||||
await refreshList()
|
||||
})
|
||||
|
||||
watch(choosedCat, async () => {
|
||||
await refreshList()
|
||||
})
|
||||
|
||||
async function refreshList() {
|
||||
const result: string[][] = []
|
||||
const keysToCheck = choosedCat.value.length > 0 ? choosedCat.value : supportedScriptCategories.map(cat => cat.type)
|
||||
for (const key of keysToCheck) {
|
||||
if (key.includes('.')) {
|
||||
const parts = key.split('.')
|
||||
const value = scriptsMap.value[parts[0]] ? scriptsMap.value[parts[0]][parts[1]] : undefined
|
||||
if (value) {
|
||||
Object.entries(value).forEach(([valueKey, item]: [string, any]) => {
|
||||
if (item === null) {
|
||||
result.push([parts[0], parts[1], valueKey])
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const value = scriptsMap.value[key]
|
||||
if (value) {
|
||||
Object.entries(value).forEach(([valueKey, item]: [string, any]) => {
|
||||
if (item === null) {
|
||||
result.push([key, valueKey])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
const fileStats =
|
||||
(await window.electron.triggerRPC<IObj[]>(IRPCActionType.GET_FILES_STAT, getRawData(result), 'scripts')) || []
|
||||
const disabledList = ((await getConfig(configPaths.scripts.disabledList)) as string[] | undefined) || []
|
||||
console.log('disabledList', disabledList)
|
||||
fileStats.forEach(file => {
|
||||
const fullPath = file.filePath.join('/')
|
||||
file.enabled = !disabledList.includes(fullPath)
|
||||
})
|
||||
scriptsList.value = fileStats
|
||||
}
|
||||
|
||||
async function getScriptsMap() {
|
||||
scriptsMap.value =
|
||||
(await window.electron.triggerRPC<Record<string, any>>(IRPCActionType.LIST_SCRIPTS_FILES, [])) || {}
|
||||
}
|
||||
|
||||
function formatDate(timestamp: number) {
|
||||
const date = dayjs(timestamp)
|
||||
return date.format('YYYY/MM/DD HH:mm:ss')
|
||||
}
|
||||
|
||||
async function getTemplate() {
|
||||
const lang = (await getConfig(configPaths.settings.language)) || II18nLanguage.ZH_CN
|
||||
if (lang === II18nLanguage.ZH_CN || lang === II18nLanguage.ZH_TW) {
|
||||
return defaultScriptTemplate
|
||||
} else {
|
||||
return defaultScriptTemplateEn
|
||||
}
|
||||
}
|
||||
|
||||
async function openEditPage(filePath: string[], mode: 'edit' | 'new' = 'edit') {
|
||||
editingScriptName.value = filePath
|
||||
if (mode === 'edit') {
|
||||
const content =
|
||||
(await window.electron.triggerRPC<string>(IRPCActionType.READ_SCRIPTS_FILE, getRawData(filePath))) || ''
|
||||
editorContent.value = content
|
||||
} else {
|
||||
editorContent.value = await getTemplate()
|
||||
}
|
||||
editorVisible.value = true
|
||||
}
|
||||
|
||||
async function saveEditorContent() {
|
||||
const content = editorContent.value.trim()
|
||||
try {
|
||||
window.electron.sendRPC(IRPCActionType.WRITE_SCRIPT_FILE, getRawData(editingScriptName.value), content)
|
||||
message.success(t('pages.settings.advanced.saveFileSuccess'))
|
||||
await getScriptsMap()
|
||||
} catch (error) {
|
||||
console.error('Failed to save file:', error)
|
||||
message.error(t('pages.settings.advanced.saveFileFailed'))
|
||||
}
|
||||
editorVisible.value = false
|
||||
}
|
||||
|
||||
async function deleteConfig(scriptPath: string[]) {
|
||||
const result = await confirm({
|
||||
title: t('pages.scripts.deleteScriptTitle'),
|
||||
message: t('pages.scripts.deleteScriptConfirm', { name: scriptPath[scriptPath.length - 1] }),
|
||||
type: 'warning',
|
||||
confirmButtonText: t('common.confirm'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
center: true,
|
||||
})
|
||||
if (!result) return
|
||||
try {
|
||||
window.electron.sendRPC(IRPCActionType.DELETE_SCRIPTS_FILE, getRawData(scriptPath))
|
||||
message.success(t('pages.scripts.deleteSuccess'))
|
||||
await getScriptsMap()
|
||||
} catch (error) {
|
||||
console.error('Failed to delete script file:', error)
|
||||
message.error(t('pages.scripts.deleteFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenScriptFolder() {
|
||||
window.electron.sendRPC(IRPCActionType.PICLIST_OPEN_DIRECTORY, 'scripts', true)
|
||||
}
|
||||
|
||||
function openNewScriptsNameDialog() {
|
||||
newScriptName.value = ''
|
||||
newScriptNameVisible.value = true
|
||||
}
|
||||
|
||||
async function runScript(scriptPath: string[]) {
|
||||
const result = await window.electron.triggerRPC(IRPCActionType.RUN_SCRIPT_FILE, getRawData(scriptPath))
|
||||
if (result instanceof Error) {
|
||||
const errorMessage = result.message || 'Unknown error'
|
||||
message.error(`${t('pages.scripts.runScriptFailed', { errorMessage })}`)
|
||||
} else {
|
||||
message.success(t('pages.scripts.runScriptSuccess'))
|
||||
}
|
||||
}
|
||||
|
||||
function checkDup(fullPath: string[]) {
|
||||
return existingPathsSet.value.has(fullPath.join('/'))
|
||||
}
|
||||
|
||||
function handleNewScriptNameConfirm() {
|
||||
let trimmedName = newScriptName.value.trim()
|
||||
trimmedName = trimmedName.endsWith('.js') ? trimmedName : `${trimmedName}.js`
|
||||
if (!trimmedName) {
|
||||
message.error(t('pages.scripts.pleaseEnterScriptName'))
|
||||
return
|
||||
}
|
||||
const scriptPath = newScriptCategory.value.includes('.')
|
||||
? [...newScriptCategory.value.split('.'), trimmedName]
|
||||
: [newScriptCategory.value, trimmedName]
|
||||
if (checkDup(scriptPath)) {
|
||||
message.error(t('pages.scripts.duplicateScriptNameError'))
|
||||
return
|
||||
}
|
||||
newScriptNameVisible.value = false
|
||||
openEditPage(scriptPath, 'new')
|
||||
}
|
||||
|
||||
async function toggleScript(scriptPath: string[]) {
|
||||
const disabledList = ((await getConfig(configPaths.scripts.disabledList)) as string[] | undefined) || []
|
||||
const fullPath = scriptPath.join('/')
|
||||
if (disabledList.includes(fullPath)) {
|
||||
const index = disabledList.indexOf(fullPath)
|
||||
if (index > -1) {
|
||||
disabledList.splice(index, 1)
|
||||
}
|
||||
} else {
|
||||
disabledList.push(fullPath)
|
||||
}
|
||||
saveConfig(configPaths.scripts.disabledList, disabledList)
|
||||
await getScriptsMap()
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
getScriptsMap()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ScriptPage',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import 'tailwindcss' reference;
|
||||
@import '../assets/css/theme.css' reference;
|
||||
@import '../assets/css/utilities.css' reference;
|
||||
|
||||
.action-btn {
|
||||
@apply flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded-md border border-accent/20 text-secondary transition-all duration-fast ease-standard hover:scale-105 hover:bg-accent/30 hover:text-white disabled:cursor-not-allowed disabled:opacity-50 hover:not-disabled:[.danger]:border-danger hover:not-disabled:[.danger]:bg-danger;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user