Compare commits

...

6 Commits

Author SHA1 Message Date
jxxghp
cc8d5cf931 style: lighten setting card accents 2026-05-21 22:05:46 +08:00
jxxghp
6083887675 fix: load registered passkeys on dialog open 2026-05-21 07:06:51 +08:00
jxxghp
beb0506b0c feat: show plugin system version compatibility 2026-05-20 19:56:21 +08:00
InfinityPacer
0f906f791a fix(filter-rule): keep custom rule chip titles in sync with props (#474) 2026-05-20 17:29:46 +08:00
jxxghp
7614696e61 fix: 修复智能推荐按钮初始不可用 2026-05-20 12:36:41 +08:00
jxxghp
4235d3687c feat: add tmdb api key setting 2026-05-20 11:28:29 +08:00
15 changed files with 140 additions and 41 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "2.12.2",
"version": "2.12.4",
"private": true,
"type": "module",
"bin": "dist/service.js",

View File

@@ -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
// 插件仓库地址

View File

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

View File

@@ -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 }))

View File

@@ -206,6 +206,7 @@ watch(
passkeyList.value = []
}
},
{ immediate: true },
)
</script>

View File

@@ -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">

View File

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

View File

@@ -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',

View File

@@ -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: '查看数据',

View File

@@ -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: '請輸入元數據緩存時間',

View File

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

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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
}
}
}

View File

@@ -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"