diff --git a/package.json b/package.json index 790ac598..0979dc4c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "moviepilot", - "version": "2.12.0", + "version": "2.12.1", "private": true, "type": "module", "bin": "dist/service.js", @@ -128,4 +128,4 @@ "workbox-window": "^7.3.0" }, "packageManager": "yarn@1.22.18" -} \ No newline at end of file +} diff --git a/src/App.vue b/src/App.vue index 39d9d7cd..eee7c131 100644 --- a/src/App.vue +++ b/src/App.vue @@ -11,6 +11,7 @@ import { preloadImage } from './@core/utils/image' import { globalLoadingStateManager } from '@/utils/loadingStateManager' import { addBackgroundTimer, removeBackgroundTimer } from '@/utils/backgroundManager' import PWAInstallPrompt from '@/components/PWAInstallPrompt.vue' +import SharedDialogHost from '@/components/dialog/SharedDialogHost.vue' import { themeManager } from '@/utils/themeManager' import { configureApexChartsTheme } from '@/utils/apexCharts' @@ -367,6 +368,8 @@ onUnmounted(() => { + + diff --git a/src/components/cards/CustomRuleCard.vue b/src/components/cards/CustomRuleCard.vue index becd79f8..9a0ba55e 100644 --- a/src/components/cards/CustomRuleCard.vue +++ b/src/components/cards/CustomRuleCard.vue @@ -1,14 +1,9 @@ diff --git a/src/components/cards/DownloaderCard.vue b/src/components/cards/DownloaderCard.vue index 3e170913..1b8ed13f 100644 --- a/src/components/cards/DownloaderCard.vue +++ b/src/components/cards/DownloaderCard.vue @@ -1,18 +1,14 @@ diff --git a/src/components/cards/FilterRuleGroupCard.vue b/src/components/cards/FilterRuleGroupCard.vue index 00ef84d7..641654ab 100644 --- a/src/components/cards/FilterRuleGroupCard.vue +++ b/src/components/cards/FilterRuleGroupCard.vue @@ -1,23 +1,14 @@ diff --git a/src/components/cards/MediaCard.vue b/src/components/cards/MediaCard.vue index fcf59935..3ee71574 100644 --- a/src/components/cards/MediaCard.vue +++ b/src/components/cards/MediaCard.vue @@ -8,12 +8,10 @@ import { doneNProgress, startNProgress } from '@/api/nprogress' import type { MediaInfo, Subscribe, MediaSeason, Site } from '@/api/types' import router from '@/router' import { useUserStore, useGlobalSettingsStore } from '@/stores' -import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue' -import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue' -import SubscribeSeasonDialog from '../dialog/SubscribeSeasonDialog.vue' import { useI18n } from 'vue-i18n' import { mediaTypeDict } from '@/api/constants' import { hasPermission } from '@/utils/permission' +import { openSharedDialog } from '@/composables/useSharedDialog' import { getCachedMediaExistsStatus, getCachedMediaSubscribeStatus, @@ -21,6 +19,10 @@ import { setCachedMediaSubscribeStatus, } from '@/utils/mediaStatusCache' +const SearchSiteDialog = defineAsyncComponent(() => import('@/components/dialog/SearchSiteDialog.vue')) +const SubscribeEditDialog = defineAsyncComponent(() => import('../dialog/SubscribeEditDialog.vue')) +const SubscribeSeasonDialog = defineAsyncComponent(() => import('../dialog/SubscribeSeasonDialog.vue')) + // 国际化 const { t } = useI18n() @@ -59,15 +61,6 @@ const isSubscribed = ref(false) // 本地存在状态 const isExists = ref(false) -// 订阅季弹窗 -const subscribeSeasonDialog = ref(false) - -// 订阅编辑弹窗 -const subscribeEditDialog = ref(false) - -// 订阅ID -const subscribeId = ref() - // 选中的订阅季 const seasonsSelected = ref([]) @@ -93,12 +86,48 @@ const selectedSites = ref([]) // 搜索菜单显示状态 const searchMenuShow = ref(false) -// 选择站点对话框 -const chooseSiteDialog = ref(false) - // 选择的剧集组 const episodeGroup = ref('') +// 打开订阅季选择弹窗,避免每个媒体卡片都持有弹窗实例。 +function openSubscribeSeasonDialog() { + openSharedDialog( + SubscribeSeasonDialog, + { media: props.media }, + { + subscribe: subscribeSeasons, + }, + { closeOn: ['close', 'subscribe'] }, + ) +} + +// 打开订阅编辑弹窗,保存、关闭或删除时释放共享弹窗实例。 +function openSubscribeEditDialog(subid: number) { + openSharedDialog( + SubscribeEditDialog, + { subid }, + { + remove: onRemoveSubscribe, + }, + { closeOn: ['close', 'save', 'remove'] }, + ) +} + +// 打开站点选择弹窗,并把选择结果交回当前媒体卡片继续搜索。 +function openSearchSiteDialog() { + openSharedDialog( + SearchSiteDialog, + { + sites: allSites.value, + selected: selectedSites.value, + }, + { + search: searchSites, + }, + { closeOn: ['close', 'search'] }, + ) +} + // 查询所有站点 async function querySites() { try { @@ -157,7 +186,7 @@ async function handleAddSubscribe() { if (props.media?.type === '电视剧') { // 弹出季选择列表,支持多选 seasonsSelected.value = [] - subscribeSeasonDialog.value = true + openSubscribeSeasonDialog() } else { // 电影 addSubscribe() @@ -199,8 +228,7 @@ async function addSubscribe(season: number | null = null, best_version: number = if (result.success && seasonsSelected.value.length <= 1) { const show_edit_dialog = await queryDefaultSubscribeConfig() if (show_edit_dialog) { - subscribeId.value = result.data.id - subscribeEditDialog.value = true + openSubscribeEditDialog(result.data.id) } } } catch (error) { @@ -330,7 +358,6 @@ function handleSubscribe() { // 订阅多季 function subscribeSeasons(seasons: MediaSeason[], seasonNoExists: { [key: number]: number }, groudId: string) { - subscribeSeasonDialog.value = false episodeGroup.value = groudId seasonsSelected.value = seasons || [] seasonsSelected.value.forEach(season => { @@ -375,7 +402,7 @@ async function clickSearch() { await querySelectedSites() } if (allSites.value?.length > 0) { - chooseSiteDialog.value = true + openSearchSiteDialog() } else { handleSearch() } @@ -399,7 +426,6 @@ function handleSearch() { // 搜索多站点 function searchSites(sites: number[]) { - chooseSiteDialog.value = false selectedSites.value = sites handleSearch() } @@ -449,7 +475,7 @@ const getImgUrl: Ref = computed(() => { // 移除订阅 function onRemoveSubscribe() { - subscribeEditDialog.value = false + isSubscribed.value = false } // 获取媒体类型文本 @@ -565,32 +591,6 @@ onBeforeUnmount(() => { - - - - - - diff --git a/src/components/dialog/CustomCssDialog.vue b/src/components/dialog/CustomCssDialog.vue new file mode 100644 index 00000000..2c1c6acd --- /dev/null +++ b/src/components/dialog/CustomCssDialog.vue @@ -0,0 +1,80 @@ + + + diff --git a/src/components/dialog/CustomRuleInfoDialog.vue b/src/components/dialog/CustomRuleInfoDialog.vue new file mode 100644 index 00000000..ab6baf03 --- /dev/null +++ b/src/components/dialog/CustomRuleInfoDialog.vue @@ -0,0 +1,209 @@ + + + diff --git a/src/components/dialog/DiscoverTabOrderDialog.vue b/src/components/dialog/DiscoverTabOrderDialog.vue new file mode 100644 index 00000000..3f8b4425 --- /dev/null +++ b/src/components/dialog/DiscoverTabOrderDialog.vue @@ -0,0 +1,161 @@ + + + + + diff --git a/src/components/dialog/DownloaderInfoDialog.vue b/src/components/dialog/DownloaderInfoDialog.vue new file mode 100644 index 00000000..de308827 --- /dev/null +++ b/src/components/dialog/DownloaderInfoDialog.vue @@ -0,0 +1,507 @@ + + + diff --git a/src/components/dialog/FileNewFolderDialog.vue b/src/components/dialog/FileNewFolderDialog.vue new file mode 100644 index 00000000..c1274898 --- /dev/null +++ b/src/components/dialog/FileNewFolderDialog.vue @@ -0,0 +1,63 @@ + + + diff --git a/src/components/dialog/FileRenameDialog.vue b/src/components/dialog/FileRenameDialog.vue new file mode 100644 index 00000000..0404bd87 --- /dev/null +++ b/src/components/dialog/FileRenameDialog.vue @@ -0,0 +1,94 @@ + + + diff --git a/src/components/dialog/FilterRuleGroupInfoDialog.vue b/src/components/dialog/FilterRuleGroupInfoDialog.vue new file mode 100644 index 00000000..9d181159 --- /dev/null +++ b/src/components/dialog/FilterRuleGroupInfoDialog.vue @@ -0,0 +1,314 @@ + + + diff --git a/src/components/dialog/LlmProviderAuthDialog.vue b/src/components/dialog/LlmProviderAuthDialog.vue new file mode 100644 index 00000000..ab81d043 --- /dev/null +++ b/src/components/dialog/LlmProviderAuthDialog.vue @@ -0,0 +1,82 @@ + + + diff --git a/src/components/dialog/LoginMfaDialog.vue b/src/components/dialog/LoginMfaDialog.vue new file mode 100644 index 00000000..7f46d9bb --- /dev/null +++ b/src/components/dialog/LoginMfaDialog.vue @@ -0,0 +1,102 @@ + + + diff --git a/src/components/dialog/MediaServerInfoDialog.vue b/src/components/dialog/MediaServerInfoDialog.vue new file mode 100644 index 00000000..a41902dc --- /dev/null +++ b/src/components/dialog/MediaServerInfoDialog.vue @@ -0,0 +1,601 @@ + + + diff --git a/src/components/dialog/NotificationChannelInfoDialog.vue b/src/components/dialog/NotificationChannelInfoDialog.vue new file mode 100644 index 00000000..5b97e0a8 --- /dev/null +++ b/src/components/dialog/NotificationChannelInfoDialog.vue @@ -0,0 +1,1131 @@ + + + diff --git a/src/components/dialog/NotificationTemplateEditorDialog.vue b/src/components/dialog/NotificationTemplateEditorDialog.vue new file mode 100644 index 00000000..0eff6026 --- /dev/null +++ b/src/components/dialog/NotificationTemplateEditorDialog.vue @@ -0,0 +1,90 @@ + + + diff --git a/src/components/dialog/OfflineStatusDialog.vue b/src/components/dialog/OfflineStatusDialog.vue new file mode 100644 index 00000000..a86d28a9 --- /dev/null +++ b/src/components/dialog/OfflineStatusDialog.vue @@ -0,0 +1,175 @@ + + + + + diff --git a/src/components/dialog/PluginCloneDialog.vue b/src/components/dialog/PluginCloneDialog.vue new file mode 100644 index 00000000..a96a5306 --- /dev/null +++ b/src/components/dialog/PluginCloneDialog.vue @@ -0,0 +1,172 @@ + + + diff --git a/src/components/dialog/PluginFolderCreateDialog.vue b/src/components/dialog/PluginFolderCreateDialog.vue new file mode 100644 index 00000000..18eeb481 --- /dev/null +++ b/src/components/dialog/PluginFolderCreateDialog.vue @@ -0,0 +1,65 @@ + + + diff --git a/src/components/dialog/PluginFolderRenameDialog.vue b/src/components/dialog/PluginFolderRenameDialog.vue new file mode 100644 index 00000000..285f0d51 --- /dev/null +++ b/src/components/dialog/PluginFolderRenameDialog.vue @@ -0,0 +1,66 @@ + + + diff --git a/src/components/dialog/PluginFolderSettingsDialog.vue b/src/components/dialog/PluginFolderSettingsDialog.vue new file mode 100644 index 00000000..0f3eb869 --- /dev/null +++ b/src/components/dialog/PluginFolderSettingsDialog.vue @@ -0,0 +1,210 @@ + + + diff --git a/src/components/dialog/PluginLogDialog.vue b/src/components/dialog/PluginLogDialog.vue new file mode 100644 index 00000000..36325170 --- /dev/null +++ b/src/components/dialog/PluginLogDialog.vue @@ -0,0 +1,69 @@ + + + diff --git a/src/components/dialog/PluginMarketDetailDialog.vue b/src/components/dialog/PluginMarketDetailDialog.vue new file mode 100644 index 00000000..a545c33f --- /dev/null +++ b/src/components/dialog/PluginMarketDetailDialog.vue @@ -0,0 +1,196 @@ + + + diff --git a/src/components/dialog/PluginSearchDialog.vue b/src/components/dialog/PluginSearchDialog.vue new file mode 100644 index 00000000..dcf67325 --- /dev/null +++ b/src/components/dialog/PluginSearchDialog.vue @@ -0,0 +1,133 @@ + + + + diff --git a/src/components/dialog/PluginVersionHistoryDialog.vue b/src/components/dialog/PluginVersionHistoryDialog.vue new file mode 100644 index 00000000..af0997d6 --- /dev/null +++ b/src/components/dialog/PluginVersionHistoryDialog.vue @@ -0,0 +1,62 @@ + + + diff --git a/src/components/dialog/ProgressDialog.vue b/src/components/dialog/ProgressDialog.vue index bc9869ec..09dda288 100644 --- a/src/components/dialog/ProgressDialog.vue +++ b/src/components/dialog/ProgressDialog.vue @@ -7,6 +7,9 @@ const props = defineProps({ value: Number, text: String, }) + +// 有明确进度值时显示确定进度,否则显示不确定进度条。 +const hasProgressValue = computed(() => typeof props.value === 'number' && Number.isFinite(props.value)) diff --git a/src/components/filebrowser/FileToolbar.vue b/src/components/filebrowser/FileToolbar.vue index 3c6a2c6e..75c320ca 100644 --- a/src/components/filebrowser/FileToolbar.vue +++ b/src/components/filebrowser/FileToolbar.vue @@ -3,6 +3,9 @@ import type { AxiosRequestConfig, AxiosInstance } from 'axios' import type { EndPoints, FileItem } from '@/api/types' import { useDisplay } from 'vuetify' import { useI18n } from 'vue-i18n' +import { openSharedDialog } from '@/composables/useSharedDialog' + +const FileNewFolderDialog = defineAsyncComponent(() => import('../dialog/FileNewFolderDialog.vue')) // 国际化 const { t } = useI18n() @@ -39,11 +42,9 @@ const inProps = defineProps({ // 对外事件 const emit = defineEmits(['storagechanged', 'pathchanged', 'loading', 'foldercreated', 'sortchanged']) -// 新建文件夹名称 -const newFolderPopper = ref(false) - // 新建文件名称 const newFolderName = ref('') +let newFolderDialogController: ReturnType | null = null // 调整排序方式 function changeSort() { @@ -105,7 +106,8 @@ async function mkdir() { // 调API await inProps.axios.request(config) - newFolderPopper.value = false + newFolderDialogController?.close() + newFolderDialogController = null newFolderName.value = '' emit('loading', false) @@ -115,7 +117,18 @@ async function mkdir() { function openNewFolderDialog() { newFolderName.value = '' - newFolderPopper.value = true + newFolderDialogController = openSharedDialog( + FileNewFolderDialog, + { name: newFolderName.value }, + { + create: mkdir, + 'update:name': (value: string) => { + newFolderName.value = value + newFolderDialogController?.updateProps({ name: value }) + }, + }, + { closeOn: ['close'] }, + ) } // 计算排序图标 @@ -124,6 +137,10 @@ const sortIcon = computed(() => { else return 'mdi-sort-alphabetical-ascending' }) +onUnmounted(() => { + newFolderDialogController?.close() +}) + defineExpose({ openNewFolderDialog, }) @@ -176,32 +193,8 @@ defineExpose({ - - - - - - - {{ t('file.newFolder') }} - - - - - - - -
- - {{ t('common.create') }} - - - - + + + diff --git a/src/components/filter/TorrentFilterBar.vue b/src/components/filter/TorrentFilterBar.vue index b7b8aa9b..696b1eac 100644 --- a/src/components/filter/TorrentFilterBar.vue +++ b/src/components/filter/TorrentFilterBar.vue @@ -1,10 +1,10 @@ - - - + diff --git a/src/layouts/components/QuickAccess.vue b/src/layouts/components/QuickAccess.vue index bb73afc4..47ed4b21 100644 --- a/src/layouts/components/QuickAccess.vue +++ b/src/layouts/components/QuickAccess.vue @@ -4,6 +4,7 @@ import type { Plugin } from '@/api/types' import { getLogoUrl } from '@/utils/imageUtils' import { useI18n } from 'vue-i18n' import { useRecentPlugins } from '@/composables/useRecentPlugins' +import { openSharedDialog } from '@/composables/useSharedDialog' import PluginDataDialog from '@/components/dialog/PluginDataDialog.vue' import { VCard } from 'vuetify/components' import { getDominantColor } from '@/@core/utils/image' @@ -66,10 +67,6 @@ const velocity = ref(0) const startedFromBottomArea = ref(false) const quickAccessRef = ref(null) -// 插件弹窗相关状态 -const showPluginDataDialog = ref(false) -const currentPlugin = ref(null) - // Vuetify 组件 ref 在不同构建下可能返回组件实例,这里统一解析为真实 DOM 节点。 function getQuickAccessElement() { const element = quickAccessRef.value @@ -199,9 +196,15 @@ function handlePluginClick(plugin: Plugin) { emit('plugin-click', plugin) - // 设置当前插件并显示数据弹窗 - currentPlugin.value = plugin - showPluginDataDialog.value = true + openSharedDialog( + PluginDataDialog, + { + plugin, + show_switch: false, + }, + {}, + { closeOn: ['close', 'update:modelValue'] }, + ) } // 关闭面板 @@ -209,12 +212,6 @@ function handleClose() { emit('close') } -// 关闭插件数据弹窗 -function handleClosePluginDataDialog() { - showPluginDataDialog.value = false - currentPlugin.value = null -} - // 管理滚动状态 function manageScrollLock() { if (isVisible.value) { @@ -571,15 +568,6 @@ function handleBackdropClick(event: MouseEvent) {
- - - diff --git a/src/layouts/components/UserProfile.vue b/src/layouts/components/UserProfile.vue index c1b982c5..5bec4ec2 100644 --- a/src/layouts/components/UserProfile.vue +++ b/src/layouts/components/UserProfile.vue @@ -3,12 +3,10 @@ import { useToast } from 'vue-toastification' import router from '@/router' import avatar1 from '@images/avatars/avatar-1.png' import api from '@/api' -import ProgressDialog from '@/components/dialog/ProgressDialog.vue' -import UserAuthDialog from '@/components/dialog/UserAuthDialog.vue' -import AboutDialog from '@/components/dialog/AboutDialog.vue' +import { openSharedDialog } from '@/composables/useSharedDialog' import { useAuthStore, useUserStore, useGlobalSettingsStore } from '@/stores' import { useI18n } from 'vue-i18n' -import { useDisplay, useTheme } from 'vuetify' +import { useTheme } from 'vuetify' import { SUPPORTED_LOCALES, SupportedLocale } from '@/types/i18n' import { checkPrefersColorSchemeIsDark } from '@/@core/utils' import { getCurrentLocale, setI18nLanguage } from '@/plugins/i18n' @@ -17,6 +15,13 @@ import type { ThemeSwitcherTheme } from '@layouts/types' import { useConfirm } from '@/composables/useConfirm' import { themeManager } from '@/utils/themeManager' import { usePWA, type UIMode } from '@/composables/usePWA' +import { applyStoredTransparencySettings } from '@/composables/useTransparencySettings' + +const AboutDialog = defineAsyncComponent(() => import('@/components/dialog/AboutDialog.vue')) +const CustomCssDialog = defineAsyncComponent(() => import('@/components/dialog/CustomCssDialog.vue')) +const ProgressDialog = defineAsyncComponent(() => import('@/components/dialog/ProgressDialog.vue')) +const TransparencySettingsDialog = defineAsyncComponent(() => import('@/components/dialog/TransparencySettingsDialog.vue')) +const UserAuthDialog = defineAsyncComponent(() => import('@/components/dialog/UserAuthDialog.vue')) // 认证 Store const authStore = useAuthStore() @@ -26,23 +31,12 @@ const userStore = useUserStore() const globalSettingsStore = useGlobalSettingsStore() // 国际化 const { t } = useI18n() -// 显示器 -const display = useDisplay() // PWA const { uiMode, setUIMode } = usePWA() // 提示框 const $toast = useToast() -// 进度框 -const progressDialog = ref(false) - -// 站点认证对话框 -const siteAuthDialog = ref(false) - -// 自定义CSS弹窗 -const cssDialog = ref(false) - // UI模式菜单是否显示 const showUIModeMenu = ref(false) @@ -55,41 +49,14 @@ const showLanguageMenu = ref(false) // 自定义CSS const customCSS = ref('') -// 透明度相关 -const transparencyOpacity = ref(parseFloat(localStorage.getItem('transparency-opacity') || '0.3')) -const transparencyBlur = ref(parseFloat(localStorage.getItem('transparency-blur') || '10')) -const backgroundPosterOpacity = ref(parseFloat(localStorage.getItem('transparency-background-poster-opacity') || '0')) -const backgroundBlur = ref(parseFloat(localStorage.getItem('transparency-background-blur') || '16')) -const transparencyLevel = ref(localStorage.getItem('transparency-level') || 'medium') const isTransparentTheme = computed(() => currentThemeName.value === 'transparent') -const showTransparencyDialog = ref(false) - -// 关于对话框 -const aboutDialog = ref(false) - -// 预设值配置 -const transparencyPresets = { - low: { opacity: 0.1, blur: 5 }, - medium: { opacity: 0.3, blur: 10 }, - high: { opacity: 0.6, blur: 15 }, -} - -// 判断当前值是否匹配预设值 -const currentPresetLevel = computed(() => { - for (const [level, preset] of Object.entries(transparencyPresets)) { - if ( - Math.abs(transparencyOpacity.value - preset.opacity) < 0.01 && - Math.abs(transparencyBlur.value - preset.blur) < 0.1 - ) { - return level - } - } - return null -}) // 重启轮询控制标识 const restartPollingId = ref(null) const isRestarting = ref(false) +let progressDialogController: ReturnType | null = null +let siteAuthDialogController: ReturnType | null = null +let customCssDialogController: ReturnType | null = null // 确认框 const { createConfirm } = useConfirm() @@ -110,6 +77,18 @@ function logout() { router.push('/login') } +/** 打开重启进度共享弹窗。 */ +function showRestartProgress() { + progressDialogController?.close() + progressDialogController = openSharedDialog(ProgressDialog, { text: t('app.restarting') }, {}, { closeOn: false }) +} + +/** 关闭重启进度共享弹窗。 */ +function closeRestartProgress() { + progressDialogController?.close() + progressDialogController = null +} + // 检测服务状态 async function checkServiceStatus(): Promise { try { @@ -144,7 +123,7 @@ async function pollServiceStatus() { if (isServiceUp) { // 服务已恢复,清理状态并执行注销 isRestarting.value = false - progressDialog.value = false + closeRestartProgress() restartPollingId.value = null setTimeout(() => { @@ -156,7 +135,7 @@ async function pollServiceStatus() { if (retryCount >= maxRetries) { // 超时未恢复,清理状态并提示用户 isRestarting.value = false - progressDialog.value = false + closeRestartProgress() restartPollingId.value = null $toast.error(t('app.restartTimeout')) return @@ -178,19 +157,19 @@ async function restart() { // 调用API重启 try { // 显示等待框 - progressDialog.value = true + showRestartProgress() const result: { [key: string]: any } = await api.get('system/restart') if (!result?.success) { // 重启失败,清理状态 isRestarting.value = false - progressDialog.value = false + closeRestartProgress() $toast.error(result.message) return } } catch (error) { // 重启失败,清理状态 isRestarting.value = false - progressDialog.value = false + closeRestartProgress() console.error(error) return } @@ -214,19 +193,28 @@ async function showRestartDialog() { await restart() } -// 显示站点认证对话框 +/** 显示站点认证共享弹窗。 */ function showSiteAuthDialog() { - siteAuthDialog.value = true + siteAuthDialogController?.close() + siteAuthDialogController = openSharedDialog( + UserAuthDialog, + {}, + { + done: siteAuthDone, + }, + { closeOn: ['close', 'update:modelValue'] }, + ) } -// 显示关于对话框 +/** 显示关于共享弹窗。 */ function showAboutDialog() { - aboutDialog.value = true + openSharedDialog(AboutDialog, {}, {}, { closeOn: ['close', 'update:modelValue'] }) } -// 用户站点认证成功 +/** 用户站点认证成功后关闭弹窗并退出登录。 */ function siteAuthDone() { - siteAuthDialog.value = false + siteAuthDialogController?.close() + siteAuthDialogController = null logout() } @@ -335,7 +323,7 @@ async function changeTheme(theme: string) { // 如果是透明主题,应用透明度设置 if (theme === 'transparent') { - applyTransparencySettings() + applyStoredTransparencySettings() } // 保存主题到服务端 @@ -365,110 +353,47 @@ async function getCustomCSS() { } } -// 保存自定义 CSS -async function saveCustomCSS() { - cssDialog.value = false +/** 打开自定义 CSS 共享弹窗。 */ +function showCustomCssDialog() { + customCssDialogController?.close() + customCssDialogController = openSharedDialog( + CustomCssDialog, + { + css: customCSS.value, + editorTheme: editorTheme.value, + }, + { + save: saveCustomCSS, + }, + { closeOn: ['close', 'update:modelValue'] }, + ) +} + +/** 打开透明主题设置共享弹窗。 */ +function showTransparencySettingsDialog() { + openSharedDialog(TransparencySettingsDialog, {}, {}, { closeOn: ['close', 'update:modelValue'] }) +} + +/** 保存自定义 CSS。 */ +async function saveCustomCSS(css: string) { + customCSS.value = css try { - const result: { [key: string]: any } = await api.post('system/setting/UserCustomCSS', customCSS.value, { + const result: { [key: string]: any } = await api.post('system/setting/UserCustomCSS', css, { headers: { 'Content-Type': 'text/plain', }, }) - if (result.success) $toast.success(t('theme.customCssSaveSuccess')) + if (result.success) { + customCssDialogController?.close() + customCssDialogController = null + $toast.success(t('theme.customCssSaveSuccess')) + } } catch (e) { console.error(t('theme.customCssSaveFailed')) } } -// 应用透明度设置 -function applyTransparencySettings() { - const root = document.documentElement - - if (!Number.isFinite(backgroundPosterOpacity.value)) { - backgroundPosterOpacity.value = 1 - } - backgroundPosterOpacity.value = Math.min(1, Math.max(0, backgroundPosterOpacity.value)) - if (!Number.isFinite(backgroundBlur.value)) { - backgroundBlur.value = 16 - } - backgroundBlur.value = Math.min(30, Math.max(0, backgroundBlur.value)) - - // 设置CSS变量 - root.style.setProperty('--transparent-opacity', transparencyOpacity.value.toString()) - root.style.setProperty('--transparent-opacity-light', (transparencyOpacity.value * 0.67).toString()) - root.style.setProperty('--transparent-opacity-heavy', (transparencyOpacity.value * 1.67).toString()) - root.style.setProperty('--transparent-blur', `${transparencyBlur.value}px`) - root.style.setProperty('--transparent-blur-light', `${transparencyBlur.value * 0.6}px`) - root.style.setProperty('--transparent-blur-heavy', `${transparencyBlur.value * 1.6}px`) - root.style.setProperty('--transparent-background-poster-opacity', (1 - backgroundPosterOpacity.value).toString()) - root.style.setProperty('--transparent-background-blur', `${backgroundBlur.value}px`) - - // 保存到本地存储 - localStorage.setItem('transparency-opacity', transparencyOpacity.value.toString()) - localStorage.setItem('transparency-blur', transparencyBlur.value.toString()) - localStorage.setItem('transparency-background-poster-opacity', backgroundPosterOpacity.value.toString()) - localStorage.setItem('transparency-background-blur', backgroundBlur.value.toString()) -} - -// 调整透明度预设 -function adjustTransparency(level: string) { - transparencyLevel.value = level - localStorage.setItem('transparency-level', level) - - // 设置预设值 - switch (level) { - case 'low': - transparencyOpacity.value = 0.1 - transparencyBlur.value = 5 - break - case 'medium': - transparencyOpacity.value = 0.3 - transparencyBlur.value = 10 - break - case 'high': - transparencyOpacity.value = 0.6 - transparencyBlur.value = 15 - break - } - - applyTransparencySettings() -} - -// 透明度变化处理 -function onOpacityChange() { - applyTransparencySettings() - // 清除预设级别,因为用户手动调整了 - transparencyLevel.value = '' -} - -// 模糊度变化处理 -function onBlurChange() { - applyTransparencySettings() - // 清除预设级别,因为用户手动调整了 - transparencyLevel.value = '' -} - -// 背景海报透明度变化处理 -function onBackgroundPosterOpacityChange() { - applyTransparencySettings() -} - -// 背景磨砂变化处理 -function onBackgroundBlurChange() { - applyTransparencySettings() -} - -// 重置透明度设置 -function resetTransparencySettings() { - transparencyOpacity.value = 0.3 - transparencyBlur.value = 10 - backgroundPosterOpacity.value = 0 - backgroundBlur.value = 16 - transparencyLevel.value = 'medium' - applyTransparencySettings() -} - // 监听主题变化 watch( () => currentThemeName.value, @@ -477,7 +402,7 @@ watch( // 如果切换到透明主题,应用透明度设置 if (currentThemeName.value === 'transparent') { - applyTransparencySettings() + applyStoredTransparencySettings() } }, ) @@ -534,7 +459,7 @@ onMounted(() => { // 初始化透明度设置 if (isTransparentTheme.value) { - applyTransparencySettings() + applyStoredTransparencySettings() } }) @@ -546,6 +471,9 @@ onUnmounted(() => { restartPollingId.value = null } isRestarting.value = false + closeRestartProgress() + siteAuthDialogController?.close() + customCssDialogController?.close() }) @@ -677,7 +605,7 @@ onUnmounted(() => { - + @@ -687,7 +615,7 @@ onUnmounted(() => { diff --git a/src/pages/discover.vue b/src/pages/discover.vue index 192a4781..31524640 100644 --- a/src/pages/discover.vue +++ b/src/pages/discover.vue @@ -1,6 +1,5 @@ @@ -1021,45 +1161,6 @@ onUnmounted(() => { - - - - - - - {{ confirmTitle }} - -
- - {{ t('transferHistory.deleteRecordOnly') }} - - - {{ t('transferHistory.deleteSourceOnly') }} - - - {{ t('transferHistory.deleteDestOnly') }} - - - {{ t('transferHistory.deleteAll') }} - -
-
-
- - - - - - - -
@@ -1095,7 +1196,7 @@ onUnmounted(() => { color="primary" appear class="compact-fab compact-fab--primary" - @click="transferQueueDialog = true" + @click="openTransferQueueDialog" />
diff --git a/src/views/setting/AccountSettingDirectory.vue b/src/views/setting/AccountSettingDirectory.vue index 6ab0ac04..64739be7 100644 --- a/src/views/setting/AccountSettingDirectory.vue +++ b/src/views/setting/AccountSettingDirectory.vue @@ -9,6 +9,7 @@ import { useI18n } from 'vue-i18n' import { useTheme } from 'vuetify' import { storageAttributes } from '@/api/constants' import { useSilentSettingRefresh } from '@/composables/useSilentSettingRefresh' +import { openSharedDialog } from '@/composables/useSharedDialog' const { t } = useI18n() const { global: globalTheme } = useTheme() @@ -22,7 +23,6 @@ const props = defineProps({ // 拖拽排序和分类编辑弹窗按需加载,避免设置框架预加载目录页时带上这些交互依赖。 const Draggable = defineAsyncComponent(() => import('vuedraggable').then(module => module.default)) -const ProgressDialog = defineAsyncComponent(() => import('@/components/dialog/ProgressDialog.vue')) const CategoryEditDialog = defineAsyncComponent(() => import('@/components/dialog/CategoryEditDialog.vue')) // 所有下载目录 @@ -37,12 +37,6 @@ const mediaCategories = ref<{ [key: string]: any }>({}) // 提示框 const $toast = useToast() -// 进度框 -const progressDialog = ref(false) - -// 分类编辑对话框 -const categoryDialog = ref(false) - // 数据源 const sourceItems = [ { 'title': 'TheMovieDb', 'value': 'themoviedb' }, @@ -79,6 +73,18 @@ const renameEditorOptions = { showGutter: true, } +// 打开共享分类编辑弹窗,保存后刷新本页分类配置。 +function openCategoryDialog() { + openSharedDialog( + CategoryEditDialog, + {}, + { + save: loadMediaCategories, + }, + { closeOn: ['close', 'save', 'update:modelValue'] }, + ) +} + const movieRenameFormat = computed({ get: () => SystemSettings.value.Basic.MOVIE_RENAME_FORMAT ?? '', set: (value: string) => { @@ -357,7 +363,7 @@ useSilentSettingRefresh(loadPageData, { - + {{ t('setting.category.title') }} @@ -443,16 +449,6 @@ useSilentSettingRefresh(loadPageData, { - - - -