Compare commits

...

14 Commits
v2.10.9 ... v2

Author SHA1 Message Date
jxxghp
a9403c9c34 chore: bump version to 2.10.11 2026-05-07 08:23:20 +08:00
jxxghp
dc4914e3ca style: adjust downloader card API key field to span full width 2026-05-07 08:22:39 +08:00
jxxghp
f3dbc4afad feat: add qBittorrent API key setup support
Expose qBittorrent WebUI API Key fields in settings and setup so 5.2 users can connect without requiring username/password.

Refs jxxghp/MoviePilot#5724
2026-05-07 07:41:05 +08:00
jxxghp
e3e22aebd9 feat: replace log level chips with VSelect dropdown in LoggingView and adjust layout spacing 2026-05-06 13:04:35 +08:00
jxxghp
0ca2f20b24 refactor: update logging record layout to use block-level elements for better alignment and structure 2026-05-06 08:02:50 +08:00
jxxghp
14279c773d fix: update LoggingView layout to support responsive height for mobile devices 2026-05-05 12:37:30 +08:00
jxxghp
8372f63eb6 refactor: dynamic logging view height calculation and remove redundant LLM model refresh on settings save 2026-05-05 12:34:09 +08:00
jxxghp
b7b62d7922 feat: overhaul logging view with advanced filtering, grouped display, and real-time streaming controls 2026-05-05 11:53:21 +08:00
jxxghp
162cce1f50 feat: replace VSelect with VAutocomplete for LLM provider selection in settings 2026-05-04 20:04:14 +08:00
jxxghp
aa49c6ccbc refactor(llm): merge preset selection into base URL field
Use a single editable Base URL combobox for LLM providers so preset endpoints and manual input share one field, with preset types shown as subtitles.
2026-05-03 11:31:06 +08:00
jxxghp
a40e52079f 更新 package.json 2026-05-03 09:44:37 +08:00
jxxghp
c29e329548 feat(llm): add provider URL presets
Expose provider-specific preset endpoints in the setup and system settings flows so users can start from the correct base URL while still editing it manually.
2026-05-03 09:38:28 +08:00
mcgrady.sun
e2d26f6a25 fix(resource): 解决重新搜索按钮 review 问题
- 简化 refreshSearch:移除多余的 switchToOriginalResults 调用,
  直接置 showingAiResults=false,其余状态由 fetchData 内部重置
- 标题栏 v-if 去掉 !progressActive 条件,避免点击重新搜索时
  整个标题栏 unmount 导致按钮 :loading 不可见、页面跳动
2026-05-02 16:33:19 +08:00
mcgrady.sun
1752256868 feat(resource): 资源搜索结果页增加重新搜索按钮
- 在搜索结果页右侧操作区新增"重新搜索"按钮(mdi-refresh 图标)
- 点击后使用相同搜索参数重新触发请求;正在请求或加载中时按钮禁用
- 若当前展示的是 AI 推荐结果,先切回原始结果再重新搜索,避免状态不一致
- 同步补充 zh-CN / zh-TW / en-US 三份本地化文案
2026-05-02 16:33:19 +08:00
14 changed files with 1166 additions and 138 deletions

View File

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

View File

