Feature(custom): add plugin browser dialog

This commit is contained in:
Kuingsmile
2026-01-17 11:51:02 +08:00
parent 2c13ae63ab
commit 8b0b476a12
7 changed files with 409 additions and 29 deletions

View File

@@ -2,15 +2,16 @@
### ✨ 新增功能 ### ✨ 新增功能
- windows下新增便携模式无需安装即可运行数据存储在程序目录下的`data`文件夹中,支持自动更新 - windows下新增便携模式无需安装即可运行数据存储在程序目录下的`data`文件夹中,支持自动更新Linux下新增`rpm`安装包
- 新增自定义主题功能,主题仓库[PicList ThemeHub](https://github.com/Kuingsmile/PicList-ThemeHub) - 新增自定义主题功能,主题仓库[PicList ThemeHub](https://github.com/Kuingsmile/PicList-ThemeHub)
- 12个内置主题供选择如bilibili、二次元、极夜紫、gemini等风格 - 12个内置主题供选择如bilibili、二次元、极夜紫、gemini等风格
- 相册页面多项优化支持显示已选择图片数量匹配的url列表和记忆过滤器打开状态 - 相册页面多项优化支持显示已选择图片数量匹配的url列表和记忆过滤器打开状态
- 插件页面现在可以浏览所有插件列表,查看详情和安装
- 新增教学引导页面,首次运行时会自动弹出 - 新增教学引导页面,首次运行时会自动弹出
- 现在支持关闭GPU加速解决部分兼容性问题 - 现在支持关闭GPU加速解决部分兼容性问题
- 新增高级动画设置开启后可获得更好的UI体验 - 新增高级动画设置开启后可获得更好的UI体验
- Linux下新增rpm安装包
- 优化了相册页面的加载速度 - 优化了多个页面的加载速度和浏览性能
### 🐛 问题修复 ### 🐛 问题修复

View File

@@ -2,15 +2,15 @@
### ✨ New Features ### ✨ New Features
- Added portable mode on Windows, allowing the program to run without installation. Data is stored in the `data` folder within the program directory, and automatic updates are supported. - Added portable mode on Windows, allowing the program to run without installation. Data is stored in the `data` folder within the program directory, and automatic updates are supported. Added `rpm` installation package for Linux
- Added custom theme functionality, with a theme repository available at [PicList ThemeHub](https://github.com/Kuingsmile/PicList-ThemeHub) - Added custom theme functionality, with a theme repository available at [PicList ThemeHub](https://github.com/Kuingsmile/PicList-ThemeHub)
- 12 built-in themes available, such as bilibili, ACG, Night Purple, gemini styles - 12 built-in themes available, such as bilibili, ACG, Night Purple, gemini styles
- Multiple optimizations on the album page, supporting display of the number of selected images, matching URL list, and remembering filter open state - Multiple optimizations on the album page, supporting display of the number of selected images, matching URL list, and remembering filter open state
- Plugin page now allows browsing of all plugin lists, viewing details, and installation
- Added tutorial guide page, which automatically pops up on first run - Added tutorial guide page, which automatically pops up on first run
- Now supports disabling GPU acceleration to resolve some compatibility issues - Now supports disabling GPU acceleration to resolve some compatibility issues
- Added advanced animation settings for a better UI experience - Added advanced animation settings for a better UI experience
- Added rpm installation package for Linux - Optimized the loading speed of multiple pages and browsing performance
- Optimized the loading speed of the album page
### 🐛 Bug Fixes ### 🐛 Bug Fixes

View File

@@ -1,5 +1,7 @@
{ {
"app": { "title": "PicList" }, "app": {
"title": "PicList"
},
"common": { "common": {
"cancel": "Cancel", "cancel": "Cancel",
"close": "Close", "close": "Close",
@@ -664,6 +666,7 @@
"viewDocFailed": "View Document Failed" "viewDocFailed": "View Document Failed"
}, },
"plugin": { "plugin": {
"browseAllPlugins": "Browse All Plugins",
"browsePlugins": "Browse Plugins", "browsePlugins": "Browse Plugins",
"configThing": "Config {c}", "configThing": "Config {c}",
"description": "PicList Plugin Management", "description": "PicList Plugin Management",
@@ -676,12 +679,15 @@
"installPluginsToGetStarted": "Please install plugins to get started", "installPluginsToGetStarted": "Please install plugins to get started",
"list": "Plugin List", "list": "Plugin List",
"loading": "Loading...", "loading": "Loading...",
"loadingPlugins": "Loading plugins...",
"needRestart": "Need Restart to Take Effect", "needRestart": "Need Restart to Take Effect",
"noPluginsFound": "No Plugins Found", "noPluginsFound": "No Plugins Found",
"NoPluginsInstalled": "No Plugins Installed", "NoPluginsInstalled": "No Plugins Installed",
"notGuiImplement": "This plugin does not have a GUI implementation, continue?", "notGuiImplement": "This plugin does not have a GUI implementation, continue?",
"openRemoteList": "Open Remote Plugin List",
"pluginList": "Plugin List", "pluginList": "Plugin List",
"restartApp": "Restart Application", "restartApp": "Restart Application",
"searchInBrowse": "Search in Browse",
"searchPlaceholder": "Search for PicGo plugins on npm, or click the button above to view the excellent plugin list", "searchPlaceholder": "Search for PicGo plugins on npm, or click the button above to view the excellent plugin list",
"setResult": "Set Result", "setResult": "Set Result",
"setSuccess": "Set Success", "setSuccess": "Set Success",
@@ -800,7 +806,10 @@
"syncConfigProxy": "Proxy", "syncConfigProxy": "Proxy",
"syncConfiguration": "Sync Platform", "syncConfiguration": "Sync Platform",
"syncEndpointConfig": "Endpoint Configuration", "syncEndpointConfig": "Endpoint Configuration",
"syncResult": { "failed": "Sync Failed", "success": "Sync Successful" }, "syncResult": {
"failed": "Sync Failed",
"success": "Sync Successful"
},
"title": "Sync", "title": "Sync",
"upDownloadDesc": "Sync configuration files and gallery data to cloud", "upDownloadDesc": "Sync configuration files and gallery data to cloud",
"upDownloadSettings": "Sync Config & Gallery", "upDownloadSettings": "Sync Config & Gallery",
@@ -1098,7 +1107,11 @@
"uploadHint": "All formats are supported, but images are recommended", "uploadHint": "All formats are supported, but images are recommended",
"uploadingMultipleUrls": "Uploading {count} URLs...", "uploadingMultipleUrls": "Uploading {count} URLs...",
"uploadViewHint": "Click to open picbeds settings", "uploadViewHint": "Click to open picbeds settings",
"urlType": { "normal": "Long Link", "short": "Short Link", "title": "Link Type" }, "urlType": {
"normal": "Long Link",
"short": "Short Link",
"title": "Link Type"
},
"urlUpload": "URL Upload" "urlUpload": "URL Upload"
}, },
"uploaderConfig": { "uploaderConfig": {
@@ -1135,5 +1148,10 @@
"toggle": "Toggle Theme" "toggle": "Toggle Theme"
} }
}, },
"titleBar": { "alwaysOnTop": "Always On Top", "close": "Close", "minimize": "Minimize", "miniWindow": "Mini Window" } "titleBar": {
"alwaysOnTop": "Always On Top",
"close": "Close",
"minimize": "Minimize",
"miniWindow": "Mini Window"
}
} }

