Feature(custom): add new script system

ISSUES CLOSED: #462
This commit is contained in:
Kuingsmile
2026-01-26 17:57:44 +08:00
parent a10e701cc9
commit 9c8698907e
34 changed files with 1359 additions and 143 deletions

View File

@@ -21,14 +21,14 @@ const editorRef = ref(null)
const view = shallowRef(null)
onMounted(() => {
const languageExtension = props.language === 'json' ? json() : javascript()
const startState = EditorState.create({
doc: props.modelValue,
extensions: [
lineNumbers(),
history(),
keymap.of([...defaultKeymap, ...historyKeymap]),
json(),
javascript(),
languageExtension,
oneDark,
search({ top: true }),
keymap.of([...searchKeymap]),

View File

@@ -95,6 +95,7 @@
</div>
</template>
</SettingCard>
<slot name="extra-config" />
<slot />
</SettingSection>
</div>

View File

@@ -1,5 +1,5 @@
<template>
<div :class="tight ? 'mb-0' : 'mb-3'" class="flex items-center gap-2 text-sm font-medium text-main">
<div v-if="title" :class="tight ? 'mb-0' : 'mb-3'" class="flex items-center gap-2 text-sm font-medium text-main">
<slot name="icon">
<component :is="icon" v-if="icon" :size="iconSize" class="text-accent" />
</slot>
@@ -68,14 +68,14 @@ onClickOutside(dropdownRef, () => {
const {
tight = true,
title,
title = '',
icon = null,
iconSize = 18,
zeroPlaceholder,
allList,
} = defineProps<{
tight?: boolean
title: string
title?: string
icon?: any
iconSize?: number
zeroPlaceholder: string

View File

@@ -4,6 +4,7 @@
},
"common": {
"cancel": "Cancel",
"clear": "Clear",
"close": "Close",
"confirm": "Confirm",
"edit": "Edit",
@@ -69,6 +70,7 @@
"picbed": "PicBed",
"picBedQrCode": "PicBed QR Code",
"plugins": "Plugins",
"scripts": "Scripts",
"selected": "Selected",
"selectPicBeds": "Select PicBeds",
"settings": "Settings",
@@ -714,15 +716,65 @@
"rename": {
"placeholder": "Please enter new file name"
},
"scripts": {
"addNew": "Add New Script",
"chooseScriptType": "Please choose script type",
"confirmDelete": "Confirm delete script?",
"createScript": "Create Script",
"createScriptsToGo": "Please create scripts to get started",
"deleteFailed": "Delete script failed",
"deleteScript": "Delete Script",
"deleteScriptConfirm": "Confirm delete script {name}?",
"deleteScriptTitle": "Delete Script",
"deleteSuccess": "Delete script succeeded",
"description": "Extend PicList functionality with lightweight scripts",
"disabled": "Disabled",
"disableScript": "Disable Script",
"duplicateScriptNameError": "Script name already exists, please use a different name",
"editScript": "Edit Script",
"emptyScriptList": "Script list is empty",
"enabled": "Enabled",
"enableScript": "Enable Script",
"newScript": "New Script",
"NoScripts": "No Scripts",
"noScriptsFound": "No Scripts Found",
"openScriptFolder": "Open Script Folder",
"pleaseEnterScriptName": "Please enter script name",
"runScript": "Run Script",
"runScriptFailed": "Run script failed {errorMessage}",
"runScriptSuccess": "Script ran successfully",
"scriptContentPlaceholder": "Please enter script content",
"scriptNamePlaceholder": "Please enter script name",
"scriptSaved": "Script saved",
"scriptsTypes": {
"_name": "Script Types",
"afterUpload": "After Upload",
"beforeTransform": "Before Transform",
"beforeUpload": "Before Upload",
"manualTrigger": "Manual Trigger",
"onGalleryRemove": "On Gallery Remove",
"onSoftwareClose": "On Software Close",
"onSoftwareOpen": "On Software Open",
"onUploadSuccess": "On Upload Success",
"preProcess": "Pre Process",
"transform": "Transform",
"upload": "On Upload",
"uploader": {
"advancedplist": "Advanced Custom Uploader"
}
},
"selectScriptType": "Select Script Type",
"title": "Script Management"
},
"settings": {
"advanced": {
"chooseLogLevel": "Please select log level",
"enableServer": "Enable Upload API Service",
"enableWebServer": "Enable Web Server",
"guiLogFile": "GUI Log File",
"guiLogFile": "Open GUI Log File",
"invalidJson": "Invalid JSON format",
"logDialogDesc": "View log files and configure log settings",
"logFile": "General Log File",
"logFile": "Open General Log File",
"logFilePath": "Log File Path",
"logFilePathDesc": "View log file directory",
"logFileSize": "Log File Size",
@@ -737,7 +789,7 @@
"success": "Success",
"warn": "Warn"
},
"manageLogFile": "Manage Log File",
"manageLogFile": "Open Cloud Log File",
"networkAndProxy": "Network and Proxy",
"pluginInstallMirror": "Plugin Install Mirror",
"pluginInstallProxy": "Plugin Install Proxy",
@@ -782,7 +834,8 @@
"commonConfig": "Common Configuration",
"configureSync": "Platform Config",
"downloadSettings": "Download Settings",
"editConfigFile": "Edit Configuration File",
"editCloudConfigFile": "Edit Cloud Configuration File",
"editConfigFile": "Edit General Configuration File",
"fileManagement": "File Management",
"galleryDB": "Gallery Database Sync",
"gitea": {
@@ -804,7 +857,7 @@
"token": "GitHub Access Token",
"username": "GitHub Username"
},
"manageConfig": "Manage Configuration",
"manageConfig": "Cloud Configuration",
"migrateDesc": "Import configuration and gallery data from PicGo",
"migrateDescPicList": "Import configuration and gallery data from PicList Installation",
"migrateFromPicGo": "Migrate from PicGo",
@@ -815,8 +868,8 @@
"mirgrateSuccess": "Import Successful, Please Restart PicList to Take Effect",
"mirgrateTitle": "Notification",
"notConfigured": "Not configured",
"openConfigFile": "Open Configuration File",
"openConfigFileDir": "Open Configuration File Directory",
"openConfigFile": "Open General Configuration File",
"openConfigFileDir": "Open Application File Directory",
"selectType": "Please select sync platform",
"syncActions": "Sync Actions",
"syncConfigProxy": "Proxy",
@@ -919,8 +972,8 @@
"advancedRnameDialogDesc": "Configure advanced renaming rules for file uploads",
"advancedRnameFormat": "Advanced Rename Format",
"autoCopyUrlAfterUpload": "Auto Copy URL After Upload",
"autoImportInManage": "Auto Import Configuration in Management Page",
"autoImportInManageHint": "After enabling, the management page will automatically import the corresponding image bed configuration",
"autoImportInManage": "Auto Import Configuration in Cloud Page",
"autoImportInManageHint": "After enabling, the cloud page will automatically import the corresponding image bed configuration",
"autoImportPicBed": "Select the image bed to enable auto import",
"availablePlaceholders": "Available Placeholders",
"availablePlaceholdersTitle": "Use these placeholders to customize link format",
@@ -1154,6 +1207,15 @@
"title": "Configurations"
}
},
"scripts": {
"createScript": "Create Script",
"deleteScript": "Delete Script",
"duplicateScriptNameError": "Script name already exists",
"editScripts": "Edit Script",
"newScriptTitle": "New Script",
"noScriptsFound": "No Scripts Found",
"pleaseEnterScriptName": "Please enter script name"
},
"settings": {
"theme": {
"auto": "Auto",

View File

@@ -4,6 +4,7 @@
},
"common": {
"cancel": "取消",
"clear": "清空",
"close": "关闭",
"confirm": "确认",
"edit": "编辑",
@@ -69,6 +70,7 @@
"picbed": "图床",
"picBedQrCode": "图床配置二维码",
"plugins": "插件",
"scripts": "脚本",
"selected": "已选中",
"selectPicBeds": "请选择图床",
"settings": "设置",
@@ -547,7 +549,7 @@
"savedConfigs": "已配置云端",
"selectPlaceholder": "请选择",
"tips": "提示",
"title": "云端管理",
"title": "云端存储管理",
"viewDetails": "查看详情"
},
"main": {
@@ -714,15 +716,65 @@
"rename": {
"placeholder": "请输入新的文件名"
},
"scripts": {
"addNew": "添加新脚本",
"chooseScriptType": "请选择脚本类型",
"confirmDelete": "确认删除脚本吗?",
"createScript": "创建脚本",
"createScriptsToGo": "请先创建脚本以开始使用",
"deleteFailed": "删除脚本失败",
"deleteScript": "删除脚本",
"deleteScriptConfirm": "确认删除脚本 {name} 吗?",
"deleteScriptTitle": "删除脚本",
"deleteSuccess": "删除脚本成功",
"description": "使用轻量级脚本扩展 PicList 的功能",
"disabled": "已禁用",
"disableScript": "禁用脚本",
"duplicateScriptNameError": "脚本名称已存在,请使用不同的名称",
"editScript": "编辑脚本",
"emptyScriptList": "脚本列表为空",
"enabled": "已启用",
"enableScript": "启用脚本",
"newScript": "新脚本",
"NoScripts": "暂无脚本",
"noScriptsFound": "未找到脚本",
"openScriptFolder": "打开脚本文件夹",
"pleaseEnterScriptName": "请输入脚本名称",
"runScript": "运行脚本",
"runScriptFailed": "运行脚本失败 {errorMessage}",
"runScriptSuccess": "脚本运行成功",
"scriptContentPlaceholder": "请输入脚本内容",
"scriptNamePlaceholder": "请输入脚本名称",
"scriptSaved": "脚本已保存",
"scriptsTypes": {
"_name": "脚本类型",
"afterUpload": "上传后",
"beforeTransform": "图片变换前",
"beforeUpload": "上传前",
"manualTrigger": "手动触发",
"onGalleryRemove": "相册删除时",
"onSoftwareClose": "软件关闭时",
"onSoftwareOpen": "软件启动时",
"onUploadSuccess": "上传成功时",
"preProcess": "上传处理前",
"transform": "图片变换时",
"upload": "上传时",
"uploader": {
"advancedplist": "高级自定义图床"
}
},
"selectScriptType": "选择脚本类型",
"title": "脚本管理"
},
"settings": {
"advanced": {
"chooseLogLevel": "请选择日志记录等级",
"enableServer": "是否开启上传API服务",
"enableWebServer": "是否开启 Web 服务",
"guiLogFile": "GUI 日志文件",
"guiLogFile": "打开 GUI 日志文件",
"invalidJson": "无效的JSON格式",
"logDialogDesc": "查看日志文件和配置日志设置",
"logFile": "常规日志文件",
"logFile": "打开常规日志文件",
"logFilePath": "日志文件路径",
"logFilePathDesc": "查看日志文件所在目录",
"logFileSize": "日志文件大小",
@@ -737,7 +789,7 @@
"success": "成功",
"warn": "警告"
},
"manageLogFile": "管理日志文件",
"manageLogFile": "打开云端日志文件",
"networkAndProxy": "网络与代理",
"pluginInstallMirror": "插件安装镜像",
"pluginInstallProxy": "插件安装代理",
@@ -782,7 +834,8 @@
"commonConfig": "通用配置",
"configureSync": "平台设置",
"downloadSettings": "下载配置",
"editConfigFile": "编辑配置文件",
"editCloudConfigFile": "编辑云端配置文件",
"editConfigFile": "编辑通用配置文件",
"fileManagement": "文件管理",
"galleryDB": "相册数据库同步",
"gitea": {
@@ -804,7 +857,7 @@
"token": "GitHub 访问令牌",
"username": "GitHub 用户名"
},
"manageConfig": "管理配置",
"manageConfig": "云端管理配置",
"migrateDesc": "从 PicGo 导入配置和相册数据",
"migrateDescPicList": "从安装版PicList导入配置和相册数据",
"migrateFromPicGo": "从PicGo迁移",
@@ -815,8 +868,8 @@
"mirgrateSuccess": "导入成功, 请重启PicList生效",
"mirgrateTitle": "通知",
"notConfigured": "未配置",
"openConfigFile": "打开配置文件",
"openConfigFileDir": "打开配置文件目录",
"openConfigFile": "打开通用配置文件",
"openConfigFileDir": "打开应用文件目录",
"selectType": "请选择平台",
"syncActions": "同步操作",
"syncConfigProxy": "代理",
@@ -919,8 +972,8 @@
"advancedRnameDialogDesc": "配置文件上传时的高级重命名规则",
"advancedRnameFormat": "高级重命名格式",
"autoCopyUrlAfterUpload": "上传后自动复制URL",
"autoImportInManage": "管理页面自动导入配置",
"autoImportInManageHint": "启用后,管理页面将自动导入对应图床配置",
"autoImportInManage": "云端页面自动导入配置",
"autoImportInManageHint": "启用后,云端页面将自动导入对应图床配置",
"autoImportPicBed": "选择需要开启自动导入的图床",
"availablePlaceholders": "可用占位符",
"availablePlaceholdersTitle": "使用以下占位符自定义链接格式",
@@ -1146,6 +1199,7 @@
"duplicateSuccess": "拷贝成功",
"duplicateTitle": "拷贝配置",
"edit": "编辑",
"removeFromFavorites": "从快速切换中移除",
"selected": "已启用",
"setAsDefault": "设为默认图床",

View File

@@ -4,6 +4,7 @@
},
"common": {
"cancel": "取消",
"clear": "清空",
"close": "關閉",
"confirm": "確認",
"edit": "編輯",
@@ -69,6 +70,7 @@
"picbed": "圖床",
"picBedQrCode": "圖床配置 QRCODE",
"plugins": "插件",
"scripts": "腳本",
"selected": "已選中",
"selectPicBeds": "請選擇圖床",
"settings": "設定",
@@ -714,15 +716,65 @@
"rename": {
"placeholder": "請輸入新的檔案名稱"
},
"scripts": {
"addNew": "添加新腳本",
"chooseScriptType": "请选择腳本類型",
"confirmDelete": "确认删除腳本吗?",
"createScript": "创建腳本",
"createScriptsToGo": "请先创建腳本以开始使用",
"deleteFailed": "删除腳本失败",
"deleteScript": "删除腳本",
"deleteScriptConfirm": "确认删除腳本 {name} 吗?",
"deleteScriptTitle": "删除腳本",
"deleteSuccess": "删除腳本成功",
"description": "使用轻量级脚本扩展 PicList 的功能",
"disabled": "已禁用",
"disableScript": "禁用腳本",
"duplicateScriptNameError": "腳本名稱已存在,請使用不同的名稱",
"editScript": "編輯腳本",
"emptyScriptList": "腳本列表為空",
"enabled": "已啟用",
"enableScript": "啟用腳本",
"newScript": "新腳本",
"NoScripts": "暫無腳本",
"noScriptsFound": "未找到腳本",
"openScriptFolder": "打開腳本文件夾",
"pleaseEnterScriptName": "請輸入腳本名稱",
"runScript": "運行腳本",
"runScriptFailed": "運行腳本失敗 {errorMessage}",
"runScriptSuccess": "腳本運行成功",
"scriptContentPlaceholder": "請輸入腳本內容",
"scriptNamePlaceholder": "請輸入腳本名稱",
"scriptSaved": "腳本已保存",
"scriptsTypes": {
"_name": "腳本類型",
"afterUpload": "上傳後",
"beforeTransform": "圖片變換前",
"beforeUpload": "上傳前",
"manualTrigger": "手動觸發",
"onGalleryRemove": "相冊刪除時",
"onSoftwareClose": "軟件關閉時",
"onSoftwareOpen": "軟件啟動時",
"onUploadSuccess": "上傳成功時",
"preProcess": "上傳處理前",
"transform": "圖片變換時",
"upload": "上傳時",
"uploader": {
"advancedplist": "高級自定義圖床"
}
},
"selectScriptType": "選擇腳本類型",
"title": "腳本管理"
},
"settings": {
"advanced": {
"chooseLogLevel": "請選擇日誌記錄等級",
"enableServer": "是否開啟上傳API服務",
"enableWebServer": "是否開啟 Web 服務",
"guiLogFile": "GUI 日誌文件",
"guiLogFile": "打開 GUI 日誌文件",
"invalidJson": "無效的 JSON 格式",
"logDialogDesc": "查看日誌文件和配置日誌設置",
"logFile": "常規日誌文件",
"logFile": "打開常規日誌文件",
"logFilePath": "日誌文件路徑",
"logFilePathDesc": "查看日誌文件所在目錄",
"logFileSize": "日誌文件大小",
@@ -737,7 +789,7 @@
"success": "成功",
"warn": "警告"
},
"manageLogFile": "管理日誌文件",
"manageLogFile": "打開雲端日誌文件",
"networkAndProxy": "網絡與代理",
"pluginInstallMirror": "插件安裝鏡像",
"pluginInstallProxy": "插件安裝代理",
@@ -782,7 +834,8 @@
"commonConfig": "通用配置",
"configureSync": "平台配置",
"downloadSettings": "下載配置",
"editConfigFile": "編輯配置文件",
"editCloudConfigFile": "編輯雲端配置文件",
"editConfigFile": "編輯通用配置文件",
"fileManagement": "文件管理",
"galleryDB": "相冊數據庫同步",
"gitea": {
@@ -804,7 +857,7 @@
"token": "GitHub 訪問令牌",
"username": "GitHub 用戶名"
},
"manageConfig": "管理配置",
"manageConfig": "雲端配置",
"migrateDesc": "從 PicGo 導入配置和相冊數據",
"migrateDescPicList": "從安裝版 PicList 導入配置和相冊數據",
"migrateFromPicGo": "從PicGo遷移",
@@ -815,8 +868,8 @@
"mirgrateSuccess": "導入成功, 請重啟PicList生效",
"mirgrateTitle": "通知",
"notConfigured": "未配置",
"openConfigFile": "打開配置文件",
"openConfigFileDir": "打開配置文件目錄",
"openConfigFile": "打開通用配置文件",
"openConfigFileDir": "打開應用文件目錄",
"selectType": "請選擇同步平台類型",
"syncActions": "同步操作",
"syncConfigProxy": "代理",
@@ -919,8 +972,8 @@
"advancedRnameDialogDesc": "配置文件上傳時的高級重命名規則",
"advancedRnameFormat": "高級重命名格式",
"autoCopyUrlAfterUpload": "上傳後自動複製URL",
"autoImportInManage": "管理頁面自動導入配置",
"autoImportInManageHint": "啟用後,管理頁面將自動導入對應圖床配置",
"autoImportInManage": "雲端頁面自動導入配置",
"autoImportInManageHint": "啟用後,雲端頁面將自動導入對應圖床配置",
"autoImportPicBed": "選擇需要開啟自動導入的圖床",
"availablePlaceholders": "可用占位符",
"availablePlaceholdersTitle": "使用以下占位符自定義鏈接格式",
@@ -1154,6 +1207,15 @@
"title": "配置"
}
},
"scripts": {
"createScript": "創建腳本",
"deleteScript": "刪除腳本",
"duplicateScriptNameError": "腳本名稱已存在",
"editScripts": "編輯腳本",
"newScriptTitle": "新建腳本",
"noScriptsFound": "未找到腳本",
"pleaseEnterScriptName": "請輸入腳本名稱"
},
"settings": {
"theme": {
"auto": "自動",

View File

@@ -221,6 +221,7 @@ import {
Cloud,
CopyIcon,
DatabaseIcon,
FileCode,
ImagesIcon,
Info,
PlugIcon,
@@ -271,6 +272,11 @@ const navigationItems = computed(() => [
path: '/main-page/plugins',
icon: PlugIcon,
},
{
name: t('navigation.scripts'),
path: '/main-page/scripts',
icon: FileCode,
},
])
watch(

View File

@@ -37,7 +37,7 @@
<config-form :id="type" ref="$configForm" :config="config" type="uploader">
<!-- Action Buttons -->
<div class="mb-4 flex flex-wrap gap-3 rounded-xl border border-border bg-accent/10 p-4">
<CustomButton type="secondary" :icon="RotateCcw" :text="t('common.reset')" @click="handleReset" />
<CustomButton type="secondary" :icon="RotateCcw" :text="t('common.clear')" @click="handleReset" />
<CustomButton type="primary" :icon="Check" :text="t('common.confirm')" @click="handleConfirm" />
<div v-if="picBedConfigList.length > 0" class="relative">
@@ -179,6 +179,7 @@ const handleConfirm = async () => {
async function getPicBeds() {
try {
const result = await window.electron.triggerRPC<any>(IRPCActionType.PICBED_GET_PICBED_CONFIG, $route.params.type)
console.log('PicBed config result:', result)
config.value = result.config
picBedName.value = result.name
} catch (error) {
@@ -220,7 +221,30 @@ async function handleConfigImport(configItem: IUploaderConfigListItem) {
const handleReset = async () => {
try {
await window.electron.triggerRPC<void>(IRPCActionType.UPLOADER_RESET_CONFIG, type.value, $route.params.configId)
config.value.forEach(item => {
let defaultValue
switch (item.type) {
case 'text':
case 'password':
defaultValue = ''
break
case 'number':
defaultValue = 0
break
case 'checkbox':
defaultValue = []
break
case 'select':
defaultValue = item.choices && item.choices.length > 0 ? item.choices[0].value : null
break
case 'switch':
defaultValue = false
break
default:
defaultValue = null
}
$configForm.value?.updateRuleForm(item.name, defaultValue)
})
message.success(t('pages.picBedConfigs.resetSuccess'))
} catch (error) {
console.error('Failed to reset configuration:', error)

View File

@@ -279,6 +279,11 @@
:icon="Edit"
@click="editFile('data.json')"
/>
<CustomNavCard
:title="t('pages.settings.sync.editCloudConfigFile')"
:icon="Edit"
@click="editFile('manage.json')"
/>
<CustomNavCard
:title="t('pages.settings.sync.openConfigFileDir')"
:icon="FolderOpen"
@@ -562,10 +567,22 @@
>
<SettingSection :icon="FileText" :title="t('pages.settings.advanced.logging')">
<CustomNavCard
:title="t('pages.settings.advanced.logFilePath')"
:description="t('pages.settings.advanced.logFilePathDesc')"
:icon="FolderOpen"
@click="openDirectory"
:title="t('pages.settings.advanced.logFile')"
description="piclist.log"
:icon="FileText"
@click="openFile('piclist.log')"
/>
<CustomNavCard
:title="t('pages.settings.advanced.guiLogFile')"
description="piclist-gui-local.log"
:icon="FileText"
@click="openFile('piclist-gui-local.log')"
/>
<CustomNavCard
:title="t('pages.settings.advanced.manageLogFile')"
description="manage.log"
:icon="FileText"
@click="openFile('manage.log')"
/>
<CustomNavCard
:title="t('pages.settings.advanced.setLog')"
@@ -902,24 +919,6 @@
>
<div class="flex h-full w-full flex-col p-4">
<SettingSection>
<CustomNavCard
:title="t('pages.settings.advanced.logFile')"
description="piclist.log"
:icon="FileText"
@click="openFile('piclist.log')"
/>
<CustomNavCard
:title="t('pages.settings.advanced.guiLogFile')"
description="piclist-gui-local.log"
:icon="FileText"
@click="openFile('piclist-gui-local.log')"
/>
<CustomNavCard
:title="t('pages.settings.advanced.manageLogFile')"
description="manage.log"
:icon="FileText"
@click="openFile('manage.log')"
/>
<CustomInput
v-model="formOfSetting.logFileSizeLimit"
:title="t('pages.settings.advanced.logFileSize')"

View File

@@ -5,7 +5,7 @@
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">
<DatabaseIcon :size="24" class="text-accent" />
<PlugIcon :size="24" class="text-accent" />
<div>
<h1 class="m-0 text-2xl font-semibold tracking-tight text-main">{{ t('pages.plugin.title') }}</h1>
<p class="m-0 text-sm text-secondary">{{ t('pages.plugin.description') }}</p>
@@ -407,10 +407,10 @@ import { debounce, DebouncedFunc } from 'lodash-es'
import {
AlertCircleIcon,
CheckIcon,
DatabaseIcon,
DownloadIcon,
ExternalLinkIcon,
PackageIcon,
PlugIcon,
RefreshCwIcon,
SearchIcon,
SettingsIcon,
@@ -835,9 +835,6 @@ onBeforeUnmount(() => {
window.electron.ipcRendererRemoveAllListeners(PICGO_CONFIG_PLUGIN)
window.electron.ipcRendererRemoveAllListeners(PICGO_HANDLE_PLUGIN_ING)
window.electron.ipcRendererRemoveAllListeners(PICGO_TOGGLE_PLUGIN)
// Reset body overflow
document.body.style.overflow = 'auto'
})
</script>

View 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>

View File

@@ -17,6 +17,58 @@
</div>
</div>
<div class="flex items-center justify-center gap-3">
<div class="relative">
<CustomButton
v-if="type === 'advancedplist'"
type="secondary"
:text="t('pages.scripts.editScript')"
@click="openScriptsList"
/>
<div
v-if="scriptsListVisible"
class="absolute top-full left-1/2 z-10 mt-2 w-max -translate-x-1/2 gap-2 rounded-md border-2 border-border bg-bg-tertiary px-3 py-1.5 text-sm font-medium text-main shadow-md transition-all duration-fast ease-apple"
>
<div class="no-scrollbar flex max-h-[200px] min-w-[150px] flex-col overflow-auto">
<div
v-for="script in scriptsList"
:key="script"
class="cursor-pointer rounded-md border-b border-border px-2 py-1 text-center whitespace-nowrap last:border-b-0 hover:bg-accent/20"
@click="handleScriptClick(script)"
>
{{ script }}
</div>
</div>
</div>
</div>
<CustomButton
v-if="type === 'advancedplist'"
type="primary"
:text="t('pages.scripts.createScript')"
@click="openNewScriptsNameDialog"
/>
<div class="relative">
<CustomButton
v-if="type === 'advancedplist'"
type="secondary"
:text="t('pages.scripts.deleteScript')"
@click="openDeleteScriptsList"
/>
<div
v-if="deleteScriptListVisible"
class="absolute top-full left-1/2 z-10 mt-2 w-max -translate-x-1/2 gap-2 rounded-md border-2 border-border bg-bg-tertiary px-3 py-1.5 text-sm font-medium text-main shadow-md transition-all duration-fast ease-apple"
>
<div class="no-scrollbar flex max-h-[200px] min-w-[150px] flex-col overflow-auto">
<div
v-for="script in scriptsList"
:key="script"
class="cursor-pointer rounded-md border-b border-border px-2 py-1 text-center whitespace-nowrap last:border-b-0 hover:bg-accent/20"
@click="deleteScript(script)"
>
{{ script }}
</div>
</div>
</div>
</div>
<button
class="relative inline-flex cursor-pointer items-center justify-center gap-2 overflow-hidden rounded-lg border-none bg-accent px-6 py-3 font-[inherit] text-sm font-semibold text-white shadow-sm transition-all duration-fast ease-apple disabled:cursor-not-allowed disabled:bg-surface disabled:text-secondary disabled:opacity-60"
:disabled="defaultPicBedG === type"
@@ -39,7 +91,7 @@
<div
v-for="(item, index) in curConfigList"
:key="item._id"
class="group/config-card relative flex min-h-[180px] 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-accent hover:shadow-md [.is-active]:border-2 [.is-active]:border-accent [.is-active]:shadow-md"
class="group/config-card relative flex min-h-[180px] 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 [.is-active]:border-2 [.is-active]:border-accent [.is-active]:shadow-md"
:class="{ 'is-active': defaultConfigId === item._id }"
:style="{ '--delay': `${index * 50}ms` }"
@click="() => selectItem(item._id)"
@@ -135,6 +187,36 @@
</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="400px"
>
<div class="flex items-center justify-center bg-bg-secondary p-6">
<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>
@@ -146,6 +228,11 @@ import { computed, onBeforeMount, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'
import CustomButton from '@/components/common/CustomButton.vue'
import CustomInput from '@/components/common/CustomInput.vue'
import CustomModal from '@/components/common/CustomModal.vue'
import SettingCard from '@/components/common/SettingCard.vue'
import Editor from '@/components/Editor.vue'
import useConfirm from '@/hooks/useConfirm'
import { usePicBed } from '@/hooks/useGlobal'
import useMessage from '@/hooks/useMessage'
@@ -153,8 +240,9 @@ import { PICBEDS_PAGE, UPLOADER_CONFIG_PAGE } from '@/router/config'
import $bus from '@/utils/bus'
import { configPaths } from '@/utils/configPaths'
import { SHOW_INPUT_BOX, SHOW_INPUT_BOX_RESPONSE } from '@/utils/constant'
import { saveConfig } from '@/utils/dataSender'
import { IRPCActionType } from '@/utils/enum'
import { getConfig, saveConfig } from '@/utils/dataSender'
import { II18nLanguage, IRPCActionType } from '@/utils/enum'
import { defaultScriptTemplate, defaultScriptTemplateEn } from '@/utils/static'
const { t } = useI18n()
const message = useMessage()
@@ -166,6 +254,14 @@ const favoritePicbeds = useStorage<IFavoritePicbedItem[]>('favorite-picbeds', []
const type = ref('')
const curConfigList = ref<IStringKeyMap[]>([])
const defaultConfigId = ref('')
const scriptsListVisible = ref(false)
const scriptsList = ref<string[]>([])
const editorVisible = ref(false)
const editorContent = ref('')
const editingScriptName = ref('')
const newScriptNameVisible = ref(false)
const newScriptName = ref('')
const deleteScriptListVisible = ref(false)
const picBedName = computed(() => {
if (!picBedG.value || picBedG.value.length === 0) {
@@ -316,6 +412,122 @@ function setDefaultPicBed(type: string) {
message.success(t('pages.uploaderConfig.setSuccess'))
}
async function getScriptsList() {
const scriptsFiles = await window.electron.triggerRPC<Record<string, any>>(IRPCActionType.LIST_SCRIPTS_FILES, [
'uploader',
'advancedplist',
])
scriptsList.value = Object.keys(scriptsFiles || {}).filter(fileName => fileName.endsWith('.js'))
}
async function openScriptsList() {
if (scriptsListVisible.value) {
scriptsListVisible.value = false
return
}
await getScriptsList()
if (scriptsList.value.length === 0) {
message.info(t('pages.scripts.noScriptsFound'))
return
}
scriptsListVisible.value = true
}
function openNewScriptsNameDialog() {
newScriptName.value = ''
newScriptNameVisible.value = true
}
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 openEditScripts(scriptName: string, mode: 'edit' | 'new' = 'edit') {
editingScriptName.value = scriptName
if (mode === 'edit') {
const filePath = ['uploader', 'advancedplist', editingScriptName.value]
const content = (await window.electron.triggerRPC<string>(IRPCActionType.READ_SCRIPTS_FILE, filePath)) || ''
editorContent.value = content
} else {
editorContent.value = await getTemplate()
}
editorVisible.value = true
}
async function saveEditorContent() {
const file = ['uploader', 'advancedplist', editingScriptName.value]
const content = editorContent.value.trim()
try {
window.electron.sendRPC(IRPCActionType.WRITE_SCRIPT_FILE, file, content)
message.success(t('pages.settings.advanced.saveFileSuccess'))
await getScriptsList()
} catch (error) {
console.error('Failed to save file:', error)
message.error(t('pages.settings.advanced.saveFileFailed'))
}
editorVisible.value = false
}
function handleNewScriptNameConfirm() {
let trimmedName = newScriptName.value.trim()
trimmedName = trimmedName.endsWith('.js') ? trimmedName : `${trimmedName}.js`
if (!trimmedName) {
message.error(t('pages.scripts.pleaseEnterScriptName'))
return
}
if (scriptsList.value.includes(trimmedName)) {
message.error(t('pages.scripts.duplicateScriptNameError'))
return
}
newScriptNameVisible.value = false
openEditScripts(trimmedName, 'new')
}
function handleScriptClick(scriptName: string) {
scriptsListVisible.value = false
openEditScripts(scriptName)
}
function openDeleteScriptsList() {
if (deleteScriptListVisible.value) {
deleteScriptListVisible.value = false
return
}
getScriptsList().then(() => {
if (scriptsList.value.length === 0) {
message.info(t('pages.scripts.noScriptsFound'))
return
}
deleteScriptListVisible.value = true
})
}
async function deleteScript(scriptName: string) {
const result = await confirm({
title: t('pages.scripts.deleteScriptTitle'),
message: t('pages.scripts.deleteScriptConfirm', { name: scriptName }),
type: 'warning',
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
center: true,
})
if (!result) return
try {
const filePath = ['uploader', 'advancedplist', scriptName]
window.electron.sendRPC(IRPCActionType.DELETE_SCRIPTS_FILE, filePath)
message.success(t('pages.scripts.deleteSuccess'))
await getScriptsList()
} catch (error) {
console.error('Failed to delete script file:', error)
message.error(t('pages.scripts.deleteFailed'))
}
}
onBeforeRouteUpdate((to, _, next) => {
if (to.params.type && to.name === UPLOADER_CONFIG_PAGE) {
type.value = to.params.type as string
@@ -327,6 +539,7 @@ onBeforeRouteUpdate((to, _, next) => {
onBeforeMount(() => {
type.value = route.params.type as string
getCurrentConfigList()
getScriptsList()
})
</script>

View File

@@ -19,3 +19,4 @@ export const UPDATE_PAGE = 'UpdatePage'
export const UPLOAD_PAGE = 'UploadPage'
export const UPLOADER_CONFIG_PAGE = 'UploaderConfigPage'
export const MANAGE_EDIT_PAGE = 'ManageEditPage'
export const SCRIPT_PAGE = 'ScriptPage'

View File

@@ -12,6 +12,7 @@ import PicBedsPage from '@/pages/PicBed.vue'
import SettingPage from '@/pages/PicGoSetting.vue'
import PluginPage from '@/pages/Plugin.vue'
import RenamePage from '@/pages/RenamePage.vue'
import ScriptPage from '@/pages/ScriptPage.vue'
import ShortKeyPage from '@/pages/ShortKey.vue'
import Toolbox from '@/pages/Toolbox.vue'
import TrayPage from '@/pages/TrayPage.vue'
@@ -104,6 +105,11 @@ export default createRouter({
component: PluginPage,
name: config.PLUGIN_PAGE,
},
{
path: 'scripts',
component: ScriptPage,
name: config.SCRIPT_PAGE,
},
{
path: 'shortKey',
component: ShortKeyPage,

View File

@@ -93,6 +93,9 @@ export interface IConfigStruct {
needReload: boolean
picgoPlugins: IPicGoPlugins
uploader: IUploaderConfig
scripts: {
disabledList: string[]
}
buildIn: {
compress: IBuildInCompressOptions
watermark: IBuildInWaterMarkOptions
@@ -189,6 +192,9 @@ export const configPaths = {
needReload: 'needReload',
picgoPlugins: 'picgoPlugins',
uploader: 'uploader',
scripts: {
disabledList: 'scripts.disabledList',
},
buildIn: {
_name: 'buildIn',
compress: 'buildIn.compress',

View File

@@ -30,6 +30,7 @@ export const IRPCActionType = {
OPEN_WINDOW: 'OPEN_WINDOW',
OPEN_MINI_WINDOW: 'OPEN_MINI_WINDOW',
CLOSE_WINDOW: 'CLOSE_WINDOW',
RELOAD_WINDOW: 'RELOAD_WINDOW',
MINIMIZE_WINDOW: 'MINIMIZE_WINDOW',
SHOW_MINI_PAGE_MENU: 'SHOW_MINI_PAGE_MENU',
SHOW_MAIN_PAGE_MENU: 'SHOW_MAIN_PAGE_MENU',
@@ -41,6 +42,7 @@ export const IRPCActionType = {
MAIN_WINDOW_ON_TOP: 'MAIN_WINDOW_ON_TOP',
UPDATE_MINI_WINDOW_ICON: 'UPDATE_MINI_WINDOW_ICON',
REFRESH_SETTING_WINDOW: 'REFRESH_SETTING_WINDOW',
// picbed RPC
PICBED_GET_PICBED_CONFIG: 'PICBED_GET_PICBED_CONFIG',
PICBED_GET_CONFIG_LIST: 'PICBED_GET_CONFIG_LIST',
@@ -51,6 +53,8 @@ export const IRPCActionType = {
UPLOADER_UPDATE_CONFIG: 'UPLOADER_UPDATE_CONFIG',
UPLOADER_RESET_CONFIG: 'UPLOADER_RESET_CONFIG',
DELETE_ALL_API: 'DELETE_ALL_API',
GET_FILES_STAT: 'GET_FILES_STAT',
RUN_SCRIPT_FILE: 'RUN_SCRIPT_FILE',
// toolbox rpc
TOOLBOX_CHECK: 'TOOLBOX_CHECK',
@@ -65,9 +69,15 @@ export const IRPCActionType = {
PICLIST_OPEN_DIRECTORY: 'PICLIST_OPEN_DIRECTORY',
PICLIST_AUTO_START: 'PICLIST_AUTO_START',
PICLIST_AUTO_START_STATUS: 'PICLIST_AUTO_START_STATUS',
// file operation rpc
READ_FILE_CONTENT: 'READ_FILE_CONTENT',
WRITE_FILE_CONTENT: 'WRITE_FILE_CONTENT',
RELOAD_WINDOW: 'RELOAD_WINDOW',
CREATE_SCRIPTS_FILE: 'CREATE_SCRIPTS_FILE',
READ_SCRIPTS_FILE: 'READ_SCRIPTS_FILE',
LIST_SCRIPTS_FILES: 'LIST_SCRIPTS_FILES',
WRITE_SCRIPT_FILE: 'WRITE_SCRIPT_FILE',
DELETE_SCRIPTS_FILE: 'DELETE_SCRIPTS_FILE',
// shortkey setting rpc
SHORTKEY_UPDATE: 'SHORTKEY_UPDATE',

View File

@@ -67,3 +67,25 @@ export const picBedManualUrlList: IStringKeyMap = {
webdavplist: 'https://piclist.cn/en/configure.html#webdav',
},
}
export const defaultScriptTemplate = `
// ctx 为 核心PicList实例, extra为额外参数, 其中extra.galleryItem为当前删除的相册对象
// 可用额外API: axios, crypto, fs, path, os, setTimeout, setInterval, clearTimeout, clearInterval, base64Decode, base64Encode
// 图床上传脚本必须返回 ctx 对象, 其它脚本可根据需求返回任意数据
async function main(ctx, extra) {
// 在这里编写你的脚本代码
return ctx
}
`
export const defaultScriptTemplateEn = `
// ctx is the core PicList instance, extra is additional parameters, among which extra.galleryItem is the currently deleted album object
// Available additional APIs: axios, crypto, fs, path, os, setTimeout, setInterval, clearTimeout, clearInterval, base64Decode, base64Encode
// The image bed upload script must return the ctx object, other scripts can return any data as needed
async function main(ctx, extra) {
// Write your script code here
return ctx
}
`