@@ -346,11 +346,23 @@ onUnmounted(() => {
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="downloaderInfo.config.apikey"
type="password"
:label="t('downloader.apiKey')"
:hint="t('downloader.qbittorrentApiKeyHint')"
persistent-hint
active
prepend-inner-icon="mdi-key-variant"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="downloaderInfo.config.username"
:label="t('downloader.username')"
:hint="t('downloader.username')"
:disabled="!!downloaderInfo.config.apikey"
persistent-hint
active
prepend-inner-icon="mdi-account"
@@ -362,6 +374,7 @@ onUnmounted(() => {
type="password"
:label="t('downloader.password')"
:hint="t('downloader.password')"
:disabled="!!downloaderInfo.config.apikey"
persistent-hint
active
prepend-inner-icon="mdi-lock"

View File

@@ -566,13 +566,13 @@ watch(
</VDialog>
<!-- 实时日志弹窗 -->
<VDialog
v-if="loggingDialog"
v-model="loggingDialog"
scrollable
max-width="60rem"
:fullscreen="!display.mdAndUp.value"
>
<VDialog
v-if="loggingDialog"
v-model="loggingDialog"
scrollable
max-width="72rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VDialogCloseBtn @click="loggingDialog = false" />
<VCardItem>
@@ -588,7 +588,7 @@ watch(
</VCardTitle>
</VCardItem>
<VDivider />
<VCardText>
<VCardText class="pa-0">
<LoggingView :logfile="`plugins/${props.plugin?.id?.toLowerCase()}.log`" />
</VCardText>
</VCard>

View File

@@ -16,11 +16,23 @@ export interface LlmProviderAuthStatus {
updated_at?: number | null
}
export interface LlmProviderUrlPreset {
label: string
value: string
}
export interface LlmProviderUrlPresetItem {
title: string
value: string
subtitle?: string
}
export interface LlmProvider {
id: string
name: string
runtime: string
default_base_url: string
base_url_presets?: LlmProviderUrlPreset[]
base_url_editable: boolean
requires_base_url: boolean
supports_api_key: boolean
@@ -96,9 +108,16 @@ export function useLlmProviderDirectory(options: UseLlmProviderDirectoryOptions)
() => models.value.find(item => item.id === normalizeValue(options.model.value)) || null,
)
const providerItems = computed(() => providers.value.map(item => ({ title: item.name, value: item.id })))
const baseUrlPresetItems = computed<LlmProviderUrlPresetItem[]>(() =>
(selectedProvider.value?.base_url_presets || []).map(item => ({
title: item.value,
value: item.value,
subtitle: item.label,
})),
)
const providerConnected = computed(() => Boolean(selectedProvider.value?.auth_status?.connected))
const showBaseUrlField = computed(
() => Boolean(selectedProvider.value?.requires_base_url || selectedProvider.value?.base_url_editable),
() => Boolean(selectedProvider.value && (selectedProvider.value.oauth_methods || []).length === 0),
)
const showApiKeyField = computed(() => selectedProvider.value?.supports_api_key !== false)
const hasUsableCredential = computed(() => {
@@ -333,6 +352,7 @@ export function useLlmProviderDirectory(options: UseLlmProviderDirectoryOptions)
return {
providers,
providerItems,
baseUrlPresetItems,
models,
selectedProvider,
selectedModel,

View File

@@ -107,6 +107,7 @@ export interface ValidationErrorState {
downloader: {
name: boolean
host: boolean
apikey: boolean
username: boolean
password: boolean
}
@@ -277,6 +278,7 @@ const validationErrors = ref<ValidationErrorState>({
downloader: {
name: false,
host: false,
apikey: false,
username: false,
password: false,
},
@@ -466,6 +468,7 @@ export function useSetupWizard() {
validationErrors.value.downloader = {
name: false,
host: false,
apikey: false,
username: false,
password: false,
}
@@ -548,9 +551,18 @@ export function useSetupWizard() {
}
// 根据下载器类型验证其他必输项
if (
wizardData.value.downloader.type === 'qbittorrent'
|| wizardData.value.downloader.type === 'transmission'
if (wizardData.value.downloader.type === 'qbittorrent') {
const hasApiKey = !!wizardData.value.downloader.config?.apikey?.trim()
if (!hasApiKey && !wizardData.value.downloader.config?.username?.trim()) {
errors.push(t('downloader.usernameRequired'))
validationErrors.value.downloader.username = true
}
if (!hasApiKey && !wizardData.value.downloader.config?.password?.trim()) {
errors.push(t('downloader.passwordRequired'))
validationErrors.value.downloader.password = true
}
} else if (
wizardData.value.downloader.type === 'transmission'
|| wizardData.value.downloader.type === 'rtorrent'
) {
if (!wizardData.value.downloader.config?.username?.trim()) {

View File

@@ -361,13 +361,13 @@ onMounted(() => {
</VCard>
</VDialog>
<!-- 实时日志弹窗 -->
<VDialog
v-if="loggingDialog"
v-model="loggingDialog"
scrollable
max-width="70rem"
:fullscreen="!display.mdAndUp.value"
>
<VDialog
v-if="loggingDialog"
v-model="loggingDialog"
scrollable
max-width="80rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VDialogCloseBtn @click="loggingDialog = false" />
<VCardItem>
@@ -383,7 +383,7 @@ onMounted(() => {
</VCardTitle>
</VCardItem>
<VDivider />
<VCardText>
<VCardText class="pa-0">
<LoggingView logfile="moviepilot.log" />
</VCardText>
</VCard>

View File

@@ -998,6 +998,7 @@ export default {
aiRecommend: 'AI Recommendation',
reRecommend: 'Regenerate Recommendation',
aiRecommendError: 'AI Recommendation Failed',
refreshSearch: 'Re-search',
},
browse: {
actor: 'Actor',
@@ -1233,6 +1234,17 @@ export default {
content: 'Content',
refreshing: 'Refreshing',
initializing: 'Initializing',
searchPlaceholder: 'Search logs',
allLevels: 'All Levels',
followTail: 'Follow latest logs',
wrapLines: 'Wrap lines',
pauseStream: 'Pause stream',
resumeStream: 'Resume stream',
waitingForLogs: 'Waiting for logs...',
paused: 'Paused',
connected: 'Live',
lineCount: 'Showing {visible}/{total} lines',
jumpToLatest: 'Jump to latest ({count})',
},
moduleTest: {
normal: 'Normal',
@@ -2921,8 +2933,10 @@ export default {
rtorrentHostHint: 'HTTP: http://ip:port/RPC2 or SCGI: scgi://ip:port',
default: 'Default',
host: 'Host',
apiKey: 'API Key',
username: 'Username',
password: 'Password',
qbittorrentApiKeyHint: 'For qBittorrent 5.2+, you can use the WebUI API Key directly. When set, API Key auth is preferred.',
category: 'Auto Category Management',
sequentail: 'Sequential Download',
force_resume: 'Force Resume',

View File

@@ -993,6 +993,7 @@ export default {
aiRecommend: '智能推荐',
reRecommend: '重新生成推荐',
aiRecommendError: '智能推荐失败',
refreshSearch: '重新搜索',
},
browse: {
actor: '演员',
@@ -1228,6 +1229,17 @@ export default {
content: '内容',
refreshing: '正在刷新',
initializing: '正在初始化',
searchPlaceholder: '搜索日志内容',
allLevels: '全部级别',
followTail: '跟随最新日志',
wrapLines: '自动换行',
pauseStream: '暂停日志流',
resumeStream: '恢复日志流',
waitingForLogs: '等待日志输出...',
paused: '已暂停',
connected: '实时更新中',
lineCount: '显示 {visible}/{total} 行',
jumpToLatest: '查看最新 ({count})',
},
moduleTest: {
normal: '正常',
@@ -2874,8 +2886,10 @@ export default {
rtorrentHostHint: 'HTTP: http://ip:port/RPC2 或 SCGI: scgi://ip:port',
default: '默认',
host: '地址',
apiKey: 'API Key',
username: '用户名',
password: '密码',
qbittorrentApiKeyHint: 'qBittorrent 5.2+ 可直接使用 WebUI API Key填写后将优先使用 API Key 登录。',
category: '自动分类管理',
sequentail: '顺序下载',
force_resume: '强制继续',

View File

@@ -994,6 +994,7 @@ export default {
aiRecommend: '智能推薦',
reRecommend: '重新生成推薦',
aiRecommendError: '智能推薦失敗',
refreshSearch: '重新搜尋',
},
browse: {
actor: '演員',
@@ -1230,6 +1231,17 @@ export default {
content: '內容',
refreshing: '正在刷新',
initializing: '正在初始化',
searchPlaceholder: '搜索日誌內容',
allLevels: '全部級別',
followTail: '跟隨最新日誌',
wrapLines: '自動換行',
pauseStream: '暫停日誌流',
resumeStream: '恢復日誌流',
waitingForLogs: '等待日誌輸出...',
paused: '已暫停',
connected: '實時更新中',
lineCount: '顯示 {visible}/{total} 行',
jumpToLatest: '查看最新 ({count})',
},
moduleTest: {
normal: '正常',
@@ -2876,8 +2888,10 @@ export default {
enabled: '啟用',
default: '預設',
host: '地址',
apiKey: 'API Key',
username: '用戶名',
password: '密碼',
qbittorrentApiKeyHint: 'qBittorrent 5.2+ 可直接使用 WebUI API Key填寫後將優先使用 API Key 登入。',
category: '自動分類管理',
sequentail: '順序下載',
force_resume: '強制繼續',

View File

@@ -95,6 +95,9 @@ const cardScroll = useInfiniteScroll(filteredCardDataList)
// 是否刷新过
const isRefreshed = ref(false)
// 是否正在重新搜索
const isRefreshing = ref(false)
// 加载进度文本
const progressText = ref(t('common.pleaseWait'))
@@ -464,6 +467,21 @@ async function fetchData() {
}
}
// 重新搜索(使用相同参数重新触发搜索)
async function refreshSearch() {
if (isRefreshing.value || progressActive.value) return
isRefreshing.value = true
try {
// 重新搜索时退出 AI 视图,其余状态由 fetchData 内部重置
showingAiResults.value = false
await fetchData()
} catch (error) {
console.error('重新搜索失败:', error)
} finally {
isRefreshing.value = false
}
}
// 切换到智能推荐结果(自动保存筛选条件)
async function switchToAiResults() {
if (showingAiResults.value) {
@@ -808,8 +826,8 @@ onUnmounted(() => {
</div>
</VFadeTransition>
<!-- 精简标题栏 -->
<VCard v-if="isRefreshed && !progressActive" class="search-header d-flex align-center mb-3">
<!-- 精简标题栏搜索过后保持挂载加载中由按钮 :disabled / :loading 表达状态 -->
<VCard v-if="isRefreshed" class="search-header d-flex align-center mb-3">
<div class="search-info-container">
<div class="search-title text-moviepilot">
<span class="d-none d-sm-inline">{{ t('resource.searchResults') }}</span>
@@ -833,6 +851,22 @@ onUnmounted(() => {
<VSpacer />
<!-- 重新搜索按钮 -->
<VBtn
variant="text"
size="small"
icon
class="me-2 refresh-search-btn"
:loading="isRefreshing"
:disabled="isRefreshing || progressActive"
@click="refreshSearch"
>
<VIcon icon="mdi-refresh" size="20" />
<VTooltip activator="parent" location="top">
{{ t('resource.refreshSearch') }}
</VTooltip>
</VBtn>
<!-- AI操作按钮组 -->
<div v-if="aiRecommendEnabled && originalDataList.length > 0" class="ai-toggle-container me-2">
<div class="ai-toggle-buttons">
@@ -1180,6 +1214,14 @@ onUnmounted(() => {
background-color: rgba(var(--v-theme-primary), 0.05);
}
/* 重新搜索按钮 */
.refresh-search-btn {
block-size: 44px !important;
inline-size: 44px !important;
border-radius: 8px !important;
background-color: rgba(var(--v-theme-surface-variant), 0.1);
}
/* AI按钮组样式 */
.ai-toggle-container {
position: relative;
@@ -1371,6 +1413,11 @@ onUnmounted(() => {
inline-size: 36px;
}
.refresh-search-btn {
block-size: 36px !important;
inline-size: 36px !important;
}
.ai-toggle-buttons {
block-size: 36px;
}

View File

@@ -221,6 +221,7 @@ const llmMaxContextRef = computed({
const {
providerItems: llmProviderItems,
baseUrlPresetItems: llmBaseUrlPresetItems,
models: llmModels,
selectedProvider: selectedLlmProvider,
selectedModel: selectedLlmModel,
@@ -525,9 +526,6 @@ async function loadSystemSettings() {
}
SystemSettings.value.Basic.LLM_THINKING_LEVEL = resolveThinkingLevelValue(result.data)
await loadLlmProviders()
if (SystemSettings.value.Basic.AI_AGENT_ENABLE && canRefreshModels.value) {
await refreshLlmModels(false)
}
}
} catch (error) {
console.log(error)
@@ -1003,7 +1001,7 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VSelect
<VAutocomplete
v-model="SystemSettings.Basic.LLM_PROVIDER"
:label="t('setting.system.llmProvider')"
:hint="t('setting.system.llmProviderHint')"
@@ -1015,14 +1013,24 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && showBaseUrlField" cols="12" md="6">
<VTextField
v-model="SystemSettings.Basic.LLM_BASE_URL"
<VCombobox
:model-value="SystemSettings.Basic.LLM_BASE_URL"
@update:model-value="(value: any) => {
SystemSettings.Basic.LLM_BASE_URL = typeof value === 'object' && value !== null ? value.value : (value || '');
}"
:label="t('setting.system.llmBaseUrl')"
:hint="t('setting.system.llmBaseUrlHint')"
placeholder="https://api.deepseek.com"
:placeholder="selectedLlmProvider?.default_base_url || 'https://api.deepseek.com'"
:items="llmBaseUrlPresetItems"
item-title="title"
item-value="value"
persistent-hint
prepend-inner-icon="mdi-link"
/>
>
<template #item="{ props, item }">
<VListItem v-bind="props" :subtitle="item.raw.subtitle" />
</template>
</VCombobox>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && showApiKeyField" cols="12" md="6">
<VTextField

View File

@@ -53,6 +53,7 @@ const authConnectedRef = computed({
const {
providerItems,
baseUrlPresetItems,
models: llmModels,
selectedProvider,
selectedModel,
@@ -213,7 +214,7 @@ onMounted(async () => {
</VCol>
<VCol cols="12" md="6">
<VSelect
<VAutocomplete
v-model="wizardData.agent.provider"
:label="t('setting.system.llmProvider')"
:hint="t('setting.system.llmProviderHint')"
@@ -228,14 +229,24 @@ onMounted(async () => {
</VCol>
<VCol v-if="showBaseUrlField" cols="12" md="6">
<VTextField
v-model="wizardData.agent.baseUrl"
<VCombobox
:model-value="wizardData.agent.baseUrl"
@update:model-value="(value: any) => {
wizardData.agent.baseUrl = typeof value === 'object' && value !== null ? value.value : (value || '');
}"
:label="t('setting.system.llmBaseUrl')"
:hint="t('setting.system.llmBaseUrlHint')"
placeholder="https://api.deepseek.com"
:placeholder="selectedProvider?.default_base_url || 'https://api.deepseek.com'"
:items="baseUrlPresetItems"
item-title="title"
item-value="value"
persistent-hint
prepend-inner-icon="mdi-link-variant"
/>
>
<template #item="{ props, item }">
<VListItem v-bind="props" :subtitle="item.raw.subtitle" />
</template>
</VCombobox>
</VCol>
<VCol v-if="showApiKeyField" cols="12" md="6">

View File

@@ -104,6 +104,17 @@ const { wizardData, selectDownloader, validationErrors } = useSetupWizard()
required
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.downloader.config.apikey"
type="password"
:label="t('downloader.apiKey')"
:hint="t('downloader.qbittorrentApiKeyHint')"
persistent-hint
active
prepend-inner-icon="mdi-key-variant"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.downloader.config.username"
@@ -111,10 +122,11 @@ const { wizardData, selectDownloader, validationErrors } = useSetupWizard()
:hint="t('downloader.username')"
:error="validationErrors.downloader.username"
:error-messages="validationErrors.downloader.username ? [t('downloader.usernameRequired')] : []"
:disabled="!!wizardData.downloader.config.apikey"
persistent-hint
active
prepend-inner-icon="mdi-account"
required
:required="!wizardData.downloader.config.apikey"
/>
</VCol>
<VCol cols="12" md="6">
@@ -125,10 +137,11 @@ const { wizardData, selectDownloader, validationErrors } = useSetupWizard()
:hint="t('downloader.password')"
:error="validationErrors.downloader.password"
:error-messages="validationErrors.downloader.password ? [t('downloader.passwordRequired')] : []"
:disabled="!!wizardData.downloader.config.apikey"
persistent-hint
active
prepend-inner-icon="mdi-lock"
required
:required="!wizardData.downloader.config.apikey"
/>
</VCol>
<VCol cols="12" md="6">

File diff suppressed because it is too large Load Diff