View File

@@ -1,5 +1,7 @@
{ {
"app": { "title": "PicList" }, "app": {
"title": "PicList"
},
"common": { "common": {
"cancel": "取消", "cancel": "取消",
"close": "关闭", "close": "关闭",
@@ -664,6 +666,7 @@
"viewDocFailed": "查看文档失败" "viewDocFailed": "查看文档失败"
}, },
"plugin": { "plugin": {
"browseAllPlugins": "浏览所有插件",
"browsePlugins": "浏览插件", "browsePlugins": "浏览插件",
"configThing": "配置 {c}", "configThing": "配置 {c}",
"description": "PicList 插件管理页面", "description": "PicList 插件管理页面",
@@ -676,12 +679,15 @@
"installPluginsToGetStarted": "请先安装插件以开始使用", "installPluginsToGetStarted": "请先安装插件以开始使用",
"list": "插件列表", "list": "插件列表",
"loading": "加载中...", "loading": "加载中...",
"loadingPlugins": "正在加载插件...",
"needRestart": "需要重启生效", "needRestart": "需要重启生效",
"noPluginsFound": "未找到插件", "noPluginsFound": "未找到插件",
"NoPluginsInstalled": "暂无已安装插件", "NoPluginsInstalled": "暂无已安装插件",
"notGuiImplement": "该插件未对可视化界面进行优化, 是否继续安装?", "notGuiImplement": "该插件未对可视化界面进行优化, 是否继续安装?",
"openRemoteList": "打开远程插件列表",
"pluginList": "插件列表", "pluginList": "插件列表",
"restartApp": "重启应用", "restartApp": "重启应用",
"searchInBrowse": "搜索插件",
"searchPlaceholder": "搜索 npm 上的 PicGo 插件,或者点击上方按钮查看优秀插件列表", "searchPlaceholder": "搜索 npm 上的 PicGo 插件,或者点击上方按钮查看优秀插件列表",
"setResult": "设置结果", "setResult": "设置结果",
"setSuccess": "设置成功", "setSuccess": "设置成功",
@@ -763,7 +769,12 @@
"downloadSettings": "下载配置", "downloadSettings": "下载配置",
"fileManagement": "文件管理", "fileManagement": "文件管理",
"galleryDB": "相册数据库同步", "galleryDB": "相册数据库同步",
"gitea": { "branch": "分支名", "repo": "仓库名", "token": "访问令牌", "username": "用户名" }, "gitea": {
"branch": "分支名",
"repo": "仓库名",
"token": "访问令牌",
"username": "用户名"
},
"giteaHost": "Gitea 地址", "giteaHost": "Gitea 地址",
"gitee": { "gitee": {
"branch": "Gitee 分支名", "branch": "Gitee 分支名",
@@ -795,7 +806,10 @@
"syncConfigProxy": "代理", "syncConfigProxy": "代理",
"syncConfiguration": "同步平台", "syncConfiguration": "同步平台",
"syncEndpointConfig": "平台设置", "syncEndpointConfig": "平台设置",
"syncResult": { "failed": "同步失败", "success": "同步成功" }, "syncResult": {
"failed": "同步失败",
"success": "同步成功"
},
"title": "同步", "title": "同步",
"upDownloadDesc": "同步配置文件和相册数据到云端", "upDownloadDesc": "同步配置文件和相册数据到云端",
"upDownloadSettings": "同步配置和相册", "upDownloadSettings": "同步配置和相册",
@@ -1093,7 +1107,11 @@
"uploadHint": "支持所有文件类型,但推荐仅上传图片", "uploadHint": "支持所有文件类型,但推荐仅上传图片",
"uploadingMultipleUrls": "正在上传 {count} 个URL...", "uploadingMultipleUrls": "正在上传 {count} 个URL...",
"uploadViewHint": "点击打开图床设置", "uploadViewHint": "点击打开图床设置",
"urlType": { "normal": "长链接", "short": "短链接", "title": "链接类型" }, "urlType": {
"normal": "长链接",
"short": "短链接",
"title": "链接类型"
},
"urlUpload": "URL上传" "urlUpload": "URL上传"
}, },
"uploaderConfig": { "uploaderConfig": {
@@ -1130,5 +1148,10 @@
"toggle": "切换主题" "toggle": "切换主题"
} }
}, },
"titleBar": { "alwaysOnTop": "置顶", "close": "关闭", "minimize": "最小化", "miniWindow": "迷你窗口" } "titleBar": {
"alwaysOnTop": "置顶",
"close": "关闭",
"minimize": "最小化",
"miniWindow": "迷你窗口"
}
} }

