mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-05-06 20:42:57 +08:00
✨ Feature(custom): optimize plugin page
This commit is contained in:
@@ -1011,10 +1011,10 @@ import type {
|
|||||||
import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, ref, toRaw, useTemplateRef, watch } from 'vue'
|
import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, ref, toRaw, useTemplateRef, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import customRadioOption from '@/components/common/customRadioOption.vue'
|
import customRadioOption from '@/components/common/CustomRadioOption.vue'
|
||||||
import customRange from '@/components/common/customRange.vue'
|
import customRange from '@/components/common/CustomRange.vue'
|
||||||
import customSwitch from '@/components/common/customSwitch.vue'
|
import customSwitch from '@/components/common/CustomSwitch.vue'
|
||||||
import placeholderTable from '@/components/common/placeholderTable.vue'
|
import placeholderTable from '@/components/common/PlaceholderTable.vue'
|
||||||
import PerPicbedSetting from '@/components/PerPicbedSetting.vue'
|
import PerPicbedSetting from '@/components/PerPicbedSetting.vue'
|
||||||
import { getRawData } from '@/utils/common'
|
import { getRawData } from '@/utils/common'
|
||||||
import { configPaths } from '@/utils/configPaths'
|
import { configPaths } from '@/utils/configPaths'
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
class="flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border border-border bg-surface-elevated text-secondary transition-all duration-fast ease-apple hover:scale-105 hover:border-danger hover:bg-danger hover:text-white focus-visible:focus-ring"
|
class="flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border border-border bg-surface-elevated text-secondary transition-all duration-fast ease-apple hover:scale-105 hover:border-danger hover:bg-danger hover:text-white focus-visible:focus-ring"
|
||||||
@click="visible = false"
|
@click="handleClose"
|
||||||
>
|
>
|
||||||
<XIcon :size="20" />
|
<XIcon :size="20" />
|
||||||
</button>
|
</button>
|
||||||
@@ -50,6 +50,10 @@ import { getConfig } from '@/utils/dataSender'
|
|||||||
|
|
||||||
const visible = defineModel<boolean>('visible')
|
const visible = defineModel<boolean>('visible')
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
const enableAdvancedAnimation = ref(false)
|
const enableAdvancedAnimation = ref(false)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -685,7 +685,7 @@
|
|||||||
"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",
|
"openRemoteList": "Remote List",
|
||||||
"pluginList": "Plugin List",
|
"pluginList": "Plugin List",
|
||||||
"restartApp": "Restart Application",
|
"restartApp": "Restart Application",
|
||||||
"searchInBrowse": "Search in Browse",
|
"searchInBrowse": "Search in Browse",
|
||||||
|
|||||||
@@ -685,7 +685,7 @@
|
|||||||
"noPluginsFound": "未找到插件",
|
"noPluginsFound": "未找到插件",
|
||||||
"NoPluginsInstalled": "暂无已安装插件",
|
"NoPluginsInstalled": "暂无已安装插件",
|
||||||
"notGuiImplement": "该插件未对可视化界面进行优化, 是否继续安装?",
|
"notGuiImplement": "该插件未对可视化界面进行优化, 是否继续安装?",
|
||||||
"openRemoteList": "打开远程插件列表",
|
"openRemoteList": "远程列表",
|
||||||
"pluginList": "插件列表",
|
"pluginList": "插件列表",
|
||||||
"restartApp": "重启应用",
|
"restartApp": "重启应用",
|
||||||
"searchInBrowse": "搜索插件",
|
"searchInBrowse": "搜索插件",
|
||||||
|
|||||||
@@ -685,7 +685,7 @@
|
|||||||
"noPluginsFound": "未找到插件",
|
"noPluginsFound": "未找到插件",
|
||||||
"NoPluginsInstalled": "尚未安裝任何插件",
|
"NoPluginsInstalled": "尚未安裝任何插件",
|
||||||
"notGuiImplement": "該插件未針對圖形介面進行優化,是否繼續安裝?",
|
"notGuiImplement": "該插件未針對圖形介面進行優化,是否繼續安裝?",
|
||||||
"openRemoteList": "打開遠程插件列表",
|
"openRemoteList": "遠程列表",
|
||||||
"pluginList": "插件列表",
|
"pluginList": "插件列表",
|
||||||
"restartApp": "重啟應用",
|
"restartApp": "重啟應用",
|
||||||
"searchInBrowse": "在瀏覽中搜索",
|
"searchInBrowse": "在瀏覽中搜索",
|
||||||
|
|||||||
@@ -8,9 +8,7 @@
|
|||||||
class="flex w-full items-center justify-between gap-4 rounded-2xl border border-border-secondary px-6 py-2 shadow-md max-md:items-stretch max-md:p-5"
|
class="flex w-full items-center justify-between gap-4 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 items-center gap-4">
|
<div class="flex flex-1 items-center gap-4">
|
||||||
<div class="flex items-center text-accent">
|
<ImagesIcon :size="24" class="text-accent" />
|
||||||
<ImagesIcon :size="24" />
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<h1 class="m-0 text-2xl font-semibold tracking-tight text-main">{{ t('pages.gallery.title') }}</h1>
|
<h1 class="m-0 text-2xl font-semibold tracking-tight text-main">{{ t('pages.gallery.title') }}</h1>
|
||||||
<p v-if="selectedCount > 0" class="m-0 text-sm text-secondary">
|
<p v-if="selectedCount > 0" class="m-0 text-sm text-secondary">
|
||||||
@@ -559,10 +557,10 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import { onBeforeRouteUpdate } from 'vue-router'
|
import { onBeforeRouteUpdate } from 'vue-router'
|
||||||
|
|
||||||
import ALLApi from '@/apis/allApi'
|
import ALLApi from '@/apis/allApi'
|
||||||
import CustomButton from '@/components/common/customButton.vue'
|
import CustomButton from '@/components/common/CustomButton.vue'
|
||||||
import CustomModal from '@/components/common/customModal.vue'
|
import CustomModal from '@/components/common/CustomModal.vue'
|
||||||
import MultiSelect from '@/components/common/multiSelect.vue'
|
import MultiSelect from '@/components/common/MultiSelect.vue'
|
||||||
import SingleSelect from '@/components/common/singleSelect.vue'
|
import SingleSelect from '@/components/common/SingleSelect.vue'
|
||||||
import VirtualScroller from '@/components/VirtualScroller.vue'
|
import VirtualScroller from '@/components/VirtualScroller.vue'
|
||||||
import useConfirm from '@/hooks/useConfirm'
|
import useConfirm from '@/hooks/useConfirm'
|
||||||
import { usePicBed } from '@/hooks/useGlobal'
|
import { usePicBed } from '@/hooks/useGlobal'
|
||||||
|
|||||||
@@ -311,7 +311,6 @@ function isPathActive(path: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isPicBedPathActive(type: string): boolean {
|
function isPicBedPathActive(type: string): boolean {
|
||||||
console.log('type:', type, 'route.params.type:', route.params.type)
|
|
||||||
return route.name === routerConfig.UPLOADER_CONFIG_PAGE && route.params.type === type
|
return route.name === routerConfig.UPLOADER_CONFIG_PAGE && route.params.type === type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1242,16 +1242,16 @@ import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, ref, toR
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
import CustomButton from '@/components/common/customButton.vue'
|
import CustomButton from '@/components/common/CustomButton.vue'
|
||||||
import CustomInput from '@/components/common/customInput.vue'
|
import CustomInput from '@/components/common/CustomInput.vue'
|
||||||
import CustomModal from '@/components/common/customModal.vue'
|
import CustomModal from '@/components/common/CustomModal.vue'
|
||||||
import CustomNavCard from '@/components/common/customNavCard.vue'
|
import CustomNavCard from '@/components/common/CustomNavCard.vue'
|
||||||
import CustomSelect from '@/components/common/customSelect.vue'
|
import CustomSelect from '@/components/common/CustomSelect.vue'
|
||||||
import CustomSwitch from '@/components/common/customSwitch.vue'
|
import CustomSwitch from '@/components/common/CustomSwitch.vue'
|
||||||
import MultiSelect from '@/components/common/multiSelect.vue'
|
import MultiSelect from '@/components/common/MultiSelect.vue'
|
||||||
import placeholderTable from '@/components/common/placeholderTable.vue'
|
import placeholderTable from '@/components/common/PlaceholderTable.vue'
|
||||||
import SettingCard from '@/components/common/settingCard.vue'
|
import SettingCard from '@/components/common/SettingCard.vue'
|
||||||
import SettingSection from '@/components/common/settingSection.vue'
|
import SettingSection from '@/components/common/SettingSection.vue'
|
||||||
import ImageProcessSetting from '@/components/ImageProcessSetting.vue'
|
import ImageProcessSetting from '@/components/ImageProcessSetting.vue'
|
||||||
import useConfirm from '@/hooks/useConfirm'
|
import useConfirm from '@/hooks/useConfirm'
|
||||||
import { osGlobal, usePicBed } from '@/hooks/useGlobal'
|
import { osGlobal, usePicBed } from '@/hooks/useGlobal'
|
||||||
|
|||||||
@@ -1,303 +1,410 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="plugin-container">
|
<div class="relative flex h-full w-full items-center justify-center">
|
||||||
<!-- Header Card -->
|
<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="header-card">
|
<div
|
||||||
<div class="card-header">
|
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="header-content">
|
>
|
||||||
<div class="header-icon">
|
<div class="flex flex-1 flex-wrap items-center gap-4">
|
||||||
<DatabaseIcon :size="24" />
|
<DatabaseIcon :size="24" class="text-accent" />
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<h1>{{ t('pages.plugin.title') }}</h1>
|
<h1 class="m-0 text-2xl font-semibold tracking-tight text-main">{{ t('pages.plugin.title') }}</h1>
|
||||||
<p>{{ t('pages.plugin.description') }}</p>
|
<p class="m-0 text-sm text-secondary">{{ t('pages.plugin.description') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="flex flex-wrap gap-3 overflow-visible">
|
||||||
<button
|
<CustomButton
|
||||||
class="action-button secondary"
|
type="secondary"
|
||||||
:title="t('pages.plugin.importLocal')"
|
:icon="DownloadIcon"
|
||||||
|
:text="t('pages.plugin.importLocal')"
|
||||||
@click="handleImportLocalPlugin"
|
@click="handleImportLocalPlugin"
|
||||||
|
/>
|
||||||
|
<CustomButton
|
||||||
|
type="secondary"
|
||||||
|
:icon="RefreshCwIcon"
|
||||||
|
:text="t('pages.plugin.updateAll')"
|
||||||
|
@click="handleUpdateAllPlugin"
|
||||||
|
/>
|
||||||
|
<CustomButton
|
||||||
|
type="primary"
|
||||||
|
:icon="ExternalLinkIcon"
|
||||||
|
:text="t('pages.plugin.openRemoteList')"
|
||||||
|
@click="goAwesomeList"
|
||||||
|
/>
|
||||||
|
<CustomButton
|
||||||
|
type="primary"
|
||||||
|
:icon="SearchIcon"
|
||||||
|
:text="t('pages.plugin.browseAllPlugins')"
|
||||||
|
@click="openBrowsePluginsDialog"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Card -->
|
||||||
|
<div
|
||||||
|
class="flex w-full flex-row 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="relative flex flex-1 items-center">
|
||||||
|
<SearchIcon class="absolute left-1 z-1 text-accent" :size="20" />
|
||||||
|
<input
|
||||||
|
v-model="searchText"
|
||||||
|
type="text"
|
||||||
|
class="w-full rounded-lg border border-border bg-bg-secondary px-8 py-3 text-sm text-main placeholder:text-secondary focus:border-accent focus:bg-bg-tertiary focus:shadow-md focus:outline-none"
|
||||||
|
:placeholder="t('pages.plugin.searchPlaceholder')"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="searchText"
|
||||||
|
class="absolute right-2 flex items-center rounded-full border border-border bg-transparent text-danger hover:bg-danger/10"
|
||||||
|
@click="cleanSearch"
|
||||||
>
|
>
|
||||||
<DownloadIcon :size="16" />
|
<XIcon :size="16" />
|
||||||
{{ t('pages.plugin.importLocal') }}
|
|
||||||
</button>
|
</button>
|
||||||
<button class="action-button secondary" :title="t('pages.plugin.updateAll')" @click="handleUpdateAllPlugin">
|
</div>
|
||||||
<RefreshCwIcon :size="16" />
|
<div class="flex items-center gap-2">
|
||||||
{{ t('pages.plugin.updateAll') }}
|
<label class="flex cursor-pointer flex-col gap-1 select-none">
|
||||||
</button>
|
<input v-model="strictSearch" type="checkbox" class="peer hidden" />
|
||||||
<div class="dropdown-wrapper">
|
<span
|
||||||
<button class="action-button" :title="t('pages.plugin.pluginList')" @click="togglePluginListMenu">
|
class="relative flex items-center gap-2 text-sm font-semibold text-secondary before:inline-block before:h-[16px] before:w-[16px] before:shrink-0 before:rounded-sm before:border-2 before:border-accent/50 before:bg-surface before:content-[''] peer-checked:before:border-accent peer-checked:before:bg-accent peer-checked:after:absolute peer-checked:after:top-[2px] peer-checked:after:left-[5px] peer-checked:after:h-[9px] peer-checked:after:w-[5px] peer-checked:after:rotate-45 peer-checked:after:border-r-2 peer-checked:after:border-b-2 peer-checked:after:border-white peer-checked:after:content-[''] before:hover:bg-accent"
|
||||||
<ExternalLinkIcon :size="16" />
|
>
|
||||||
{{ t('pages.plugin.list') }}
|
{{ t('pages.plugin.strictSearch') }}
|
||||||
</button>
|
</span>
|
||||||
<div v-if="showPluginListMenu" class="dropdown-menu">
|
<span class="ml-[24px] text-xs font-semibold text-secondary">{{
|
||||||
<button class="dropdown-item" @click="goAwesomeList">
|
t('pages.plugin.strictSearchDescription')
|
||||||
<ExternalLinkIcon :size="16" />
|
}}</span>
|
||||||
{{ t('pages.plugin.openRemoteList') }}
|
</label>
|
||||||
</button>
|
</div>
|
||||||
<button class="dropdown-item" @click="openBrowsePluginsDialog">
|
</div>
|
||||||
<SearchIcon :size="16" />
|
|
||||||
{{ t('pages.plugin.browseAllPlugins') }}
|
<!-- Reload Notice -->
|
||||||
</button>
|
<transition name="notice">
|
||||||
|
<div
|
||||||
|
v-if="needReload"
|
||||||
|
class="flex w-full flex-row items-center justify-center gap-4 overflow-visible rounded-2xl border border-border-secondary p-2 shadow-md max-md:items-stretch max-md:p-5"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<AlertCircleIcon class="shrink-0 text-warning" :size="22" />
|
||||||
|
<span class="flex-1 text-sm font-bold text-secondary">{{ t('pages.plugin.needRestart') }}</span>
|
||||||
|
<CustomButton
|
||||||
|
type="primary"
|
||||||
|
:icon="RefreshCwIcon"
|
||||||
|
:text="t('pages.plugin.restartApp')"
|
||||||
|
class="bg-warning/80"
|
||||||
|
@click="reloadApp"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<!-- Loading Overlay -->
|
||||||
|
<div
|
||||||
|
v-if="loading"
|
||||||
|
class="absolute inset-0 z-10 flex h-full w-full flex-col items-center justify-center rounded-xl bg-black/15"
|
||||||
|
>
|
||||||
|
<div class="mb-5 h-10 w-10 animate-spin rounded-full border-4 border-t-3 border-border border-t-accent" />
|
||||||
|
<span class="text-2xl font-bold text-white">{{ t('pages.plugin.loading') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Plugin Grid -->
|
||||||
|
<div
|
||||||
|
v-if="pluginList.length > 0 && !loading"
|
||||||
|
class="relative flex h-full w-full flex-1 items-center justify-center overflow-hidden rounded-2xl border border-border-secondary p-1 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-4 max-md:gap-4">
|
||||||
|
<div
|
||||||
|
v-for="item in pluginList"
|
||||||
|
:key="item.fullName"
|
||||||
|
class="relative flex h-auto min-h-[200px] flex-col rounded-xl border-2 border-border-secondary p-6 shadow-md transition-all duration-200 ease-apple hover:border-accent hover:shadow-xl [.disabled]:opacity-70"
|
||||||
|
:class="{ disabled: !item.enabled && !searchText }"
|
||||||
|
>
|
||||||
|
<!-- Plugin Badge -->
|
||||||
|
<div
|
||||||
|
v-if="!item.gui"
|
||||||
|
class="absolute top-4 right-4 z-1 rounded-sm bg-accent/20 px-2 py-1 text-sm font-semibold text-secondary"
|
||||||
|
>
|
||||||
|
CLI
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Update Badge -->
|
||||||
|
<div
|
||||||
|
v-if="latestVersionMap[item.fullName] && latestVersionMap[item.fullName] !== item.version"
|
||||||
|
class="absolute top-4 right-4 z-1 rounded-sm bg-success px-2 py-1 text-sm font-semibold text-white"
|
||||||
|
>
|
||||||
|
NEW
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Plugin Header -->
|
||||||
|
<div class="mb-4 flex items-start gap-4">
|
||||||
|
<img
|
||||||
|
class="h-[48px] w-[48px] shrink-0 rounded-lg object-cover"
|
||||||
|
:src="item.logo"
|
||||||
|
:onerror="setSrc"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<div class="relative min-w-0 flex-1">
|
||||||
|
<div class="flex flex-row gap-3">
|
||||||
|
<h3
|
||||||
|
class="br-3 mb-1 flex cursor-pointer items-center overflow-hidden text-base font-semibold text-ellipsis whitespace-nowrap text-main hover:text-accent"
|
||||||
|
@click="openHomepage(item.homepage)"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
<span class="rounded-sm bg-bg-tertiary px-2 py-1 text-xs font-normal text-secondary"
|
||||||
|
>v{{ item.version }}</span
|
||||||
|
>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<p class="m-0 overflow-hidden text-sm text-ellipsis whitespace-nowrap text-secondary">
|
||||||
|
{{ item.author.replace(/<.*>/, '') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Plugin Description -->
|
||||||
|
<div class="mb-6 flex flex-1 items-start">
|
||||||
|
<p
|
||||||
|
class="m-0 min-h-10 overflow-hidden text-sm leading-[1.5] font-semibold text-secondary"
|
||||||
|
:title="item.description"
|
||||||
|
>
|
||||||
|
{{ item.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Plugin Actions -->
|
||||||
|
<div class="mt-auto pt-4">
|
||||||
|
<template v-if="searchText">
|
||||||
|
<template v-if="!item.hasInstall">
|
||||||
|
<button
|
||||||
|
v-if="!item.ing"
|
||||||
|
class="flex w-full cursor-pointer items-center gap-2 rounded-md border-none bg-success/90 px-4 py-3 font-[inherit] text-sm font-semibold text-white not-disabled:hover:-translate-y-px disabled:cursor-not-allowed disabled:opacity-70"
|
||||||
|
@click="installPlugin(item)"
|
||||||
|
>
|
||||||
|
<DownloadIcon :size="16" />
|
||||||
|
{{ t('pages.plugin.install') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="flex w-full cursor-pointer items-center gap-2 rounded-md border bg-surface-elevated px-4 py-3 font-[inherit] text-sm font-semibold text-secondary not-disabled:hover:-translate-y-px disabled:cursor-not-allowed disabled:opacity-70"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="h-[16px] w-[16px] animate-spin rounded-full border-2 border-t-2 border-transparent border-t-current"
|
||||||
|
/>
|
||||||
|
{{ t('pages.plugin.installing') }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="flex w-full cursor-pointer items-center gap-2 rounded-md border border-success bg-success/30 px-4 py-3 font-[inherit] text-sm font-semibold text-secondary not-disabled:hover:-translate-y-px disabled:cursor-not-allowed disabled:opacity-70"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<CheckIcon :size="16" />
|
||||||
|
{{ t('pages.plugin.installed') }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<button
|
||||||
|
v-if="item.ing"
|
||||||
|
class="flex w-full cursor-pointer items-center gap-2 rounded-md border bg-surface-elevated px-4 py-3 font-[inherit] text-sm font-semibold text-secondary not-disabled:hover:-translate-y-px disabled:cursor-not-allowed disabled:opacity-70"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="h-[16px] w-[16px] animate-spin rounded-full border-2 border-t-2 border-transparent border-t-current"
|
||||||
|
/>
|
||||||
|
{{ t('pages.plugin.doingSomething') }}
|
||||||
|
</button>
|
||||||
|
<template v-else>
|
||||||
|
<button
|
||||||
|
v-if="item.enabled"
|
||||||
|
class="flex w-full cursor-pointer items-center gap-2 rounded-md border border-border bg-bg-secondary px-4 py-3 font-[inherit] text-sm font-semibold text-secondary not-disabled:hover:-translate-y-px disabled:cursor-not-allowed disabled:opacity-70"
|
||||||
|
@click="buildContextMenu(item)"
|
||||||
|
>
|
||||||
|
<SettingsIcon :size="16" />
|
||||||
|
{{ t('pages.plugin.settings') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="flex w-full cursor-pointer items-center gap-2 rounded-md border border-border bg-bg-secondary px-4 py-3 font-[inherit] text-sm font-semibold text-secondary not-disabled:hover:-translate-y-px not-disabled:hover:border-warning not-disabled:hover:bg-surface-elevated not-disabled:hover:text-warning disabled:cursor-not-allowed disabled:opacity-70"
|
||||||
|
@click="buildContextMenu(item)"
|
||||||
|
>
|
||||||
|
<XCircleIcon :size="16" />
|
||||||
|
{{ t('pages.plugin.disabled') }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Search Card -->
|
<!-- Empty State -->
|
||||||
<div class="plugin-card search-card">
|
|
||||||
<div class="search-container">
|
|
||||||
<div class="search-input-wrapper">
|
|
||||||
<SearchIcon class="search-icon" :size="20" />
|
|
||||||
<input
|
|
||||||
v-model="searchText"
|
|
||||||
type="text"
|
|
||||||
class="search-input"
|
|
||||||
:placeholder="t('pages.plugin.searchPlaceholder')"
|
|
||||||
/>
|
|
||||||
<button v-if="searchText" class="clear-button" @click="cleanSearch">
|
|
||||||
<XIcon :size="16" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="search-options">
|
|
||||||
<label class="strict-search-checkbox">
|
|
||||||
<input v-model="strictSearch" type="checkbox" class="checkbox-input" />
|
|
||||||
<span class="checkbox-label">{{ t('pages.plugin.strictSearch') }}</span>
|
|
||||||
<span class="checkbox-description">{{ t('pages.plugin.strictSearchDescription') }}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Reload Notice -->
|
|
||||||
<transition name="notice">
|
|
||||||
<div v-if="needReload" class="plugin-card notice-card">
|
|
||||||
<div class="notice-content">
|
|
||||||
<AlertCircleIcon class="notice-icon" :size="20" />
|
|
||||||
<span class="notice-text">{{ t('pages.plugin.needRestart') }}</span>
|
|
||||||
<button class="action-button small" @click="reloadApp">
|
|
||||||
{{ t('pages.plugin.restartApp') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
|
|
||||||
<!-- Loading Overlay -->
|
|
||||||
<div v-if="loading" class="loading-overlay">
|
|
||||||
<div class="loading-spinner" />
|
|
||||||
<span class="loading-text">{{ t('pages.plugin.loading') }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Plugin Grid -->
|
|
||||||
<div class="plugin-grid">
|
|
||||||
<div
|
<div
|
||||||
v-for="item in pluginList"
|
v-if="!loading && pluginList.length === 0"
|
||||||
:key="item.fullName"
|
class="relative flex h-full w-full flex-1 items-center justify-center overflow-hidden rounded-2xl border border-border-secondary p-1 shadow-md"
|
||||||
class="plugin-card plugin-item-card"
|
|
||||||
:class="{ disabled: !item.enabled && !searchText }"
|
|
||||||
>
|
>
|
||||||
<!-- Plugin Badge -->
|
<div class="flex flex-col items-center gap-4 text-center">
|
||||||
<div v-if="!item.gui" class="cli-badge">CLI</div>
|
<PackageIcon class="text-secondary" :size="48" />
|
||||||
|
<h3 class="m-0 text-lg font-semibold text-main">
|
||||||
<!-- Update Badge -->
|
{{ searchText ? t('pages.plugin.noPluginsFound') : t('pages.plugin.NoPluginsInstalled') }}
|
||||||
<div
|
</h3>
|
||||||
v-if="latestVersionMap[item.fullName] && latestVersionMap[item.fullName] !== item.version"
|
<p class="m-0 max-w-[400px] text-sm font-semibold text-secondary">
|
||||||
class="update-badge"
|
{{ searchText ? t('pages.plugin.tryDifferentSearch') : t('pages.plugin.installPluginsToGetStarted') }}
|
||||||
>
|
|
||||||
NEW
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Plugin Header -->
|
|
||||||
<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>
|
|
||||||
</h3>
|
|
||||||
<p class="plugin-author">
|
|
||||||
{{ item.author.replace(/<.*>/, '') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Plugin Description -->
|
|
||||||
<div class="plugin-description">
|
|
||||||
<p :title="item.description">
|
|
||||||
{{ item.description }}
|
|
||||||
</p>
|
</p>
|
||||||
|
<CustomButton
|
||||||
|
v-if="!searchText"
|
||||||
|
type="primary"
|
||||||
|
:icon="ExternalLinkIcon"
|
||||||
|
:text="t('pages.plugin.browsePlugins')"
|
||||||
|
@click="goAwesomeList"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Plugin Actions -->
|
|
||||||
<div class="plugin-actions">
|
|
||||||
<template v-if="searchText">
|
|
||||||
<template v-if="!item.hasInstall">
|
|
||||||
<button v-if="!item.ing" class="plugin-button install-button" @click="installPlugin(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>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<button v-if="item.ing" class="plugin-button processing-button" disabled>
|
|
||||||
<div class="button-spinner" />
|
|
||||||
{{ t('pages.plugin.doingSomething') }}
|
|
||||||
</button>
|
|
||||||
<template v-else>
|
|
||||||
<button v-if="item.enabled" class="plugin-button settings-button" @click="buildContextMenu(item)">
|
|
||||||
<SettingsIcon :size="16" />
|
|
||||||
{{ t('pages.plugin.settings') }}
|
|
||||||
</button>
|
|
||||||
<button v-else class="plugin-button disabled-button" @click="buildContextMenu(item)">
|
|
||||||
<XCircleIcon :size="16" />
|
|
||||||
{{ t('pages.plugin.disabled') }}
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Empty State -->
|
|
||||||
<div v-if="!loading && pluginList.length === 0" class="plugin-card empty-state">
|
|
||||||
<div class="empty-content">
|
|
||||||
<PackageIcon class="empty-icon" :size="48" />
|
|
||||||
<h3>{{ searchText ? t('pages.plugin.noPluginsFound') : t('pages.plugin.NoPluginsInstalled') }}</h3>
|
|
||||||
<p>{{ searchText ? t('pages.plugin.tryDifferentSearch') : t('pages.plugin.installPluginsToGetStarted') }}</p>
|
|
||||||
<button v-if="!searchText" class="action-button" @click="goAwesomeList">
|
|
||||||
<ExternalLinkIcon :size="16" />
|
|
||||||
{{ t('pages.plugin.browsePlugins') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Config Modal -->
|
<!-- Config Modal -->
|
||||||
<transition name="modal">
|
<transition name="modal">
|
||||||
<div v-if="dialogVisible" class="modal-overlay" :class="advancedAnimation" @click="dialogVisible = false">
|
<CustomModal
|
||||||
<div class="modal-container" @click.stop>
|
v-if="dialogVisible"
|
||||||
<div class="modal-header">
|
v-model:visible="dialogVisible"
|
||||||
<h2 class="modal-title">
|
:title="t('pages.plugin.configThing', { c: configName })"
|
||||||
{{ t('pages.plugin.configThing', { c: configName }) }}
|
width="600px"
|
||||||
</h2>
|
height="auto"
|
||||||
<button class="modal-close" @click="dialogVisible = false">
|
>
|
||||||
<XIcon :size="20" />
|
<div class="flex-1 overflow-y-auto p-4">
|
||||||
</button>
|
<config-form
|
||||||
</div>
|
:id="configName"
|
||||||
<div class="modal-content">
|
ref="$configForm"
|
||||||
<config-form
|
:config="config"
|
||||||
:id="configName"
|
:type="currentType"
|
||||||
ref="$configForm"
|
color-mode="white"
|
||||||
:config="config"
|
mode="plugin"
|
||||||
:type="currentType"
|
:show-tooltips="false"
|
||||||
color-mode="white"
|
/>
|
||||||
mode="plugin"
|
|
||||||
:show-tooltips="false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="btn btn-secondary" @click="dialogVisible = false">
|
|
||||||
{{ t('common.cancel') }}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-primary" @click="handleConfirmConfig">
|
|
||||||
{{ t('common.confirm') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<template #footer>
|
||||||
|
<CustomButton type="secondary" :text="t('common.cancel')" @click="dialogVisible = false" />
|
||||||
|
<CustomButton type="primary" :text="t('common.confirm')" @click="handleConfirmConfig" />
|
||||||
|
</template>
|
||||||
|
</CustomModal>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
<!-- Browse All Plugins Modal -->
|
<!-- Browse All Plugins Modal -->
|
||||||
<transition name="modal">
|
<transition name="modal">
|
||||||
<div v-if="showBrowseDialog" class="modal-overlay" :class="advancedAnimation">
|
<CustomModal
|
||||||
<div class="modal-container browse-modal" @click.stop>
|
v-if="showBrowseDialog"
|
||||||
<div class="modal-header">
|
v-model:visible="showBrowseDialog"
|
||||||
<h2 class="modal-title">
|
:title="t('pages.plugin.browseAllPlugins')"
|
||||||
{{ t('pages.plugin.browseAllPlugins') }}
|
>
|
||||||
</h2>
|
<div class="flex h-full w-full flex-col gap-4 p-4">
|
||||||
<button class="modal-close" @click="closeBrowseDialog">
|
<div class="shrink-0">
|
||||||
<XIcon :size="20" />
|
<div class="relative flex items-center">
|
||||||
</button>
|
<SearchIcon class="absolute left-4 z-10 text-secondary" :size="20" />
|
||||||
|
<input
|
||||||
|
v-model="browseSearchText"
|
||||||
|
type="text"
|
||||||
|
class="w-full rounded-lg border border-border bg-bg-secondary pt-3 pr-4 pb-3 pl-12 font-[inherit] text-sm text-main placeholder:text-secondary focus:border-accent focus:bg-bg-tertiary focus:shadow-md focus:outline-none"
|
||||||
|
:placeholder="t('pages.plugin.searchInBrowse')"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="browseSearchText"
|
||||||
|
class="absolute right-2 flex items-center rounded-full border border-border bg-transparent text-danger hover:bg-danger/10"
|
||||||
|
@click="browseSearchText = ''"
|
||||||
|
>
|
||||||
|
<XIcon :size="16" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-content browse-content">
|
<div v-if="loadingBrowse" class="flex flex-1 flex-col items-center justify-center gap-4 p-4">
|
||||||
<div class="browse-search">
|
<div
|
||||||
<div class="search-input-wrapper">
|
class="h-12 w-12 animate-spin rounded-full border-[3px] border-t-[3px] border-border border-t-accent"
|
||||||
<SearchIcon class="search-icon" :size="20" />
|
/>
|
||||||
<input
|
<span class="text-sm font-semibold text-accent">{{ t('pages.plugin.loadingPlugins') }}</span>
|
||||||
v-model="browseSearchText"
|
</div>
|
||||||
type="text"
|
<div v-else class="flex-1 overflow-hidden rounded-md border border-border shadow-md">
|
||||||
class="search-input"
|
<div class="grid h-full grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-4 overflow-auto p-4">
|
||||||
:placeholder="t('pages.plugin.searchInBrowse')"
|
<div
|
||||||
/>
|
v-for="item in filteredBrowsePlugins"
|
||||||
<button v-if="browseSearchText" class="clear-button" @click="browseSearchText = ''">
|
:key="item.fullName"
|
||||||
<XIcon :size="16" />
|
class="relative flex h-auto min-h-[200px] flex-col rounded-xl border-2 border-border-secondary p-6 shadow-md transition-all duration-200 ease-apple hover:border-accent hover:shadow-xl [.disabled]:opacity-70"
|
||||||
</button>
|
>
|
||||||
</div>
|
<div class="mb-4 flex items-start gap-4">
|
||||||
</div>
|
<img
|
||||||
<div v-if="loadingBrowse" class="browse-loading">
|
class="h-[48px] w-[48px] shrink-0 rounded-lg object-cover"
|
||||||
<div class="loading-spinner" />
|
:src="item.logo"
|
||||||
<span class="loading-text">{{ t('pages.plugin.loadingPlugins') }}</span>
|
:onerror="setSrc"
|
||||||
</div>
|
alt=""
|
||||||
<div v-else class="browse-plugin-grid">
|
/>
|
||||||
<div v-for="item in filteredBrowsePlugins" :key="item.fullName" class="browse-plugin-item">
|
<div class="relative min-w-0 flex-1">
|
||||||
<!-- Plugin Badge -->
|
<h3
|
||||||
|
class="br-3 mb-1 flex cursor-pointer items-center overflow-hidden text-base font-semibold text-ellipsis whitespace-nowrap text-main hover:text-accent"
|
||||||
<div class="plugin-header">
|
@click="openHomepage(item.homepage)"
|
||||||
<img class="plugin-logo" :src="item.logo" :onerror="setSrc" alt="" />
|
>
|
||||||
<div class="plugin-info">
|
|
||||||
<h3 class="plugin-name" @click="openHomepage(item.homepage)">
|
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
<span class="plugin-version">v{{ item.version }}</span>
|
<span class="rounded-sm bg-bg-tertiary px-2 py-1 text-xs font-normal text-secondary"
|
||||||
<div v-if="!item.gui" class="cli-badge-browser">CLI</div>
|
>v{{ item.version }}</span
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="!item.gui"
|
||||||
|
class="absolute top-4 right-4 z-1 rounded-sm bg-accent/20 px-2 py-1 text-sm font-semibold text-secondary"
|
||||||
|
>
|
||||||
|
CLI
|
||||||
|
</div>
|
||||||
</h3>
|
</h3>
|
||||||
<p class="plugin-author">
|
<p class="m-0 overflow-hidden text-sm text-ellipsis whitespace-nowrap text-secondary">
|
||||||
{{ item.author }}
|
{{ item.author }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="plugin-description">
|
<div class="mb-6 flex flex-1 items-start">
|
||||||
<p :title="item.description">
|
<p
|
||||||
|
class="m-0 min-h-10 overflow-hidden text-sm leading-[1.5] font-semibold text-secondary"
|
||||||
|
:title="item.description"
|
||||||
|
>
|
||||||
{{ item.description }}
|
{{ item.description }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="plugin-actions">
|
<div class="mt-auto pt-4">
|
||||||
<template v-if="!item.hasInstall">
|
<template v-if="!item.hasInstall">
|
||||||
<button
|
<button
|
||||||
v-if="!item.ing"
|
v-if="!item.ing"
|
||||||
class="plugin-button install-button"
|
class="flex w-full cursor-pointer items-center gap-2 rounded-md border-none bg-success/90 px-4 py-3 font-[inherit] text-sm font-semibold text-white not-disabled:hover:-translate-y-px disabled:cursor-not-allowed disabled:opacity-70"
|
||||||
@click="installPluginFromBrowse(item)"
|
@click="installPluginFromBrowse(item)"
|
||||||
>
|
>
|
||||||
<DownloadIcon :size="16" />
|
<DownloadIcon :size="16" />
|
||||||
{{ t('pages.plugin.install') }}
|
{{ t('pages.plugin.install') }}
|
||||||
</button>
|
</button>
|
||||||
<button v-else class="plugin-button installing-button" disabled>
|
<button
|
||||||
<div class="button-spinner" />
|
v-else
|
||||||
|
class="flex w-full cursor-pointer items-center gap-2 rounded-md border bg-surface-elevated px-4 py-3 font-[inherit] text-sm font-semibold text-secondary not-disabled:hover:-translate-y-px disabled:cursor-not-allowed disabled:opacity-70"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="h-[16px] w-[16px] animate-spin rounded-full border-2 border-t-2 border-transparent border-t-current"
|
||||||
|
/>
|
||||||
{{ t('pages.plugin.installing') }}
|
{{ t('pages.plugin.installing') }}
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<button v-else class="plugin-button installed-button" disabled>
|
<button
|
||||||
|
v-else
|
||||||
|
class="flex w-full cursor-pointer items-center gap-2 rounded-md border border-success bg-success/30 px-4 py-3 font-[inherit] text-sm font-semibold text-secondary not-disabled:hover:-translate-y-px disabled:cursor-not-allowed disabled:opacity-70"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
<CheckIcon :size="16" />
|
<CheckIcon :size="16" />
|
||||||
{{ t('pages.plugin.installed') }}
|
{{ t('pages.plugin.installed') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!loadingBrowse && filteredBrowsePlugins.length === 0" class="empty-content">
|
</div>
|
||||||
<PackageIcon class="empty-icon" :size="48" />
|
<div
|
||||||
<h3>{{ t('pages.plugin.noPluginsFound') }}</h3>
|
v-if="!loadingBrowse && filteredBrowsePlugins.length === 0"
|
||||||
<p>{{ t('pages.plugin.tryDifferentSearch') }}</p>
|
class="flex flex-col items-center gap-4 text-center"
|
||||||
</div>
|
>
|
||||||
|
<PackageIcon class="text-secondary opacity-50" :size="48" />
|
||||||
|
<h3 class="m-0 text-lg font-semibold text-main">{{ t('pages.plugin.noPluginsFound') }}</h3>
|
||||||
|
<p class="m-0 max-w-[400px] text-sm text-secondary">{{ t('pages.plugin.tryDifferentSearch') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CustomModal>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -321,6 +428,8 @@ import {
|
|||||||
import { computed, onBeforeMount, onBeforeUnmount, reactive, ref, toRaw, useTemplateRef, watch } from 'vue'
|
import { computed, onBeforeMount, onBeforeUnmount, reactive, ref, toRaw, useTemplateRef, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import CustomButton from '@/components/common/CustomButton.vue'
|
||||||
|
import CustomModal from '@/components/common/CustomModal.vue'
|
||||||
import ConfigForm from '@/components/UnifiedConfigForm.vue'
|
import ConfigForm from '@/components/UnifiedConfigForm.vue'
|
||||||
import { usePicBed } from '@/hooks/useGlobal'
|
import { usePicBed } from '@/hooks/useGlobal'
|
||||||
import { getRawData, handleStreamlinePluginName } from '@/utils/common'
|
import { getRawData, handleStreamlinePluginName } from '@/utils/common'
|
||||||
@@ -345,11 +454,9 @@ const dialogVisible = ref(false)
|
|||||||
const pluginNameList = ref<string[]>([])
|
const pluginNameList = ref<string[]>([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const needReload = ref(false)
|
const needReload = ref(false)
|
||||||
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 showBrowseDialog = ref(false)
|
||||||
const browseSearchText = ref('')
|
const browseSearchText = ref('')
|
||||||
const browsePlugins = ref<IPicGoPlugin[]>([])
|
const browsePlugins = ref<IPicGoPlugin[]>([])
|
||||||
@@ -360,14 +467,6 @@ function setSrc(e: Event) {
|
|||||||
target.src = import.meta.env.BASE_URL + 'roundLogo.png'
|
target.src = import.meta.env.BASE_URL + 'roundLogo.png'
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initConf() {
|
|
||||||
enableAdvancedAnimation.value = (await getConfig<boolean>(configPaths.settings.isCustomMiniIcon)) || false
|
|
||||||
}
|
|
||||||
|
|
||||||
const advancedAnimation = computed(() => ({
|
|
||||||
advancedAnimation: enableAdvancedAnimation.value,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const npmSearchText = computed(() => {
|
const npmSearchText = computed(() => {
|
||||||
return searchText.value.match('picgo-plugin-')
|
return searchText.value.match('picgo-plugin-')
|
||||||
? searchText.value
|
? searchText.value
|
||||||
@@ -402,14 +501,6 @@ watch(npmSearchText, (val: string) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(dialogVisible, (val: boolean) => {
|
|
||||||
if (val) {
|
|
||||||
document.body.style.overflow = 'hidden'
|
|
||||||
} else {
|
|
||||||
document.body.style.overflow = 'auto'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(showBrowseDialog, (val: boolean) => {
|
watch(showBrowseDialog, (val: boolean) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
document.body.style.overflow = 'hidden'
|
document.body.style.overflow = 'hidden'
|
||||||
@@ -675,26 +766,15 @@ 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() {
|
async function openBrowsePluginsDialog() {
|
||||||
showPluginListMenu.value = false
|
|
||||||
showBrowseDialog.value = true
|
showBrowseDialog.value = true
|
||||||
browseSearchText.value = ''
|
browseSearchText.value = ''
|
||||||
await fetchAllPlugins()
|
await fetchAllPlugins()
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeBrowseDialog() {
|
|
||||||
showBrowseDialog.value = false
|
|
||||||
browseSearchText.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchAllPlugins() {
|
async function fetchAllPlugins() {
|
||||||
loadingBrowse.value = true
|
loadingBrowse.value = true
|
||||||
try {
|
try {
|
||||||
@@ -750,16 +830,7 @@ onBeforeMount(async () => {
|
|||||||
window.electron.ipcRendererOn(PICGO_TOGGLE_PLUGIN, picgoTogglePluginHandler)
|
window.electron.ipcRendererOn(PICGO_TOGGLE_PLUGIN, picgoTogglePluginHandler)
|
||||||
getPluginList()
|
getPluginList()
|
||||||
getSearchResult = debounce(_getSearchResult, 50)
|
getSearchResult = debounce(_getSearchResult, 50)
|
||||||
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(() => {
|
||||||
@@ -783,5 +854,3 @@ export default {
|
|||||||
name: 'PluginPage',
|
name: 'PluginPage',
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped src="./css/PluginPage.css"></style>
|
|
||||||
|
|||||||
@@ -676,7 +676,7 @@ import { computed, onBeforeMount, onBeforeUnmount, reactive, ref, useTemplateRef
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
import CustomModal from '@/components/common/customModal.vue'
|
import CustomModal from '@/components/common/CustomModal.vue'
|
||||||
import ImageProcessSetting from '@/components/ImageProcessSetting.vue'
|
import ImageProcessSetting from '@/components/ImageProcessSetting.vue'
|
||||||
import { usePicBed } from '@/hooks/useGlobal'
|
import { usePicBed } from '@/hooks/useGlobal'
|
||||||
import useMessage from '@/hooks/useMessage'
|
import useMessage from '@/hooks/useMessage'
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
/* stylelint-disable selector-pseudo-class-no-unknown */
|
/* stylelint-disable selector-pseudo-class-no-unknown */
|
||||||
@import url('./common/advancedAnimation.css');
|
|
||||||
@import 'tailwindcss' reference;
|
@import 'tailwindcss' reference;
|
||||||
@import '../../assets/css/theme.css' reference;
|
@import '../../assets/css/theme.css' reference;
|
||||||
@import '../../assets/css/utilities.css' reference;
|
@import '../../assets/css/utilities.css' reference;
|
||||||
|
|||||||
@@ -1,909 +0,0 @@
|
|||||||
@import url('./common/advancedAnimation.css');
|
|
||||||
|
|
||||||
/* Global scrolling behavior */
|
|
||||||
html, body {
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Container */
|
|
||||||
.plugin-container {
|
|
||||||
display: flex;
|
|
||||||
overflow-y: auto;
|
|
||||||
margin: 0;
|
|
||||||
padding: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100vh;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.25rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Card Base */
|
|
||||||
.plugin-card {
|
|
||||||
overflow: hidden;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card:hover {
|
|
||||||
border-color: var(--color-border);
|
|
||||||
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 .card-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 1px solid var(--color-border-secondary);
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-content h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
letter-spacing: -0.025em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-content p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 0.625rem 1rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-family: inherit;
|
|
||||||
font-weight: 500;
|
|
||||||
color: white;
|
|
||||||
background: var(--color-accent);
|
|
||||||
transition: var(--transition-fast);
|
|
||||||
gap: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button:hover {
|
|
||||||
background: var(--color-accent-hover);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button.secondary {
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
background: var(--color-background-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button.secondary:hover {
|
|
||||||
border-color: var(--color-accent);
|
|
||||||
color: var(--color-accent);
|
|
||||||
background: var(--color-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button.small {
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Search Card */
|
|
||||||
.search-card {
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-container {
|
|
||||||
display: flex;
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input-wrapper {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-icon {
|
|
||||||
position: absolute;
|
|
||||||
left: 1rem;
|
|
||||||
z-index: 1;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: 0.75rem 1rem 0.75rem 3rem;
|
|
||||||
width: 100%;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-family: inherit;
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
background: var(--color-background-secondary);
|
|
||||||
transition: var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input:focus {
|
|
||||||
border-color: var(--color-accent);
|
|
||||||
background: var(--color-background-tertiary);
|
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 0 2px rgb(0 122 255 / 20%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input::placeholder {
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-button {
|
|
||||||
position: absolute;
|
|
||||||
right: 0.5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 0.5rem;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
background: transparent;
|
|
||||||
transition: var(--transition-fast);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-button:hover {
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
background: var(--color-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Search Options */
|
|
||||||
.search-options {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
border-top: 1px solid var(--color-border-secondary);
|
|
||||||
padding-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.strict-search-checkbox {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.25rem;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-label {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-label::before {
|
|
||||||
border: 2px solid var(--color-border);
|
|
||||||
border-radius: 3px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
background: var(--color-surface);
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
content: '';
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-input:checked + .checkbox-label::before {
|
|
||||||
border-color: var(--color-accent);
|
|
||||||
background: var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-input:checked + .checkbox-label::after {
|
|
||||||
position: absolute;
|
|
||||||
left: 3px;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: white;
|
|
||||||
content: '✓';
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-label:hover::before {
|
|
||||||
border-color: var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-description {
|
|
||||||
margin-left: 24px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Notice Card */
|
|
||||||
.notice-card {
|
|
||||||
border-color: var(--color-warning);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
background: linear-gradient(135deg, rgb(255 193 7 / 10%) 0%, var(--color-surface) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notice-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notice-icon {
|
|
||||||
color: var(--color-warning);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notice-text {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading */
|
|
||||||
.loading-overlay {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 100;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: var(--radius-xl);
|
|
||||||
background: rgb(0 0 0 / 50%);
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
border: 3px solid var(--color-border);
|
|
||||||
border-top: 3px solid var(--color-accent);
|
|
||||||
border-radius: var(--radius-round);
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-text {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Plugin Grid */
|
|
||||||
.plugin-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
||||||
gap: 1.25rem;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Plugin Item Card */
|
|
||||||
.plugin-item-card {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: 1.5rem;
|
|
||||||
height: auto;
|
|
||||||
min-height: 200px;
|
|
||||||
transition: var(--transition-medium);
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-item-card:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-item-card.disabled {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Plugin Badges */
|
|
||||||
.cli-badge,
|
|
||||||
.update-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: 1rem;
|
|
||||||
right: 1rem;
|
|
||||||
z-index: 1;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cli-badge {
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
background: var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-badge {
|
|
||||||
right: 3.5rem;
|
|
||||||
color: white;
|
|
||||||
background: var(--color-success);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Plugin Header */
|
|
||||||
.plugin-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-logo {
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
object-fit: cover;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-info {
|
|
||||||
position: relative;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-name {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 0 0 0.25rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
transition: var(--transition-fast);
|
|
||||||
cursor: pointer;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-name:hover {
|
|
||||||
color: var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-version {
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
padding: 0.125rem 0.375rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 400;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
background: var(--color-background-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-author {
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Plugin Description */
|
|
||||||
.plugin-description {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-description p {
|
|
||||||
display: -webkit-box;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 0;
|
|
||||||
min-height: 2.6rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
line-height: 1.5;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Plugin Actions */
|
|
||||||
.plugin-actions {
|
|
||||||
margin-top: auto;
|
|
||||||
padding-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
width: 100%;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-family: inherit;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: var(--transition-fast);
|
|
||||||
gap: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-button:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.install-button {
|
|
||||||
color: white;
|
|
||||||
background: var(--color-success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.install-button:hover:not(:disabled) {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.installing-button,
|
|
||||||
.processing-button {
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
background: var(--color-surface-elevated);
|
|
||||||
}
|
|
||||||
|
|
||||||
.installed-button {
|
|
||||||
border: 1px solid var(--color-success);
|
|
||||||
color: var(--color-success);
|
|
||||||
background: var(--color-success-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-button {
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
background: var(--color-background-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-button:hover:not(:disabled) {
|
|
||||||
background: var(--color-accent);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.disabled-button {
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
background: var(--color-background-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.disabled-button:hover:not(:disabled) {
|
|
||||||
border-color: var(--color-warning);
|
|
||||||
color: var(--color-warning);
|
|
||||||
background: var(--color-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Button Spinner */
|
|
||||||
.button-spinner {
|
|
||||||
border: 2px solid transparent;
|
|
||||||
border-top: 2px solid currentcolor;
|
|
||||||
border-radius: var(--radius-round);
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Empty State */
|
|
||||||
.empty-state {
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: 3rem 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-icon {
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-content h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-content p {
|
|
||||||
margin: 0;
|
|
||||||
max-width: 400px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal */
|
|
||||||
.modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background: rgb(0 0 0 / 50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container {
|
|
||||||
display: flex;
|
|
||||||
margin: 2rem;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-xl);
|
|
||||||
width: 100%;
|
|
||||||
max-width: 70vw;
|
|
||||||
max-height: 80vh;
|
|
||||||
background: var(--color-background-tertiary);
|
|
||||||
box-shadow: var(--shadow-xl);
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem 1.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
width: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
background: transparent;
|
|
||||||
transition: var(--transition-fast);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close:hover {
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
background: var(--color-surface-elevated);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 1.5rem;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content::-webkit-scrollbar-track {
|
|
||||||
border-radius: 3px;
|
|
||||||
background: var(--color-surface-elevated);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 3px;
|
|
||||||
background: var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
border-top: 1px solid var(--color-border-secondary);
|
|
||||||
padding: 1.5rem;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-family: inherit;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: var(--transition-fast);
|
|
||||||
gap: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover:not(:disabled) {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
color: white;
|
|
||||||
background: var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover:not(:disabled) {
|
|
||||||
background: var(--color-accent-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
background: var(--color-surface-elevated);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover:not(:disabled) {
|
|
||||||
border-color: var(--color-accent);
|
|
||||||
background: var(--color-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Transitions */
|
|
||||||
.notice-enter-active,
|
|
||||||
.notice-leave-active {
|
|
||||||
transition: all var(--transition-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notice-enter-from,
|
|
||||||
.notice-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-1rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Design */
|
|
||||||
@media (width <= 768px) {
|
|
||||||
.plugin-container {
|
|
||||||
padding: 0.75rem;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-card .card-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
justify-content: flex-start;
|
|
||||||
width: 100%;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container {
|
|
||||||
margin: 1rem;
|
|
||||||
max-width: 95vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button {
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 120px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width <= 480px) {
|
|
||||||
.plugin-container {
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-item-card {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button {
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Accessibility */
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
* {
|
|
||||||
animation-duration: 0.01ms !important;
|
|
||||||
animation-iteration-count: 1 !important;
|
|
||||||
transition-duration: 0.01ms !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Focus styles for keyboard navigation */
|
|
||||||
.action-button:focus-visible,
|
|
||||||
.plugin-button:focus-visible,
|
|
||||||
.btn:focus-visible,
|
|
||||||
.search-input:focus-visible,
|
|
||||||
.clear-button:focus-visible,
|
|
||||||
.modal-close:focus-visible {
|
|
||||||
outline: 2px solid var(--color-accent);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-name:focus-visible {
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
outline: 2px solid var(--color-accent);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user