mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-29 19:41:36 +08:00
feat(plugin): add release version install actions (#494)
This commit is contained in:
@@ -656,6 +656,8 @@ export interface Plugin {
|
||||
system_version_message?: string
|
||||
// 主系统版本限定范围
|
||||
system_version?: string
|
||||
// 是否声明支持通过 GitHub Release 资产安装
|
||||
release?: boolean
|
||||
// 是否本地插件
|
||||
is_local?: boolean
|
||||
// 插件仓库地址
|
||||
@@ -668,6 +670,38 @@ export interface Plugin {
|
||||
page_open?: boolean
|
||||
}
|
||||
|
||||
// 插件 Release 可安装版本
|
||||
export interface PluginReleaseVersion {
|
||||
// 插件版本
|
||||
version: string
|
||||
// GitHub Release tag
|
||||
tag_name: string
|
||||
// Release 标题
|
||||
name?: string
|
||||
// 发布时间
|
||||
published_at?: string
|
||||
// Release 说明
|
||||
body?: string
|
||||
// 匹配到的资产文件名
|
||||
asset_name?: string
|
||||
// 是否为当前市场最新版本
|
||||
is_latest?: boolean
|
||||
// 是否为本地已安装版本
|
||||
is_current?: boolean
|
||||
}
|
||||
|
||||
// 插件 Release 可安装版本响应
|
||||
export interface PluginReleaseVersionsResponse {
|
||||
// 当前插件是否存在可直接安装的 Release 资产
|
||||
release_supported: boolean
|
||||
// 当前市场 package 声明的最新版本
|
||||
latest_version?: string | null
|
||||
// 本地已安装版本
|
||||
current_version?: string | null
|
||||
// 可安装版本列表
|
||||
items: PluginReleaseVersion[]
|
||||
}
|
||||
|
||||
// 插件侧栏全页导航项(与后端 PluginSidebarNavItem 对齐)
|
||||
export interface PluginSidebarNavItem {
|
||||
plugin_id: string
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import type { Plugin } from '@/api/types'
|
||||
import { getLogoUrl } from '@/utils/imageUtils'
|
||||
import { getDominantColor } from '@/@core/utils/image'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import { formatDownloadCount } from '@/@core/utils/formatters'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { openSharedDialog } from '@/composables/useSharedDialog'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
|
||||
const PluginMarketDetailDialog = defineAsyncComponent(() => import('@/components/dialog/PluginMarketDetailDialog.vue'))
|
||||
const PluginVersionHistoryDialog = defineAsyncComponent(
|
||||
() => import('@/components/dialog/PluginVersionHistoryDialog.vue'),
|
||||
)
|
||||
const ProgressDialog = defineAsyncComponent(() => import('@/components/dialog/ProgressDialog.vue'))
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -26,6 +30,11 @@ const emit = defineEmits(['install'])
|
||||
// 多语言
|
||||
const { t } = useI18n()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 背景颜色
|
||||
const backgroundColor = ref('#28A9E1')
|
||||
|
||||
@@ -48,6 +57,21 @@ const isImageLoaded = ref(false)
|
||||
// 图片是否加载失败
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
let progressDialogController: ReturnType<typeof openSharedDialog> | null = null
|
||||
let versionHistoryDialogController: ReturnType<typeof openSharedDialog> | null = null
|
||||
|
||||
/** 打开插件安装进度弹窗。 */
|
||||
function showInstallProgress(text: string) {
|
||||
progressDialogController?.close()
|
||||
progressDialogController = openSharedDialog(ProgressDialog, { text }, {}, { closeOn: false })
|
||||
}
|
||||
|
||||
/** 关闭插件安装进度弹窗。 */
|
||||
function closeInstallProgress() {
|
||||
progressDialogController?.close()
|
||||
progressDialogController = null
|
||||
}
|
||||
|
||||
// 图片加载完成
|
||||
async function imageLoaded() {
|
||||
isImageLoaded.value = true
|
||||
@@ -96,14 +120,69 @@ function visitPluginPage() {
|
||||
|
||||
// 显示更新日志
|
||||
function showUpdateHistory() {
|
||||
openSharedDialog(
|
||||
versionHistoryDialogController?.close()
|
||||
versionHistoryDialogController = openSharedDialog(
|
||||
PluginVersionHistoryDialog,
|
||||
{ plugin: props.plugin },
|
||||
{},
|
||||
{ plugin: props.plugin, actionMode: 'install' },
|
||||
{
|
||||
update: installPlugin,
|
||||
},
|
||||
{ closeOn: ['close', 'update:modelValue'] },
|
||||
)
|
||||
}
|
||||
|
||||
/** 从插件市场版本历史安装指定 Release;最新版本走普通安装路径以保留主程序兼容校验。 */
|
||||
async function installPlugin(releaseVersion?: string, repoUrl?: string) {
|
||||
if (!releaseVersion && props.plugin?.system_version_compatible === false) {
|
||||
$toast.error(props.plugin?.system_version_message || t('plugin.incompatibleSystemVersion'))
|
||||
return
|
||||
}
|
||||
|
||||
if (releaseVersion) {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: t('common.confirm'),
|
||||
content: t('plugin.confirmInstallOldRelease', {
|
||||
name: props.plugin?.plugin_name,
|
||||
version: releaseVersion,
|
||||
}),
|
||||
confirmText: t('common.confirm'),
|
||||
})
|
||||
|
||||
if (!isConfirmed) return
|
||||
}
|
||||
|
||||
try {
|
||||
showInstallProgress(
|
||||
t('plugin.installing', {
|
||||
name: props.plugin?.plugin_name,
|
||||
version: releaseVersion || props.plugin?.plugin_version,
|
||||
}),
|
||||
)
|
||||
|
||||
const result: { [key: string]: any } = await api.get(`plugin/install/${props.plugin?.id}`, {
|
||||
params: {
|
||||
repo_url: repoUrl || props.plugin?.repo_url,
|
||||
release_version: releaseVersion,
|
||||
force: props.plugin?.has_update || Boolean(releaseVersion),
|
||||
},
|
||||
})
|
||||
|
||||
closeInstallProgress()
|
||||
|
||||
if (result.success) {
|
||||
$toast.success(t('plugin.installSuccess', { name: props.plugin?.plugin_name }))
|
||||
versionHistoryDialogController?.close()
|
||||
versionHistoryDialogController = null
|
||||
emit('install')
|
||||
} else {
|
||||
$toast.error(t('plugin.installFailed', { name: props.plugin?.plugin_name, message: result.message }))
|
||||
}
|
||||
} catch (error) {
|
||||
closeInstallProgress()
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开共享插件市场详情弹窗。 */
|
||||
function showPluginDetail() {
|
||||
openSharedDialog(
|
||||
@@ -140,6 +219,11 @@ const dropdownItems = ref([
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
onUnmounted(() => {
|
||||
closeInstallProgress()
|
||||
versionHistoryDialogController?.close()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -69,6 +69,7 @@ const imageLoadError = ref(false)
|
||||
|
||||
let progressDialogController: ReturnType<typeof openSharedDialog> | null = null
|
||||
let cloneDialogController: ReturnType<typeof openSharedDialog> | null = null
|
||||
let versionHistoryDialogController: ReturnType<typeof openSharedDialog> | null = null
|
||||
|
||||
/** 打开插件操作进度弹窗,插件卡片自身不再持有进度弹窗实例。 */
|
||||
function showPluginProgress(text: string) {
|
||||
@@ -103,11 +104,12 @@ async function imageLoaded() {
|
||||
|
||||
// 显示更新日志
|
||||
function showUpdateHistory(showUpdateAction: boolean = false) {
|
||||
openSharedDialog(
|
||||
versionHistoryDialogController?.close()
|
||||
versionHistoryDialogController = openSharedDialog(
|
||||
PluginVersionHistoryDialog,
|
||||
{ plugin: props.plugin, showUpdateAction },
|
||||
{ update: updatePlugin },
|
||||
{ closeOn: ['close', 'update', 'update:modelValue'] },
|
||||
{ closeOn: ['close', 'update:modelValue'] },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -219,19 +221,37 @@ async function resetPlugin() {
|
||||
}
|
||||
|
||||
// 更新插件
|
||||
async function updatePlugin() {
|
||||
if (props.plugin?.system_version_compatible === false) {
|
||||
async function updatePlugin(releaseVersion?: string, repoUrl?: string) {
|
||||
if (!releaseVersion && props.plugin?.system_version_compatible === false) {
|
||||
$toast.error(props.plugin?.system_version_message || t('plugin.incompatibleSystemVersion'))
|
||||
return
|
||||
}
|
||||
|
||||
if (releaseVersion) {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: t('common.confirm'),
|
||||
content: t('plugin.confirmInstallOldRelease', {
|
||||
name: props.plugin?.plugin_name,
|
||||
version: releaseVersion,
|
||||
}),
|
||||
confirmText: t('common.confirm'),
|
||||
})
|
||||
|
||||
if (!isConfirmed) return
|
||||
}
|
||||
|
||||
try {
|
||||
// 显示等待提示框
|
||||
showPluginProgress(t('plugin.updating', { name: props.plugin?.plugin_name }))
|
||||
showPluginProgress(
|
||||
releaseVersion
|
||||
? t('plugin.installing', { name: props.plugin?.plugin_name, version: releaseVersion })
|
||||
: t('plugin.updating', { name: props.plugin?.plugin_name }),
|
||||
)
|
||||
|
||||
const result: { [key: string]: any } = await api.get(`plugin/install/${props.plugin?.id}`, {
|
||||
params: {
|
||||
repo_url: props.plugin?.repo_url,
|
||||
repo_url: repoUrl || props.plugin?.repo_url,
|
||||
release_version: releaseVersion,
|
||||
force: true,
|
||||
},
|
||||
})
|
||||
@@ -241,6 +261,8 @@ async function updatePlugin() {
|
||||
|
||||
if (result.success) {
|
||||
$toast.success(t('plugin.updateSuccess', { name: props.plugin?.plugin_name }))
|
||||
versionHistoryDialogController?.close()
|
||||
versionHistoryDialogController = null
|
||||
|
||||
// 通知父组件刷新
|
||||
emit('save')
|
||||
|
||||
@@ -6,8 +6,12 @@ import { getLogoUrl } from '@/utils/imageUtils'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { openSharedDialog } from '@/composables/useSharedDialog'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
|
||||
const ProgressDialog = defineAsyncComponent(() => import('@/components/dialog/ProgressDialog.vue'))
|
||||
const PluginVersionHistoryDialog = defineAsyncComponent(
|
||||
() => import('@/components/dialog/PluginVersionHistoryDialog.vue'),
|
||||
)
|
||||
|
||||
// 多语言
|
||||
const { t } = useI18n()
|
||||
@@ -15,6 +19,8 @@ const { t } = useI18n()
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -47,6 +53,7 @@ const imageRef = ref<any>()
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
let progressDialogController: ReturnType<typeof openSharedDialog> | null = null
|
||||
let versionHistoryDialogController: ReturnType<typeof openSharedDialog> | null = null
|
||||
|
||||
/** 打开插件安装进度弹窗。 */
|
||||
function showInstallProgress(text: string) {
|
||||
@@ -97,24 +104,38 @@ function visitPluginPage() {
|
||||
}
|
||||
|
||||
/** 安装插件并通知父级刷新市场列表。 */
|
||||
async function installPlugin() {
|
||||
if (props.plugin?.system_version_compatible === false) {
|
||||
async function installPlugin(releaseVersion?: string, repoUrl?: string) {
|
||||
if (!releaseVersion && props.plugin?.system_version_compatible === false) {
|
||||
$toast.error(props.plugin?.system_version_message || t('plugin.incompatibleSystemVersion'))
|
||||
return
|
||||
}
|
||||
|
||||
if (releaseVersion) {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: t('common.confirm'),
|
||||
content: t('plugin.confirmInstallOldRelease', {
|
||||
name: props.plugin?.plugin_name,
|
||||
version: releaseVersion,
|
||||
}),
|
||||
confirmText: t('common.confirm'),
|
||||
})
|
||||
|
||||
if (!isConfirmed) return
|
||||
}
|
||||
|
||||
try {
|
||||
showInstallProgress(
|
||||
t('plugin.installing', {
|
||||
name: props.plugin?.plugin_name,
|
||||
version: props?.plugin?.plugin_version,
|
||||
version: releaseVersion || props?.plugin?.plugin_version,
|
||||
}),
|
||||
)
|
||||
|
||||
const result: { [key: string]: any } = await api.get(`plugin/install/${props.plugin?.id}`, {
|
||||
params: {
|
||||
repo_url: props.plugin?.repo_url,
|
||||
force: props.plugin?.has_update,
|
||||
repo_url: repoUrl || props.plugin?.repo_url,
|
||||
release_version: releaseVersion,
|
||||
force: props.plugin?.has_update || Boolean(releaseVersion),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -122,6 +143,8 @@ async function installPlugin() {
|
||||
|
||||
if (result.success) {
|
||||
$toast.success(t('plugin.installSuccess', { name: props.plugin?.plugin_name }))
|
||||
versionHistoryDialogController?.close()
|
||||
versionHistoryDialogController = null
|
||||
visible.value = false
|
||||
emit('install')
|
||||
} else {
|
||||
@@ -133,8 +156,22 @@ async function installPlugin() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开版本历史并支持从 Release 资产安装指定版本。 */
|
||||
function showUpdateHistory() {
|
||||
versionHistoryDialogController?.close()
|
||||
versionHistoryDialogController = openSharedDialog(
|
||||
PluginVersionHistoryDialog,
|
||||
{ plugin: props.plugin, actionMode: 'install' },
|
||||
{
|
||||
update: installPlugin,
|
||||
},
|
||||
{ closeOn: ['close', 'update:modelValue'] },
|
||||
)
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
closeInstallProgress()
|
||||
versionHistoryDialogController?.close()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -191,9 +228,17 @@ onUnmounted(() => {
|
||||
:text="props.plugin?.system_version_message || t('plugin.incompatibleSystemVersion')"
|
||||
/>
|
||||
<div class="text-center text-md-left">
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="showUpdateHistory"
|
||||
prepend-icon="mdi-update"
|
||||
class="me-2"
|
||||
>
|
||||
{{ t('plugin.versionHistory') }}
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="primary"
|
||||
@click="installPlugin"
|
||||
@click="installPlugin()"
|
||||
prepend-icon="mdi-download"
|
||||
:disabled="props.plugin?.system_version_compatible === false"
|
||||
>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import type { Plugin } from '@/api/types'
|
||||
import type { Plugin, PluginReleaseVersion, PluginReleaseVersionsResponse } from '@/api/types'
|
||||
import VersionHistory from '@/components/misc/VersionHistory.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 多语言
|
||||
const { t } = useI18n()
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -21,14 +21,25 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
actionMode: {
|
||||
type: String as PropType<'install' | 'update'>,
|
||||
default: 'update',
|
||||
},
|
||||
})
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['update:modelValue', 'close', 'update'])
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: boolean): void
|
||||
(event: 'close'): void
|
||||
(event: 'update', releaseVersion?: string, repoUrl?: string): void
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const loadError = ref('')
|
||||
const pluginDetail = ref<Plugin | null>(null)
|
||||
const releaseLoading = ref(false)
|
||||
const releaseError = ref('')
|
||||
const releaseDetail = ref<PluginReleaseVersionsResponse | null>(null)
|
||||
|
||||
// 弹窗显示状态
|
||||
const visible = computed({
|
||||
@@ -41,19 +52,66 @@ const visible = computed({
|
||||
|
||||
const resolvedPlugin = computed(() => pluginDetail.value ?? props.plugin)
|
||||
|
||||
const resolvedHistory = computed(() => resolvedPlugin.value?.history || {})
|
||||
const resolvedHistory = computed(() => {
|
||||
const history = { ...(resolvedPlugin.value?.history || {}) }
|
||||
releaseItems.value.forEach(item => {
|
||||
const key = normalizeHistoryVersion(item.version)
|
||||
if (!(key in history)) history[key] = item.body || ''
|
||||
})
|
||||
return history
|
||||
})
|
||||
|
||||
const hasHistory = computed(() => Object.keys(resolvedHistory.value).length > 0)
|
||||
|
||||
const latestActionText = computed(() => props.actionMode === 'install' ? t('plugin.installReleaseVersion') : t('plugin.updateToLatest'))
|
||||
|
||||
const releaseItems = computed(() => releaseDetail.value?.items || [])
|
||||
|
||||
const releaseByHistoryVersion = computed(() => {
|
||||
const releaseMap = new Map<string, PluginReleaseVersion>()
|
||||
releaseItems.value.forEach(item => {
|
||||
releaseMap.set(normalizeHistoryVersion(item.version), item)
|
||||
})
|
||||
return releaseMap
|
||||
})
|
||||
|
||||
function normalizeHistoryVersion(version: string) {
|
||||
return version.startsWith('v') ? version : `v${version}`
|
||||
}
|
||||
|
||||
function formatReleaseDate(value?: string) {
|
||||
if (!value) return ''
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
return date.toLocaleDateString(locale.value)
|
||||
}
|
||||
|
||||
function releaseItemByHistoryVersion(version: string) {
|
||||
return releaseByHistoryVersion.value.get(version)
|
||||
}
|
||||
|
||||
async function loadPluginHistory() {
|
||||
if (!props.plugin?.id) {
|
||||
pluginDetail.value = null
|
||||
loadError.value = ''
|
||||
releaseDetail.value = null
|
||||
releaseError.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
loadError.value = ''
|
||||
releaseDetail.value = null
|
||||
releaseError.value = ''
|
||||
|
||||
// 插件市场条目已经携带远端信息;history 接口只查询已安装插件,
|
||||
// 未安装插件打开版本历史时只能基于传入的市场数据和 Release 列表展示。
|
||||
if (props.actionMode === 'install' && props.plugin?.repo_url) {
|
||||
pluginDetail.value = null
|
||||
loading.value = false
|
||||
loadPluginReleases(props.plugin, false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
pluginDetail.value = await api.get(`plugin/history/${props.plugin.id}`, {
|
||||
@@ -61,6 +119,7 @@ async function loadPluginHistory() {
|
||||
force: true,
|
||||
},
|
||||
})
|
||||
loadPluginReleases(pluginDetail.value ?? props.plugin, true)
|
||||
} catch (error) {
|
||||
pluginDetail.value = null
|
||||
loadError.value = t('plugin.updateHistoryLoadFailed')
|
||||
@@ -70,35 +129,107 @@ async function loadPluginHistory() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPluginReleases(plugin: Plugin | null | undefined = resolvedPlugin.value, force = false) {
|
||||
if (!plugin?.id || !plugin?.repo_url) {
|
||||
releaseDetail.value = null
|
||||
releaseError.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
releaseLoading.value = true
|
||||
releaseError.value = ''
|
||||
|
||||
try {
|
||||
releaseDetail.value = await api.get(`plugin/releases/${plugin.id}`, {
|
||||
params: {
|
||||
repo_url: plugin.repo_url,
|
||||
force,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
releaseDetail.value = null
|
||||
releaseError.value = t('plugin.releaseVersionsLoadFailed')
|
||||
console.error(error)
|
||||
} finally {
|
||||
releaseLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 触发插件更新操作。 */
|
||||
function handleUpdate() {
|
||||
emit('update')
|
||||
function handleUpdate(releaseItem?: PluginReleaseVersion) {
|
||||
emit('update', releaseItem?.is_latest ? undefined : releaseItem?.version, resolvedPlugin.value?.repo_url)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [visible.value, props.plugin?.id],
|
||||
([isVisible]) => {
|
||||
if (isVisible) loadPluginHistory()
|
||||
if (isVisible) {
|
||||
loadPluginHistory()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog v-if="visible" v-model="visible" width="600" max-height="85vh" scrollable>
|
||||
<VDialog v-if="visible" v-model="visible" width="680" max-height="85vh" scrollable>
|
||||
<VCard :title="t('plugin.updateHistoryTitle', { name: resolvedPlugin?.plugin_name })">
|
||||
<VDialogCloseBtn v-model="visible" />
|
||||
<VDivider />
|
||||
<VProgressLinear v-if="releaseLoading && !loading" indeterminate color="primary" height="2" />
|
||||
<div v-if="loading" class="plugin-version-history-dialog__loading">
|
||||
<VProgressCircular indeterminate color="primary" />
|
||||
</div>
|
||||
<VCardText v-else-if="loadError && !hasHistory">
|
||||
<VAlert type="warning" variant="tonal" density="compact" :text="loadError" />
|
||||
</VCardText>
|
||||
<VCardText v-else-if="!hasHistory">
|
||||
<VCardText v-else-if="!hasHistory && !releaseLoading">
|
||||
<VAlert type="info" variant="tonal" density="compact" :text="t('plugin.updateHistoryEmpty')" />
|
||||
</VCardText>
|
||||
<VersionHistory v-else :history="resolvedHistory" />
|
||||
<template v-else>
|
||||
<VCardText v-if="releaseError" class="pb-0">
|
||||
<VAlert type="warning" variant="tonal" density="compact" :text="releaseError" />
|
||||
</VCardText>
|
||||
<VersionHistory :history="resolvedHistory">
|
||||
<template #action="{ version }">
|
||||
<div class="plugin-release-action" v-if="releaseItemByHistoryVersion(version)">
|
||||
<template v-if="releaseItemByHistoryVersion(version)">
|
||||
<div class="plugin-release-action__meta">
|
||||
<VChip v-if="releaseItemByHistoryVersion(version)?.is_current" size="x-small" color="success" variant="tonal">
|
||||
{{ t('plugin.currentVersion') }}
|
||||
</VChip>
|
||||
<VChip v-if="releaseItemByHistoryVersion(version)?.is_latest" size="x-small" color="primary" variant="tonal">
|
||||
{{ t('plugin.latestVersion') }}
|
||||
</VChip>
|
||||
<span v-if="formatReleaseDate(releaseItemByHistoryVersion(version)?.published_at)" class="text-caption text-medium-emphasis">
|
||||
{{ formatReleaseDate(releaseItemByHistoryVersion(version)?.published_at) }}
|
||||
</span>
|
||||
</div>
|
||||
<VBtn
|
||||
size="small"
|
||||
min-width="5.5rem"
|
||||
:block="$vuetify.display.xs"
|
||||
:color="releaseItemByHistoryVersion(version)?.is_latest ? 'primary' : undefined"
|
||||
:variant="releaseItemByHistoryVersion(version)?.is_latest ? 'flat' : 'tonal'"
|
||||
:disabled="
|
||||
releaseItemByHistoryVersion(version)?.is_current ||
|
||||
(releaseItemByHistoryVersion(version)?.is_latest && resolvedPlugin?.system_version_compatible === false)
|
||||
"
|
||||
@click.stop="handleUpdate(releaseItemByHistoryVersion(version))"
|
||||
>
|
||||
{{
|
||||
releaseItemByHistoryVersion(version)?.is_current
|
||||
? t('plugin.installed')
|
||||
: releaseItemByHistoryVersion(version)?.is_latest
|
||||
? latestActionText
|
||||
: t('plugin.installReleaseVersion')
|
||||
}}
|
||||
</VBtn>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</VersionHistory>
|
||||
</template>
|
||||
<template v-if="props.showUpdateAction">
|
||||
<VDivider />
|
||||
<VCardItem>
|
||||
@@ -110,7 +241,7 @@ watch(
|
||||
class="mb-3"
|
||||
:text="resolvedPlugin?.system_version_message || t('plugin.incompatibleSystemVersion')"
|
||||
/>
|
||||
<VBtn @click="handleUpdate" block :disabled="resolvedPlugin?.system_version_compatible === false">
|
||||
<VBtn @click="handleUpdate()" block :disabled="resolvedPlugin?.system_version_compatible === false">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-arrow-up-circle-outline" />
|
||||
</template>
|
||||
@@ -129,4 +260,33 @@ watch(
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.plugin-release-action,
|
||||
.plugin-release-action__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.plugin-release-action {
|
||||
justify-content: flex-end;
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
.plugin-release-action__meta {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.plugin-release-action,
|
||||
.plugin-release-action__meta {
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.plugin-release-action {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -31,19 +31,39 @@ const props = defineProps({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCardText>
|
||||
<VList>
|
||||
<VListItem v-for="(value, key) in props.history" :key="key">
|
||||
<VCardText class="version-history">
|
||||
<VList bg-color="transparent" class="version-history__list">
|
||||
<VListItem v-for="(value, key) in props.history" :key="key" class="version-history__item">
|
||||
<VListItemTitle class="font-bold text-lg">
|
||||
{{ key }}
|
||||
</VListItemTitle>
|
||||
<div class="markdown-body text-gray-500" v-html="renderMarkdown(value)" />
|
||||
<template v-if="$slots.action" #append>
|
||||
<slot name="action" :version="String(key)" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.version-history {
|
||||
padding-block: 1rem;
|
||||
}
|
||||
|
||||
.version-history__list {
|
||||
padding-block: 0;
|
||||
}
|
||||
|
||||
.version-history__item {
|
||||
align-items: start;
|
||||
padding-block: 0.75rem;
|
||||
}
|
||||
|
||||
.version-history__item + .version-history__item {
|
||||
border-block-start: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
|
||||
.markdown-body :deep(h1),
|
||||
.markdown-body :deep(h2),
|
||||
.markdown-body :deep(h3) {
|
||||
@@ -112,4 +132,25 @@ const props = defineProps({
|
||||
border-inline-start: 3px solid rgba(127, 127, 127, 0.4);
|
||||
color: rgba(127, 127, 127, 0.8);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.version-history {
|
||||
padding-inline: 0.75rem;
|
||||
}
|
||||
|
||||
.version-history__item {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
:deep(.version-history__item .v-list-item__append) {
|
||||
align-self: stretch;
|
||||
margin-inline-start: 0;
|
||||
padding-inline-start: 0;
|
||||
padding-block-start: 0.75rem;
|
||||
}
|
||||
|
||||
:deep(.version-history__item .v-list-item__content) {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3036,6 +3036,12 @@ export default {
|
||||
projectHome: 'Project Home',
|
||||
updateHistory: 'Update History',
|
||||
versionHistory: 'Version History',
|
||||
releaseVersionsLoadFailed: 'Failed to load Release versions',
|
||||
latestVersion: 'Latest',
|
||||
currentVersion: 'Current',
|
||||
installReleaseVersion: 'Install',
|
||||
confirmInstallOldRelease:
|
||||
'Install {name} v{version}? This version has no MoviePilot compatibility metadata and may fail to load or run.',
|
||||
local: 'Local',
|
||||
systemVersion: 'System Version',
|
||||
incompatibleSystemVersion: 'The current MoviePilot version does not meet this plugin requirement.',
|
||||
|
||||
@@ -2986,6 +2986,12 @@ export default {
|
||||
projectHome: '项目主页',
|
||||
updateHistory: '更新说明',
|
||||
versionHistory: '版本历史',
|
||||
releaseVersionsLoadFailed: 'Release 版本加载失败',
|
||||
latestVersion: '最新',
|
||||
currentVersion: '当前',
|
||||
installReleaseVersion: '安装',
|
||||
confirmInstallOldRelease:
|
||||
'是否确认安装 {name} v{version}?该版本缺少主程序兼容元数据,安装后可能无法加载或运行异常。',
|
||||
local: '本地',
|
||||
systemVersion: '系统版本',
|
||||
incompatibleSystemVersion: '当前 MoviePilot 版本不满足插件要求,无法安装',
|
||||
|
||||
@@ -2987,6 +2987,12 @@ export default {
|
||||
projectHome: '項目主頁',
|
||||
updateHistory: '更新說明',
|
||||
versionHistory: '版本歷史',
|
||||
releaseVersionsLoadFailed: 'Release 版本載入失敗',
|
||||
latestVersion: '最新',
|
||||
currentVersion: '當前',
|
||||
installReleaseVersion: '安裝',
|
||||
confirmInstallOldRelease:
|
||||
'是否確認安裝 {name} v{version}?該版本缺少主程序兼容元數據,安裝後可能無法載入或運行異常。',
|
||||
local: '本地',
|
||||
installToLocal: '安裝到本地',
|
||||
totalDownloads: '共 {count} 次下載',
|
||||
|
||||
Reference in New Issue
Block a user