View File

@@ -1,5 +1,7 @@
{ {
"app": { "title": "PicList" }, "app": {
"title": "PicList"
},
"common": { "common": {
"cancel": "取消", "cancel": "取消",
"close": "關閉", "close": "關閉",
@@ -664,6 +666,7 @@
"viewDocFailed": "查看文件失敗" "viewDocFailed": "查看文件失敗"
}, },
"plugin": { "plugin": {
"browseAllPlugins": "瀏覽所有插件",
"browsePlugins": "瀏覽插件", "browsePlugins": "瀏覽插件",
"configThing": "配置 {c}", "configThing": "配置 {c}",
"description": "PicList 插件管理頁面", "description": "PicList 插件管理頁面",
@@ -676,12 +679,15 @@
"installPluginsToGetStarted": "請先安裝插件以開始使用", "installPluginsToGetStarted": "請先安裝插件以開始使用",
"list": "插件列表", "list": "插件列表",
"loading": "載入中...", "loading": "載入中...",
"needRestart": "需要重啟生效", "loadingPlugins": "正在加載插件...",
"needRestart": "需要重啟才能生效",
"noPluginsFound": "未找到插件", "noPluginsFound": "未找到插件",
"NoPluginsInstalled": "尚未安裝任何插件", "NoPluginsInstalled": "尚未安裝任何插件",
"notGuiImplement": "該插件未針對圖形介面進行優化,是否繼續安裝?", "notGuiImplement": "該插件未針對圖形介面進行優化,是否繼續安裝?",
"openRemoteList": "打開遠程插件列表",
"pluginList": "插件列表", "pluginList": "插件列表",
"restartApp": "重啟應用", "restartApp": "重啟應用",
"searchInBrowse": "在瀏覽中搜索",
"searchPlaceholder": "搜尋 npm 上的 PicGo 插件,或者點擊上方按鈕查看優秀插件列表", "searchPlaceholder": "搜尋 npm 上的 PicGo 插件,或者點擊上方按鈕查看優秀插件列表",
"setResult": "設定結果", "setResult": "設定結果",
"setSuccess": "設定成功", "setSuccess": "設定成功",
@@ -763,7 +769,12 @@
"downloadSettings": "下載配置", "downloadSettings": "下載配置",
"fileManagement": "文件管理", "fileManagement": "文件管理",
"galleryDB": "相冊數據庫同步", "galleryDB": "相冊數據庫同步",
"gitea": { "branch": "分支名", "repo": "倉庫名", "token": "訪問令牌", "username": "用戶名" }, "gitea": {
"branch": "分支名",
"repo": "倉庫名",
"token": "訪問令牌",
"username": "用戶名"
},
"giteaHost": "Gitea 地址", "giteaHost": "Gitea 地址",
"gitee": { "gitee": {
"branch": "Gitee 分支名", "branch": "Gitee 分支名",
@@ -795,7 +806,10 @@
"syncConfigProxy": "代理", "syncConfigProxy": "代理",
"syncConfiguration": "同步平台", "syncConfiguration": "同步平台",
"syncEndpointConfig": "平台配置", "syncEndpointConfig": "平台配置",
"syncResult": { "failed": "同步失敗", "success": "同步成功" }, "syncResult": {
"failed": "同步失敗",
"success": "同步成功"
},
"title": "同步", "title": "同步",
"upDownloadDesc": "同步配置文件和相冊數據到雲端", "upDownloadDesc": "同步配置文件和相冊數據到雲端",
"upDownloadSettings": "同步配置和相冊", "upDownloadSettings": "同步配置和相冊",
@@ -1093,7 +1107,11 @@
"uploadHint": "支持所有文件類型,但推薦僅上傳圖片", "uploadHint": "支持所有文件類型,但推薦僅上傳圖片",
"uploadingMultipleUrls": "正在上傳 {count} 個URL...", "uploadingMultipleUrls": "正在上傳 {count} 個URL...",
"uploadViewHint": "點擊打開圖床設定", "uploadViewHint": "點擊打開圖床設定",
"urlType": { "normal": "長連結", "short": "短連結", "title": "連結類型" }, "urlType": {
"normal": "長連結",
"short": "短連結",
"title": "連結類型"
},
"urlUpload": "URL上傳" "urlUpload": "URL上傳"
}, },
"uploaderConfig": { "uploaderConfig": {
@@ -1130,5 +1148,10 @@
"toggle": "切換主題" "toggle": "切換主題"
} }
}, },
"titleBar": { "alwaysOnTop": "置頂", "close": "關閉", "minimize": "最小化", "miniWindow": "迷你視窗" } "titleBar": {
"alwaysOnTop": "置頂",
"close": "關閉",
"minimize": "最小化",
"miniWindow": "迷你視窗"
}
} }

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="plugin-container"> <div class="plugin-container">
<!-- Header Card --> <!-- Header Card -->
<div class="plugin-card header-card"> <div class="header-card">
<div class="card-header"> <div class="card-header">
<div class="header-content"> <div class="header-content">
<div class="header-icon"> <div class="header-icon">
@@ -25,10 +25,22 @@
<RefreshCwIcon :size="16" /> <RefreshCwIcon :size="16" />
{{ t('pages.plugin.updateAll') }} {{ t('pages.plugin.updateAll') }}
</button> </button>
<button class="action-button" :title="t('pages.plugin.pluginList')" @click="goAwesomeList"> <div class="dropdown-wrapper">
<button class="action-button" :title="t('pages.plugin.pluginList')" @click="togglePluginListMenu">
<ExternalLinkIcon :size="16" /> <ExternalLinkIcon :size="16" />
{{ t('pages.plugin.list') }} {{ t('pages.plugin.list') }}
</button> </button>
<div v-if="showPluginListMenu" class="dropdown-menu">
<button class="dropdown-item" @click="goAwesomeList">
<ExternalLinkIcon :size="16" />
{{ t('pages.plugin.openRemoteList') }}
</button>
<button class="dropdown-item" @click="openBrowsePluginsDialog">
<SearchIcon :size="16" />
{{ t('pages.plugin.browseAllPlugins') }}
</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -202,6 +214,91 @@
</div> </div>
</div> </div>
</transition> </transition>
<!-- Browse All Plugins Modal -->
<transition name="modal">
<div v-if="showBrowseDialog" class="modal-overlay" :class="advancedAnimation">
<div class="modal-container browse-modal" @click.stop>
<div class="modal-header">
<h2 class="modal-title">
{{ t('pages.plugin.browseAllPlugins') }}
</h2>
<button class="modal-close" @click="closeBrowseDialog">
<XIcon :size="20" />
</button>
</div>
<div class="modal-content browse-content">
<div class="browse-search">
<div class="search-input-wrapper">
<SearchIcon class="search-icon" :size="20" />
<input
v-model="browseSearchText"
type="text"
class="search-input"
:placeholder="t('pages.plugin.searchInBrowse')"
/>
<button v-if="browseSearchText" class="clear-button" @click="browseSearchText = ''">
<XIcon :size="16" />
</button>
</div>
</div>
<div v-if="loadingBrowse" class="browse-loading">
<div class="loading-spinner" />
<span class="loading-text">{{ t('pages.plugin.loadingPlugins') }}</span>
</div>
<div v-else class="browse-plugin-grid">
<div v-for="item in filteredBrowsePlugins" :key="item.fullName" class="browse-plugin-item">
<!-- Plugin Badge -->
<div class="plugin-header">
<img class="plugin-logo" :src="item.logo" :onerror="setSrc" alt="" />
<div class="plugin-info">
<h3 class="plugin-name" @click="openHomepage(item.homepage)">
{{ item.name }}
<span class="plugin-version">v{{ item.version }}</span>
<div v-if="!item.gui" class="cli-badge-browser">CLI</div>
</h3>
<p class="plugin-author">
{{ item.author }}
</p>
</div>
</div>
<div class="plugin-description">
<p :title="item.description">
{{ item.description }}
</p>
</div>
<div class="plugin-actions">
<template v-if="!item.hasInstall">
<button
v-if="!item.ing"
class="plugin-button install-button"
@click="installPluginFromBrowse(item)"
>
<DownloadIcon :size="16" />
{{ t('pages.plugin.install') }}
</button>
<button v-else class="plugin-button installing-button" disabled>
<div class="button-spinner" />
{{ t('pages.plugin.installing') }}
</button>
</template>
<button v-else class="plugin-button installed-button" disabled>
<CheckIcon :size="16" />
{{ t('pages.plugin.installed') }}
</button>
</div>
</div>
</div>
<div v-if="!loadingBrowse && filteredBrowsePlugins.length === 0" class="empty-content">
<PackageIcon class="empty-icon" :size="48" />
<h3>{{ t('pages.plugin.noPluginsFound') }}</h3>
<p>{{ t('pages.plugin.tryDifferentSearch') }}</p>
</div>
</div>
</div>
</div>
</transition>
</div> </div>
</template> </template>
@@ -252,6 +349,11 @@ const enableAdvancedAnimation = ref(false)
const latestVersionMap = reactive<Record<string, string>>({}) const latestVersionMap = reactive<Record<string, string>>({})
const $configForm = useTemplateRef('$configForm') const $configForm = useTemplateRef('$configForm')
const strictSearch = useStorage('plugin-strict-search', true) const strictSearch = useStorage('plugin-strict-search', true)
const showPluginListMenu = ref(false)
const showBrowseDialog = ref(false)
const browseSearchText = ref('')
const browsePlugins = ref<IPicGoPlugin[]>([])
const loadingBrowse = ref(false)
function setSrc(e: Event) { function setSrc(e: Event) {
const target = e.target as HTMLImageElement const target = e.target as HTMLImageElement
@@ -274,6 +376,21 @@ const npmSearchText = computed(() => {
: searchText.value : searchText.value
}) })
const filteredBrowsePlugins = computed(() => {
if (!browseSearchText.value) {
return browsePlugins.value
}
const search = browseSearchText.value.toLowerCase()
return browsePlugins.value.filter(plugin => {
return (
plugin.name.toLowerCase().includes(search) ||
plugin.fullName.toLowerCase().includes(search) ||
plugin.description?.toLowerCase().includes(search) ||
plugin.author?.toLowerCase().includes(search)
)
})
})
let getSearchResult: DebouncedFunc<(val: string) => void> let getSearchResult: DebouncedFunc<(val: string) => void>
watch(npmSearchText, (val: string) => { watch(npmSearchText, (val: string) => {
@@ -293,6 +410,14 @@ watch(dialogVisible, (val: boolean) => {
} }
}) })
watch(showBrowseDialog, (val: boolean) => {
if (val) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = 'auto'
}
})
async function getLatestVersionOfPlugIn(pluginName: string) { async function getLatestVersionOfPlugIn(pluginName: string) {
try { try {
const res = await fetch(`https://registry.npmjs.com/${pluginName}`) const res = await fetch(`https://registry.npmjs.com/${pluginName}`)
@@ -333,6 +458,13 @@ const installPluginHandler = ({ success, body }: { success: boolean; body: strin
item.hasInstall = success item.hasInstall = success
} }
}) })
// Update browse dialog if open
browsePlugins.value.forEach(item => {
if (item.fullName === body) {
item.ing = false
item.hasInstall = success
}
})
} }
const updateSuccessHandler = (plugin: string) => { const updateSuccessHandler = (plugin: string) => {
@@ -543,9 +675,60 @@ function openHomepage(url: string) {
} }
function goAwesomeList() { function goAwesomeList() {
showPluginListMenu.value = false
window.electron.sendRPC(IRPCActionType.OPEN_URL, 'https://github.com/PicGo/Awesome-PicGo') window.electron.sendRPC(IRPCActionType.OPEN_URL, 'https://github.com/PicGo/Awesome-PicGo')
} }
function togglePluginListMenu() {
showPluginListMenu.value = !showPluginListMenu.value
}
async function openBrowsePluginsDialog() {
showPluginListMenu.value = false
showBrowseDialog.value = true
browseSearchText.value = ''
await fetchAllPlugins()
}
function closeBrowseDialog() {
showBrowseDialog.value = false
browseSearchText.value = ''
}
async function fetchAllPlugins() {
loadingBrowse.value = true
try {
const res = await fetch('https://registry.npmjs.com/-/v1/search?text=picgo-plugin-&size=250')
const data = await res.json()
browsePlugins.value = data.objects
.filter((item: INPMSearchResultObject) => {
return item.package.name.startsWith('picgo-plugin-')
})
.map((item: INPMSearchResultObject) => {
return handleSearchResult(item)
})
.sort((a: IPicGoPlugin, b: IPicGoPlugin) => {
return b.fullName.localeCompare(a.fullName)
})
} catch (err) {
console.error('Failed to fetch plugins:', err)
} finally {
loadingBrowse.value = false
}
}
function installPluginFromBrowse(item: IPicGoPlugin) {
if (!item.gui) {
if (confirm(t('pages.plugin.notGuiImplement'))) {
item.ing = true
window.electron.sendRPC(IRPCActionType.PLUGIN_INSTALL, item.fullName)
}
} else {
item.ing = true
window.electron.sendRPC(IRPCActionType.PLUGIN_INSTALL, item.fullName)
}
}
function handleImportLocalPlugin() { function handleImportLocalPlugin() {
window.electron.sendRPC(IRPCActionType.PLUGIN_IMPORT_LOCAL) window.electron.sendRPC(IRPCActionType.PLUGIN_IMPORT_LOCAL)
loading.value = true loading.value = true
@@ -569,6 +752,14 @@ onBeforeMount(async () => {
getSearchResult = debounce(_getSearchResult, 50) getSearchResult = debounce(_getSearchResult, 50)
initConf() initConf()
needReload.value = (await getConfig<boolean>(configPaths.needReload)) || false needReload.value = (await getConfig<boolean>(configPaths.needReload)) || false
// Close dropdown menu when clicking outside
document.addEventListener('click', (e: MouseEvent) => {
const target = e.target as HTMLElement
if (!target.closest('.dropdown-wrapper')) {
showPluginListMenu.value = false
}
})
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {

View File

@@ -33,6 +33,15 @@ html, body {
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);
} }
.header-card {
overflow: visible;
border: 1px solid var(--color-border-secondary);
border-radius: var(--radius-xl);
background: var(--color-background-secondary);
box-shadow: var(--shadow-sm);
transition: var(--transition-medium);
}
/* Header Card */ /* Header Card */
.header-card .card-header { .header-card .card-header {
display: flex; display: flex;
@@ -353,8 +362,8 @@ html, body {
} }
.cli-badge { .cli-badge {
color: var(--color-blue-common); color: var(--color-text-primary);
background: var(--color-info); background: var(--color-accent);
} }
.update-badge { .update-badge {
@@ -380,6 +389,7 @@ html, body {
} }
.plugin-info { .plugin-info {
position: relative;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
@@ -575,7 +585,7 @@ html, body {
width: 100%; width: 100%;
max-width: 70vw; max-width: 70vw;
max-height: 80vh; max-height: 80vh;
background: var(--color-surface); background: var(--color-background-tertiary);
box-shadow: var(--shadow-xl); box-shadow: var(--shadow-xl);
flex-direction: column; flex-direction: column;
} }
@@ -783,3 +793,117 @@ html, body {
outline: 2px solid var(--color-accent); outline: 2px solid var(--color-accent);
outline-offset: 2px; outline-offset: 2px;
} }
/* Dropdown Menu */
.dropdown-wrapper {
position: relative;
}
.dropdown-menu {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
z-index: 1000;
min-width: 200px;
border: 1px solid var(--color-border-secondary);
border-radius: var(--radius-md);
background: var(--color-background-tertiary);
box-shadow: var(--shadow-lg);
padding: 0.5rem;
}
.dropdown-item {
display: flex;
align-items: center;
border: none;
border-radius: var(--radius-sm);
padding: 0.75rem 1rem;
width: 100%;
font-size: 0.875rem;
font-family: inherit;
font-weight: 500;
color: var(--color-text-primary);
background: transparent;
transition: var(--transition-fast);
gap: 0.5rem;
cursor: pointer;
text-align: left;
}
.dropdown-item:hover {
background: var(--color-background-hover);
color: var(--color-accent);
}
.dropdown-item svg {
flex-shrink: 0;
}
/* Browse Modal */
.browse-modal {
max-width: 900px;
max-height: 80vh;
width: 90%;
}
.browse-content {
display: flex;
max-height: 600px;
flex-direction: column;
gap: 1rem;
}
.browse-search {
flex-shrink: 0;
}
.browse-loading {
display: flex;
justify-content: center;
align-items: center;
padding: 3rem 1rem;
flex-direction: column;
gap: 1rem;
}
.browse-plugin-grid {
display: grid;
overflow-y: auto;
padding: 0.5rem;
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.browse-plugin-item {
display: flex;
border: 1px solid var(--color-border-secondary);
border-radius: var(--radius-lg);
padding: 1rem;
background: var(--color-background-primary);
flex-direction: column;
gap: 0.75rem;
transition: var(--transition-medium);
}
.browse-plugin-item:hover {
border-color: var(--color-accent);
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.browse-plugin-item .plugin-actions {
margin-top: auto;
}
.cli-badge-browser {
position: absolute;
top: 0;
right: 0;
z-index: 1;
border-radius: var(--radius-sm);
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
font-weight: 600;
color: var(--color-text-primary);
background: var(--color-accent);
}