mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-03 23:01:04 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc8d5cf931 | ||
|
|
6083887675 | ||
|
|
beb0506b0c | ||
|
|
0f906f791a | ||
|
|
7614696e61 | ||
|
|
4235d3687c |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "2.12.2",
|
||||
"version": "2.12.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"bin": "dist/service.js",
|
||||
|
||||
@@ -646,6 +646,12 @@ export interface Plugin {
|
||||
has_page?: boolean
|
||||
// 是否有新版本
|
||||
has_update?: boolean
|
||||
// 主系统版本是否兼容
|
||||
system_version_compatible?: boolean
|
||||
// 主系统版本兼容提示
|
||||
system_version_message?: string
|
||||
// 主系统版本限定范围
|
||||
system_version?: string
|
||||
// 是否本地插件
|
||||
is_local?: boolean
|
||||
// 插件仓库地址
|
||||
|
||||
@@ -28,19 +28,18 @@ function filtersChanged(value: string[]) {
|
||||
}
|
||||
|
||||
// 过滤规则下拉框
|
||||
const selectFilterOptions = ref<{ [key: string]: string }[]>([])
|
||||
|
||||
onMounted(() => {
|
||||
selectFilterOptions.value = cloneDeep(innerFilterRules)
|
||||
if (props.custom_rules) {
|
||||
console.log(props.custom_rules)
|
||||
props.custom_rules.map(rule => {
|
||||
selectFilterOptions.value.push({
|
||||
title: rule.name,
|
||||
value: rule.id,
|
||||
})
|
||||
// 同时包含内置规则与用户自定义规则;使用 computed 而非 onMounted 一次性赋值,
|
||||
// 是为了在父组件异步加载完 custom_rules 或后续新增/删除规则时,
|
||||
// 选项与已选 chip 的显示名(title)能跟随刷新,避免回退到原始 ID(如 "zhong")。
|
||||
const selectFilterOptions = computed<{ [key: string]: string }[]>(() => {
|
||||
const options = cloneDeep(innerFilterRules)
|
||||
props.custom_rules?.forEach(rule => {
|
||||
options.push({
|
||||
title: rule.name,
|
||||
value: rule.id,
|
||||
})
|
||||
}
|
||||
})
|
||||
return options
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -226,6 +226,11 @@ async function resetPlugin() {
|
||||
|
||||
// 更新插件
|
||||
async function updatePlugin() {
|
||||
if (props.plugin?.system_version_compatible === false) {
|
||||
$toast.error(props.plugin?.system_version_message || t('plugin.incompatibleSystemVersion'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 显示等待提示框
|
||||
showPluginProgress(t('plugin.updating', { name: props.plugin?.plugin_name }))
|
||||
|
||||
@@ -206,6 +206,7 @@ watch(
|
||||
passkeyList.value = []
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
@@ -98,6 +98,11 @@ function visitPluginPage() {
|
||||
|
||||
/** 安装插件并通知父级刷新市场列表。 */
|
||||
async function installPlugin() {
|
||||
if (props.plugin?.system_version_compatible === false) {
|
||||
$toast.error(props.plugin?.system_version_message || t('plugin.incompatibleSystemVersion'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
showInstallProgress(
|
||||
t('plugin.installing', {
|
||||
@@ -176,9 +181,28 @@ onUnmounted(() => {
|
||||
</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem v-if="props.plugin?.system_version" class="ps-0">
|
||||
<VListItemTitle class="text-center text-md-left">
|
||||
<span class="font-weight-medium">{{ t('plugin.systemVersion') }}:</span>
|
||||
<span class="text-body-1">{{ props.plugin?.system_version }}</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
<VAlert
|
||||
v-if="props.plugin?.system_version_compatible === false"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="mb-3"
|
||||
:text="props.plugin?.system_version_message || t('plugin.incompatibleSystemVersion')"
|
||||
/>
|
||||
<div class="text-center text-md-left">
|
||||
<VBtn color="primary" @click="installPlugin" prepend-icon="mdi-download">
|
||||
<VBtn
|
||||
color="primary"
|
||||
@click="installPlugin"
|
||||
prepend-icon="mdi-download"
|
||||
:disabled="props.plugin?.system_version_compatible === false"
|
||||
>
|
||||
{{ t('plugin.installToLocal') }}
|
||||
</VBtn>
|
||||
<div class="text-xs mt-2" v-if="props.count">
|
||||
|
||||
@@ -49,7 +49,15 @@ function handleUpdate() {
|
||||
<template v-if="props.showUpdateAction">
|
||||
<VDivider />
|
||||
<VCardItem>
|
||||
<VBtn @click="handleUpdate" block>
|
||||
<VAlert
|
||||
v-if="props.plugin?.system_version_compatible === false"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="mb-3"
|
||||
:text="props.plugin?.system_version_message || t('plugin.incompatibleSystemVersion')"
|
||||
/>
|
||||
<VBtn @click="handleUpdate" block :disabled="props.plugin?.system_version_compatible === false">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-arrow-up-circle-outline" />
|
||||
</template>
|
||||
|
||||
@@ -1559,15 +1559,19 @@ export default {
|
||||
'Can improve read/write concurrency performance, but may increase the risk of data loss in exceptional cases, requires restart to take effect',
|
||||
tmdbApiDomain: 'TMDB API Service Address',
|
||||
tmdbApiDomainPlaceholder: 'api.themoviedb.org',
|
||||
tmdbApiDomainHint: 'Customize themoviedb API domain or proxy address',
|
||||
tmdbApiDomainHint: 'Customize TheMovieDb API domain or proxy address',
|
||||
tmdbApiDomainRequired: 'Please enter TMDB API domain',
|
||||
tmdbApiKey: 'TMDB API Key',
|
||||
tmdbApiKeyPlaceholder: 'Please enter TMDB API Key',
|
||||
tmdbApiKeyHint: 'Set TheMovieDb API Key',
|
||||
tmdbApiKeyRequired: 'Please enter TMDB API Key',
|
||||
tmdbImageDomain: 'TMDB Image Service Address',
|
||||
tmdbImageDomainPlaceholder: 'image.tmdb.org',
|
||||
tmdbImageDomainHint: 'Customize themoviedb image service domain or proxy address',
|
||||
tmdbImageDomainHint: 'Customize TheMovieDb image service domain or proxy address',
|
||||
tmdbImageDomainRequired: 'Please enter image service domain',
|
||||
tmdbLocale: 'TMDB Metadata Language',
|
||||
tmdbLocalePlaceholder: 'en',
|
||||
tmdbLocaleHint: 'Customize themoviedb metadata language',
|
||||
tmdbLocaleHint: 'Customize TheMovieDb metadata language',
|
||||
metaCacheExpire: 'Media Metadata Cache Expiration Time',
|
||||
metaCacheExpireHint: 'Recognition metadata local cache time, use built-in default value when set to 0',
|
||||
metaCacheExpireRequired: 'Please enter metadata cache time',
|
||||
@@ -1576,7 +1580,7 @@ export default {
|
||||
scrapFollowTmdbHint:
|
||||
'When turned off, organization history will be used (if available) to avoid TMDB data changes during subscription',
|
||||
scrapOriginalImage: 'Scrap TheMovieDb Original Language Image',
|
||||
scrapOriginalImageHint: 'Scrap original language image from themoviedb, otherwise scrap metadata language image',
|
||||
scrapOriginalImageHint: 'Scrap original language image from TheMovieDb, otherwise scrap metadata language image',
|
||||
fanartEnable: 'Fanart Image Data Source',
|
||||
fanartEnableHint: 'Use image data from fanart.tv',
|
||||
fanartLang: 'Fanart Language',
|
||||
@@ -2838,6 +2842,8 @@ export default {
|
||||
projectHome: 'Project Home',
|
||||
updateHistory: 'Update History',
|
||||
local: 'Local',
|
||||
systemVersion: 'System Version',
|
||||
incompatibleSystemVersion: 'The current MoviePilot version does not meet this plugin requirement.',
|
||||
installToLocal: 'Install to Local',
|
||||
totalDownloads: 'Total {count} downloads',
|
||||
viewData: 'View Data',
|
||||
|
||||
@@ -1542,15 +1542,19 @@ export default {
|
||||
dbWalEnableHint: '可提升读写并发性能,但可能在异常情况下增加数据丢失风险,更改后需重启生效',
|
||||
tmdbApiDomain: 'TMDB API服务地址',
|
||||
tmdbApiDomainPlaceholder: 'api.themoviedb.org',
|
||||
tmdbApiDomainHint: '自定义themoviedb API域名或代理地址',
|
||||
tmdbApiDomainHint: '自定义 TheMovieDb API域名或代理地址',
|
||||
tmdbApiDomainRequired: '请输入TMDB API域名',
|
||||
tmdbApiKey: 'TMDB API Key',
|
||||
tmdbApiKeyPlaceholder: '请输入 TMDB API Key',
|
||||
tmdbApiKeyHint: '设置 TheMovieDb API Key',
|
||||
tmdbApiKeyRequired: '请输入TMDB API Key',
|
||||
tmdbImageDomain: 'TMDB 图片服务地址',
|
||||
tmdbImageDomainPlaceholder: 'image.tmdb.org',
|
||||
tmdbImageDomainHint: '自定义themoviedb图片服务域名或代理地址',
|
||||
tmdbImageDomainHint: '自定义 TheMovieDb 图片服务域名或代理地址',
|
||||
tmdbImageDomainRequired: '请输入图片服务域名',
|
||||
tmdbLocale: 'TMDB 元数据语言',
|
||||
tmdbLocalePlaceholder: 'zh',
|
||||
tmdbLocaleHint: '自定义themoviedb元数据语言',
|
||||
tmdbLocaleHint: '自定义 TheMovieDb 元数据语言',
|
||||
metaCacheExpire: '媒体元数据缓存过期时间',
|
||||
metaCacheExpireHint: '识别元数据本地缓存时间,为 0 时使用内置默认值',
|
||||
metaCacheExpireRequired: '请输入元数据缓存时间',
|
||||
@@ -2791,6 +2795,8 @@ export default {
|
||||
projectHome: '项目主页',
|
||||
updateHistory: '更新说明',
|
||||
local: '本地',
|
||||
systemVersion: '系统版本',
|
||||
incompatibleSystemVersion: '当前 MoviePilot 版本不满足插件要求,无法安装',
|
||||
installToLocal: '安装到本地',
|
||||
totalDownloads: '共 {count} 次下载',
|
||||
viewData: '查看数据',
|
||||
|
||||
@@ -1543,15 +1543,19 @@ export default {
|
||||
dbWalEnableHint: '可提升讀寫併發性能,但可能在異常情況下增加數據丟失風險,更改後需重啟生效',
|
||||
tmdbApiDomain: 'TMDB API服務地址',
|
||||
tmdbApiDomainPlaceholder: 'api.themoviedb.org',
|
||||
tmdbApiDomainHint: '自定義themoviedb API域名或代理地址',
|
||||
tmdbApiDomainHint: '自定義 TheMovieDb API域名或代理地址',
|
||||
tmdbApiDomainRequired: '請輸入TMDB API域名',
|
||||
tmdbApiKey: 'TMDB API Key',
|
||||
tmdbApiKeyPlaceholder: '請輸入 TMDB API Key',
|
||||
tmdbApiKeyHint: '設定 TheMovieDb API Key',
|
||||
tmdbApiKeyRequired: '請輸入TMDB API Key',
|
||||
tmdbImageDomain: 'TMDB 圖片服務地址',
|
||||
tmdbImageDomainPlaceholder: 'image.tmdb.org',
|
||||
tmdbImageDomainHint: '自定義themoviedb圖片服務域名或代理地址',
|
||||
tmdbImageDomainHint: '自定義 TheMovieDb 圖片服務域名或代理地址',
|
||||
tmdbImageDomainRequired: '請輸入圖片服務域名',
|
||||
tmdbLocale: 'TMDB 元數據語言',
|
||||
tmdbLocalePlaceholder: 'zh',
|
||||
tmdbLocaleHint: '自定義themoviedb元數據語言',
|
||||
tmdbLocaleHint: '自定義 TheMovieDb 元數據語言',
|
||||
metaCacheExpire: '媒體元數據緩存過期時間',
|
||||
metaCacheExpireHint: '識別元數據本地緩存時間,為 0 時使用內置默認值',
|
||||
metaCacheExpireRequired: '請輸入元數據緩存時間',
|
||||
|
||||
@@ -294,6 +294,7 @@ const streamPreviewLimit = 24
|
||||
const streamUiFlushDelay = 1000
|
||||
const streamPreviewBufferLimit = streamPreviewLimit * 4
|
||||
const searchStreamIdleTimeout = 90_000
|
||||
const searchStreamDoneCloseDelay = 1500
|
||||
|
||||
const streamTotalCount = ref(0)
|
||||
const streamPreviewDataList = ref<Array<Context>>([])
|
||||
@@ -528,6 +529,11 @@ function resetSearchResults() {
|
||||
applyFilter()
|
||||
}
|
||||
|
||||
// 判断当前页面是否已经完成过一次带关键词的空结果搜索,避免 keep-alive 返回时自动重搜。
|
||||
function hasLoadedEmptySearchResult() {
|
||||
return isRefreshed.value && !progressActive.value && rawDataList.value.length === 0 && hasSearchKeyword(activeSearchParams.value)
|
||||
}
|
||||
|
||||
// 更新搜索进度
|
||||
function updateSearchProgress(eventData: { [key: string]: any }, flushNow: boolean = false) {
|
||||
if (eventData.text) {
|
||||
@@ -658,6 +664,7 @@ function searchByStream(params: SearchParams, requestToken?: string) {
|
||||
closeSearchEventSource()
|
||||
|
||||
let settled = false
|
||||
let receivedDone = false
|
||||
const source = new EventSource(buildSearchStreamUrl(params, requestToken))
|
||||
searchEventSource = source
|
||||
|
||||
@@ -692,7 +699,12 @@ function searchByStream(params: SearchParams, requestToken?: string) {
|
||||
}
|
||||
|
||||
if (eventData.type === 'done') {
|
||||
settleSearchStream(resolve)
|
||||
// 收到 done 后给后端留出收尾时间,避免过早关闭连接中断搜索结果缓存写入
|
||||
receivedDone = true
|
||||
clearSearchStreamIdleTimer()
|
||||
searchStreamIdleTimer = setTimeout(() => {
|
||||
settleSearchStream(resolve)
|
||||
}, searchStreamDoneCloseDelay)
|
||||
}
|
||||
} catch (error) {
|
||||
settleSearchStream(() => reject(error))
|
||||
@@ -702,6 +714,11 @@ function searchByStream(params: SearchParams, requestToken?: string) {
|
||||
source.onerror = () => {
|
||||
if (source !== searchEventSource || settled) return
|
||||
|
||||
if (receivedDone) {
|
||||
settleSearchStream(resolve)
|
||||
return
|
||||
}
|
||||
|
||||
settleSearchStream(() => reject(new Error(t('resource.noResourceFound'))))
|
||||
}
|
||||
})
|
||||
@@ -1009,8 +1026,8 @@ async function checkAiRecommendStatus() {
|
||||
const { success, data } = result
|
||||
const status = data?.status
|
||||
|
||||
// 只要有数据且状态不是disabled,就标记已检查(允许重试)
|
||||
if (data && status !== 'disabled') {
|
||||
// 状态检查只是初始化已有推荐结果,非禁用状态下即使后端暂无历史状态也不应锁住按钮
|
||||
if (status !== 'disabled') {
|
||||
aiStatusChecked.value = true
|
||||
}
|
||||
|
||||
@@ -1030,6 +1047,8 @@ async function checkAiRecommendStatus() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查AI状态失败:', error)
|
||||
// 检查失败不影响用户手动发起智能推荐,避免按钮永久不可用
|
||||
aiStatusChecked.value = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1081,6 +1100,7 @@ onMounted(async () => {
|
||||
|
||||
useKeepAliveRefresh(async () => {
|
||||
if (progressActive.value || isRefreshing.value || isRecommending.value || showingAiResults.value) return
|
||||
if (hasLoadedEmptySearchResult()) return
|
||||
|
||||
const refreshParams = await resolveRefreshSearchParams()
|
||||
if (!refreshParams) return
|
||||
|
||||
@@ -74,7 +74,7 @@ html {
|
||||
block-size: 100%;
|
||||
}
|
||||
|
||||
// 设置项强调卡片:复用通知模板入口的强调条、轻渐变与悬浮反馈。
|
||||
// 设置项强调卡片:复用通知模板入口的弱强调条、轻渐变与悬浮反馈。
|
||||
.app-card-colorful {
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(var(--app-card-accent-rgb), var(--app-card-border-opacity)) !important;
|
||||
@@ -91,11 +91,11 @@ html {
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
--app-card-accent-rgb: var(--v-theme-primary);
|
||||
--app-card-accent-end-rgb: var(--app-card-accent-rgb);
|
||||
--app-card-accent-start-opacity: 0.09;
|
||||
--app-card-accent-end-opacity: 0.06;
|
||||
--app-card-border-opacity: 0.2;
|
||||
--app-card-hover-border-opacity: 0.34;
|
||||
--app-card-stripe-opacity: 0.78;
|
||||
--app-card-accent-start-opacity: 0.025;
|
||||
--app-card-accent-end-opacity: 0.012;
|
||||
--app-card-border-opacity: 0.08;
|
||||
--app-card-hover-border-opacity: 0.16;
|
||||
--app-card-stripe-opacity: 0.22;
|
||||
--app-card-surface-opacity: 0.92;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
@@ -105,7 +105,7 @@ html {
|
||||
background: rgb(var(--app-card-accent-rgb));
|
||||
block-size: 100%;
|
||||
content: "";
|
||||
inline-size: 0.25rem;
|
||||
inline-size: 0.125rem;
|
||||
inset-block: 0;
|
||||
inset-inline-start: 0;
|
||||
opacity: var(--app-card-stripe-opacity);
|
||||
@@ -136,11 +136,11 @@ html[data-theme="transparent"] .app-card-colorful,
|
||||
.v-theme--transparent .app-card-colorful {
|
||||
backdrop-filter: blur(var(--transparent-blur, 10px));
|
||||
border: 0 !important;
|
||||
--app-card-accent-start-opacity: 0.04;
|
||||
--app-card-accent-end-opacity: 0.03;
|
||||
--app-card-accent-start-opacity: 0.018;
|
||||
--app-card-accent-end-opacity: 0.01;
|
||||
--app-card-border-opacity: 0;
|
||||
--app-card-hover-border-opacity: 0;
|
||||
--app-card-stripe-opacity: 0.42;
|
||||
--app-card-stripe-opacity: 0.16;
|
||||
--app-card-surface-opacity: var(--transparent-opacity-light, 0.2);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,13 +37,13 @@ html[data-theme="transparent"] {
|
||||
}
|
||||
}
|
||||
|
||||
// 设置页彩色卡片保留透明主题的玻璃质感,只叠加非常轻的图标主色。
|
||||
// 设置页彩色卡片保留透明主题的玻璃质感,只叠加极轻的图标主色。
|
||||
.app-card-colorful {
|
||||
background:
|
||||
linear-gradient(
|
||||
135deg,
|
||||
rgba(var(--app-card-accent-rgb), var(--app-card-accent-start-opacity, 0.04)),
|
||||
rgba(var(--app-card-accent-end-rgb, var(--app-card-accent-rgb)), var(--app-card-accent-end-opacity, 0.03)) 46%,
|
||||
rgba(var(--app-card-accent-rgb), var(--app-card-accent-start-opacity, 0.018)),
|
||||
rgba(var(--app-card-accent-end-rgb, var(--app-card-accent-rgb)), var(--app-card-accent-end-opacity, 0.01)) 46%,
|
||||
rgba(var(--v-theme-surface), 0) 76%
|
||||
),
|
||||
rgba(var(--v-theme-surface), var(--transparent-opacity-light)) !important;
|
||||
|
||||
@@ -665,6 +665,11 @@ function closePluginProgressDialog() {
|
||||
|
||||
// 安装插件
|
||||
async function installPlugin(item: Plugin) {
|
||||
if (item?.system_version_compatible === false) {
|
||||
$toast.error(item.system_version_message || t('plugin.incompatibleSystemVersion'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 显示等待提示框
|
||||
progressText.value = t('plugin.installing', { name: item?.plugin_name, version: item?.plugin_version })
|
||||
@@ -775,6 +780,9 @@ async function fetchUninstalledPlugins(force: boolean = false, context: KeepAliv
|
||||
data.has_update = true
|
||||
data.repo_url = uninstalled.repo_url
|
||||
data.history = uninstalled.history
|
||||
data.system_version = uninstalled.system_version
|
||||
data.system_version_compatible = uninstalled.system_version_compatible
|
||||
data.system_version_message = uninstalled.system_version_message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ const SystemSettings = ref<any>({
|
||||
RECOGNIZE_PLUGIN_FIRST: false,
|
||||
MEDIA_RECOGNIZE_SHARE: true,
|
||||
TMDB_API_DOMAIN: null,
|
||||
TMDB_API_KEY: null,
|
||||
TMDB_IMAGE_DOMAIN: null,
|
||||
TMDB_LOCALE: null,
|
||||
META_CACHE_EXPIRE: 0,
|
||||
@@ -1798,6 +1799,17 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
|
||||
prepend-inner-icon="mdi-api"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="SystemSettings.Advanced.TMDB_API_KEY"
|
||||
:label="t('setting.system.tmdbApiKey')"
|
||||
:hint="t('setting.system.tmdbApiKeyHint')"
|
||||
persistent-hint
|
||||
:placeholder="t('setting.system.tmdbApiKeyPlaceholder')"
|
||||
:rules="[(v: string) => !!v || t('setting.system.tmdbApiKeyRequired')]"
|
||||
prepend-inner-icon="mdi-key-variant"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VCombobox
|
||||
v-model="SystemSettings.Advanced.TMDB_IMAGE_DOMAIN"
|
||||
|
||||
Reference in New Issue
Block a user