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 @@ - - - - - - - - - - - {{ props.rule.name }} - {{ props.rule.id }} - - - - - - - - - - - - - {{ t('customRule.title', { id: props.rule.id }) }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ - t('customRule.action.confirm') - }} - - - - + + + + + + + + + + {{ props.rule.name }} + {{ props.rule.id }} + + + + + + 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 @@ - - - - - - - - - - - - - - {{ downloader.name }} - - - {{ `↑ ${formatFileSize(upload_rate, 1)}/s` }} - {{ `↓ ${formatFileSize(download_rate, 1)}/s` }} - - - 自定义下载器 - - - - - - - - - - + - - - - - - {{ t('common.config') }} - {{ props.downloader.name }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ t('downloader.pathMapping') }} - - - - - {{ t('common.noData') }} - - - - - - - - - {{ t('downloader.storagePath') }} - - - - updateStoragePrefix(row, v)" - /> - - - updateStorageSuffix(row, v)" - /> - - - - - - - - - - - {{ t('downloader.downloadPath') }} - - - - - - - - - - - - - - - {{ t('common.add') }} {{ t('downloader.pathMapping') }} - - - - - - - - {{ t('common.save') }} - - - - - + + + + + + + + + + + {{ downloader.name }} + + + {{ `↑ ${formatFileSize(upload_rate, 1)}/s` }} + {{ `↓ ${formatFileSize(download_rate, 1)}/s` }} + + + {{ t('setting.system.custom') }} + + + + + + + + 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 @@ - - - - - - - - - - - {{ props.group.name }} - - {{ props.group.media_type || t('common.all') }} - {{ props.group.category }} - + + + + + + + + + + {{ props.group.name }} + + {{ props.group.media_type || t('common.all') }} + {{ props.group.category }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ t('filterRule.add') }} - - - - - - - - - - - - - - {{ t('common.save') }} - - - - - - + + + + + + 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 @@ + + + + + + + + + {{ t('theme.custom') }} + + + + + + + + + + + + {{ t('common.save') }} + + + + + 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 @@ + + + + + + + + + + {{ t('customRule.title', { id: props.rule.id }) }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ t('customRule.action.confirm') }} + + + + + 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 @@ + + + + + + + + + {{ t('discover.setTabOrder') }} + + + + + + {{ t('discover.dragToReorder') }} + + + + + {{ element.name }} + + + + + + + + + + + + + {{ t('common.save') }} + + + + + + + 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 @@ + + + + + + + + + + {{ t('common.config') }} + {{ props.downloader.name }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ t('downloader.pathMapping') }} + + + + + {{ t('common.noData') }} + + + + + + + + + {{ t('downloader.storagePath') }} + + + + updateStoragePrefix(row, v)" + /> + + + updateStorageSuffix(row, v)" + /> + + + + + + + + + + + {{ t('downloader.downloadPath') }} + + + + + + + + + + + + + + + {{ t('common.add') }} {{ t('downloader.pathMapping') }} + + + + + + + + {{ t('common.save') }} + + + + + 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 @@ + + + + + + + + + + {{ t('file.newFolder') }} + + + + + + + + + + {{ t('common.create') }} + + + + + 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 @@ + + + + + + + + + + {{ t('file.rename') }} + + + + + + + + + + + + + + + + {{ t('file.autoRecognizeName') }} + + + {{ t('common.confirm') }} + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ t('filterRule.add') }} + + + + + + + + + + + + + + {{ t('common.save') }} + + + + + 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 @@ + + + + + + {{ t('setting.system.llmProviderAuthDialogTitle') }} + + + {{ props.authSession.instructions }} + + + + {{ t('setting.system.llmProviderPopupBlocked') }} + + + + {{ t('setting.system.llmProviderDeviceCode') }} + {{ props.authSession.user_code }} + + + + {{ props.authSession.message }} + + + + + {{ t('setting.system.llmProviderOpenAuthPage') }} + + + {{ t('setting.system.llmProviderCheckAuthStatus') }} + + + + + + + {{ t('common.close') }} + + + + + 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 @@ + + + + + + {{ t('login.secondaryVerification') }} + + {{ t('login.mfa.selectVerificationMethod') }} + + + + + + + {{ t('login.loginWithOtp') }} + + + + + + + + {{ t('login.orUsePasskey') }} + + {{ t('login.verifyWithPasskey') }} + + + + + + {{ props.errorMessage }} + + + {{ t('common.cancel') }} + + + + 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 @@ + + + + + + + + + + {{ t('common.config') }} + {{ props.mediaserver.name }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ t('common.confirm') }} + + + + + 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 @@ + + + + + + + + + + {{ t('common.config') }} + {{ props.notification.name }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ t('notification.wechatclawbot.loginStatus') }} + {{ wechatClawBotStatusText }} + + + + {{ t('common.refresh') }} + + + {{ t('notification.wechatclawbot.refreshQrcode') }} + + + {{ t('notification.wechatclawbot.logout') }} + + + + + + + + + + {{ t('notification.wechatclawbot.noQrcode') }} + + + + + + {{ t('notification.wechatclawbot.scanHint') }} + + {{ t('notification.wechatclawbot.accountId') }}: {{ wechatClawBotStatus.account_id }} + + + {{ t('notification.wechatclawbot.qrcodeUpdatedAt') }}: + {{ formatWechatClawBotTime(wechatClawBotStatus.qrcode_updated_at) }} + + + {{ t('notification.wechatclawbot.knownTargets') }} + + + + + {{ t('notification.wechatclawbot.noKnownTargets') }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ t('common.confirm') }} + + + + + 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 @@ + + + + + + + + + + + {{ t('setting.notification.templateConfigTitle') }} + + + {{ props.subtitle }} + + + + + + + + + {{ t('common.save') }} + + + + + 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 @@ + + + + + + + + + + + + + + {{ props.type === 'online' ? t('app.online') : t('app.offline') }} + + + + {{ statusText }} + + + + + + {{ retrying ? t('common.checking') : t('common.retry') }} + + + + + + {{ isOnline ? t('common.networkOnline') : t('common.networkOffline') }} + + + + {{ canPerformNetworkAction ? t('common.serviceAvailable') : t('common.serviceUnavailable') }} + + + + + + + + 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 @@ + + + + + + + + + + {{ t('plugin.cloneTitle') }} + {{ t('plugin.cloneSubtitle', { name: props.plugin?.plugin_name }) }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ t('common.notice') }}:{{ t('plugin.cloneNotice') }} + + + + + + + + + + {{ t('plugin.createClone') }} + + + + + 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 @@ + + + + + + + + {{ t('plugin.newFolder') }} + + + + + + + + + {{ t('plugin.create') }} + + + + + 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 @@ + + + + + + + + + + {{ t('folder.renameFolder') }} + + + + + + + + + 确认 + + + + 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 @@ + + + + + + + + + + {{ t('folder.folderAppearanceSettings') }} + + + + + + + + + + + {{ t('folder.icon') }} + + + + + + + + + {{ t('folder.iconColor') }} + + + + + + + + + {{ t('folder.backgroundGradient') }} + + + + + + + + + + + + + + + 保存 + + + + 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 @@ + + + + + + + + + + {{ t('plugin.logTitle') }} + + + + {{ t('common.openInNewWindow') }} + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + {{ props.plugin?.plugin_name }} + + + {{ props.plugin?.plugin_desc }} + + + + + {{ t('common.version') }}: + v{{ props.plugin?.plugin_version }} + + + + + {{ t('common.author') }}: + + {{ props.plugin?.plugin_author }} + + + + + + + {{ t('plugin.installToLocal') }} + + + + {{ t('plugin.totalDownloads', { count: formatDownloadCount(props.count) }) }} + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + {{ item.plugin_name }}v{{ item?.plugin_version }} + + + + + {{ label }} + + {{ item.plugin_desc }} + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + {{ t('plugin.updateToLatest') }} + + + + + + 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)) @@ -14,7 +17,12 @@ const props = defineProps({ {{ props.text || t('dialog.progress.processing') }} - + diff --git a/src/components/dialog/SharedDialogHost.vue b/src/components/dialog/SharedDialogHost.vue new file mode 100644 index 00000000..34fb9831 --- /dev/null +++ b/src/components/dialog/SharedDialogHost.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/src/components/dialog/ShortcutLogDialog.vue b/src/components/dialog/ShortcutLogDialog.vue new file mode 100644 index 00000000..06b147a8 --- /dev/null +++ b/src/components/dialog/ShortcutLogDialog.vue @@ -0,0 +1,61 @@ + + + + + + + + + + {{ t('shortcut.log.subtitle') }} + + + + {{ t('common.openInNewWindow') }} + + + + + + + + + + + diff --git a/src/components/dialog/ShortcutMessageDialog.vue b/src/components/dialog/ShortcutMessageDialog.vue new file mode 100644 index 00000000..34cd0d86 --- /dev/null +++ b/src/components/dialog/ShortcutMessageDialog.vue @@ -0,0 +1,139 @@ + + + + + + + + + {{ t('shortcut.message.subtitle') }} + + + + + + + + + + + + {{ t('common.send') }} + + + + + + diff --git a/src/components/dialog/ShortcutToolDialog.vue b/src/components/dialog/ShortcutToolDialog.vue new file mode 100644 index 00000000..c58497d5 --- /dev/null +++ b/src/components/dialog/ShortcutToolDialog.vue @@ -0,0 +1,82 @@ + + + + + + + + + {{ props.title }} + + {{ props.subtitle }} + + + + + + + + + + + diff --git a/src/components/dialog/StorageCustomConfigDialog.vue b/src/components/dialog/StorageCustomConfigDialog.vue new file mode 100644 index 00000000..ec01497f --- /dev/null +++ b/src/components/dialog/StorageCustomConfigDialog.vue @@ -0,0 +1,99 @@ + + + + + + + + + + {{ t('storage.custom') }} + + + + + + + + + + + + + + + + {{ t('common.save') }} + + + + + diff --git a/src/components/dialog/TorrentAllFiltersDialog.vue b/src/components/dialog/TorrentAllFiltersDialog.vue new file mode 100644 index 00000000..27fb8fe3 --- /dev/null +++ b/src/components/dialog/TorrentAllFiltersDialog.vue @@ -0,0 +1,144 @@ + + + + + + + + + {{ t('torrent.allFilters') }} + + + {{ t('torrent.clearAll') }} + + + + + + + + + + + {{ title }} + + + {{ t('torrent.selectAll') }} + + + {{ t('torrent.clear') }} + + + + + updateFilter(String(key), val)" + > + + {{ option }} + + + + + + + + + + + diff --git a/src/components/dialog/TorrentMoreSourcesDialog.vue b/src/components/dialog/TorrentMoreSourcesDialog.vue new file mode 100644 index 00000000..52fd1e5d --- /dev/null +++ b/src/components/dialog/TorrentMoreSourcesDialog.vue @@ -0,0 +1,145 @@ + + + + + + + 其他来源 + + + + + + + + + + + + + + {{ item.torrent_info?.site_name?.substring(0, 1) }} + + {{ item.torrent_info.site_name }} + + + {{ item.meta_info.season_episode }} + + + + {{ item.torrent_info?.volume_factor }} + + + + + + + + {{ formatFileSize(item.torrent_info?.size) }} + + + + {{ item.torrent_info?.seeders }} + + + + + + + + + + + + + + diff --git a/src/components/dialog/TorrentSingleFilterDialog.vue b/src/components/dialog/TorrentSingleFilterDialog.vue new file mode 100644 index 00000000..e808ee3d --- /dev/null +++ b/src/components/dialog/TorrentSingleFilterDialog.vue @@ -0,0 +1,108 @@ + + + + + + + + {{ props.filterTitle }} + + + {{ t('torrent.clear') }} + + + {{ t('torrent.selectAll') }} + + + + + + + {{ option }} + + + + + + + {{ t('torrent.confirm') }} + + + + + diff --git a/src/components/dialog/TransferHistoryDeleteDialog.vue b/src/components/dialog/TransferHistoryDeleteDialog.vue new file mode 100644 index 00000000..66da9a88 --- /dev/null +++ b/src/components/dialog/TransferHistoryDeleteDialog.vue @@ -0,0 +1,56 @@ + + + + + + + + {{ props.title }} + + + + {{ $t('transferHistory.deleteRecordOnly') }} + + + {{ $t('transferHistory.deleteSourceOnly') }} + + + {{ $t('transferHistory.deleteDestOnly') }} + + + {{ $t('transferHistory.deleteAll') }} + + + + + diff --git a/src/components/dialog/TransparencySettingsDialog.vue b/src/components/dialog/TransparencySettingsDialog.vue new file mode 100644 index 00000000..d7f60991 --- /dev/null +++ b/src/components/dialog/TransparencySettingsDialog.vue @@ -0,0 +1,166 @@ + + + + + + + + + {{ t('theme.transparencyAdjust') }} + + + + + + + + + {{ t('theme.transparencyOpacity') }} + {{ Math.round(transparencyOpacity * 100) }}% + + + + + + + {{ t('theme.transparencyBlur') }} + {{ transparencyBlur }}px + + + + + + + {{ t('theme.backgroundPosterOpacity') }} + {{ Math.round(backgroundPosterOpacity * 100) }}% + + + + + + + {{ t('theme.backgroundBlur') }} + {{ backgroundBlur }}px + + + + + + {{ t('common.preset') }} + + + {{ t('theme.transparencyLow') }} + + + {{ t('theme.transparencyMedium') }} + + + {{ t('theme.transparencyHigh') }} + + + + + + + + + + + + {{ t('theme.transparencyReset') }} + + + {{ t('common.confirm') }} + + + + + diff --git a/src/components/dialog/VerifyPasswordDialog.vue b/src/components/dialog/VerifyPasswordDialog.vue new file mode 100644 index 00000000..2ead97dd --- /dev/null +++ b/src/components/dialog/VerifyPasswordDialog.vue @@ -0,0 +1,71 @@ + + + + + + {{ props.title }} + + {{ props.text }} + + + + + {{ t('common.cancel') }} + + + {{ t('common.confirm') }} + + + + + + + diff --git a/src/components/filebrowser/FileList.vue b/src/components/filebrowser/FileList.vue index 61d4c878..d98d0a91 100644 --- a/src/components/filebrowser/FileList.vue +++ b/src/components/filebrowser/FileList.vue @@ -3,18 +3,21 @@ import type { AxiosRequestConfig, AxiosInstance } from 'axios' import type { PropType } from 'vue' import { useConfirm } from '@/composables/useConfirm' import { useToast } from 'vue-toastification' -import ReorganizeDialog from '../dialog/ReorganizeDialog.vue' import { formatBytes } from '@core/utils/formatters' import type { Context, EndPoints, FileItem } from '@/api/types' import api from '@/api' -import ProgressDialog from '../dialog/ProgressDialog.vue' import { useDisplay } from 'vuetify' -import MediaInfoDialog from '../dialog/MediaInfoDialog.vue' import { useI18n } from 'vue-i18n' import { useBackground } from '@/composables/useBackground' import { usePWA } from '@/composables/usePWA' import { useAvailableHeight } from '@/composables/useAvailableHeight' import { useKeepAliveRefresh, type KeepAliveRefreshContext } from '@/composables/useKeepAliveRefresh' +import { openSharedDialog } from '@/composables/useSharedDialog' + +const FileRenameDialog = defineAsyncComponent(() => import('../dialog/FileRenameDialog.vue')) +const MediaInfoDialog = defineAsyncComponent(() => import('../dialog/MediaInfoDialog.vue')) +const ProgressDialog = defineAsyncComponent(() => import('../dialog/ProgressDialog.vue')) +const ReorganizeDialog = defineAsyncComponent(() => import('../dialog/ReorganizeDialog.vue')) // 国际化 const { t } = useI18n() @@ -76,9 +79,6 @@ const loading = ref(true) // 重命名loading const renameLoading = ref(false) -// 识别进度条 -const progressDialog = ref(false) - // 识别进度文本 const progressText = ref(t('common.pleaseWait')) @@ -94,12 +94,6 @@ const filter = ref('') // 是否忽略大小写 const ignoreCase = ref(true) -// 重命名弹窗 -const renamePopper = ref(false) - -// 整理弹窗 -const transferPopper = ref(false) - // 新名称 const newName = ref('') @@ -156,8 +150,20 @@ function setItemSelected(item: FileItem, checked: boolean) { // 识别结果 const nameTestResult = ref() -// 识别结果对话框 -const nameTestDialog = ref(false) +let renameDialogController: ReturnType | null = null +let progressDialogController: ReturnType | null = null + +// 打开共享进度弹窗并记录控制器,方便 SSE 更新文本和进度值。 +function openProgressDialog(text = progressText.value, value = progressValue.value) { + progressDialogController?.close() + progressDialogController = openSharedDialog(ProgressDialog, { text, value }, {}, { closeOn: false }) +} + +// 关闭当前共享进度弹窗。 +function closeProgressDialog() { + progressDialogController?.close() + progressDialogController = null +} // 弹出菜单 const dropdownItems = ref<{ [key: string]: any }[]>([]) @@ -318,17 +324,18 @@ async function batchDelete() { if (!confirmed) return // 显示进度条 - progressDialog.value = true progressValue.value = 0 + openProgressDialog(progressText.value, progressValue.value) // 删除选中的项目 selected.value.every(async item => { progressText.value = t('file.deleting', { name: item.name }) + progressDialogController?.updateProps({ text: progressText.value }) await deleteItem(item, false) }) // 关闭进度条 - progressDialog.value = false + closeProgressDialog() // 重新加载 list_files() @@ -408,12 +415,39 @@ function showRenmae(item: FileItem) { currentItem.value = item newName.value = item.name renameAll.value = false - renamePopper.value = true + openRenameDialog() +} + +// 打开共享重命名弹窗,并双向同步当前文件名和递归选项。 +function openRenameDialog() { + renameDialogController = openSharedDialog( + FileRenameDialog, + { + item: currentItem.value, + loading: renameLoading.value, + name: newName.value, + recursive: renameAll.value, + }, + { + 'auto-name': get_recommend_name, + rename, + 'update:name': (value: string) => { + newName.value = value + renameDialogController?.updateProps({ name: value }) + }, + 'update:recursive': (value: boolean) => { + renameAll.value = value + renameDialogController?.updateProps({ recursive: value }) + }, + }, + { closeOn: ['close'] }, + ) } // 调用API获取新名称 async function get_recommend_name() { renameLoading.value = true + renameDialogController?.updateProps({ loading: true }) try { const result: { [key: string]: any } = await api.get('transfer/name', { params: { @@ -430,23 +464,21 @@ async function get_recommend_name() { console.error(error) } renameLoading.value = false + renameDialogController?.updateProps({ loading: false, name: newName.value }) } // 重命名 async function rename() { emit('loading', true) - // 关闭弹窗 - renamePopper.value = false - // 显示进度条 - progressDialog.value = true progressValue.value = 0 if (renameAll.value) { progressText.value = t('file.renamingAll', { path: currentItem.value?.path }) } else { progressText.value = t('file.renaming', { name: currentItem.value?.name }) } + openProgressDialog(progressText.value, progressValue.value) if (renameAll.value) { startLoadingProgress() } @@ -471,11 +503,13 @@ async function rename() { if (renameAll.value) { stopLoadingProgress() } - progressDialog.value = false + closeProgressDialog() // 通知重新加载 newName.value = '' renameAll.value = false + renameDialogController?.close() + renameDialogController = null emit('loading', false) emit('renamed') } @@ -483,21 +517,35 @@ async function rename() { // 显示整理对话框 function showTransfer(item: FileItem) { transferItems.value = [item] - transferPopper.value = true + openTransferDialog() } // 显示批量整理对话框 function showBatchTransfer() { transferItems.value = dedupeFileItems(selected.value) - transferPopper.value = true + openTransferDialog() } // 整理完成 function transferDone() { - transferPopper.value = false list_files() } +// 打开共享文件整理弹窗,整理完成后刷新当前目录。 +function openTransferDialog() { + openSharedDialog( + ReorganizeDialog, + { + items: transferItems.value, + target_storage: inProps.item.storage, + }, + { + done: transferDone, + }, + { closeOn: ['close', 'done'] }, + ) +} + // 将文件修改时间(timestape)转换为本地时间 function formatTime(timestape: number) { return new Date(timestape * 1000).toLocaleString() @@ -528,7 +576,6 @@ watch( selected.value = [] // 关闭弹窗 nameTestResult.value = undefined - nameTestDialog.value = false // 重置菜单 dropdownItems.value = [ { @@ -591,19 +638,22 @@ watch( async function recognize(path: string) { try { // 显示进度条 - progressDialog.value = true progressText.value = t('file.recognizing', { path }) progressValue.value = 0 + openProgressDialog(progressText.value, progressValue.value) nameTestResult.value = await api.get('media/recognize_file', { params: { path, }, }) // 关闭进度条 - progressDialog.value = false + closeProgressDialog() if (!nameTestResult.value) $toast.error(t('file.recognizeFailed', { path })) - nameTestDialog.value = !!nameTestResult.value?.meta_info?.name + if (nameTestResult.value?.meta_info?.name) { + openSharedDialog(MediaInfoDialog, { context: nameTestResult.value }, {}, { closeOn: ['close'] }) + } } catch (error) { + closeProgressDialog() console.error(error) } } @@ -621,16 +671,17 @@ async function scrape(item: FileItem, confirm: boolean = true) { } // 显示进度条 - progressDialog.value = true progressText.value = t('file.scraping', { path: item.path }) + openProgressDialog(progressText.value) const result: { [key: string]: any } = await api.post(`media/scrape/${inProps.item.storage}`, item) // 关闭进度条 - progressDialog.value = false + closeProgressDialog() if (!result.success) $toast.error(result.message) else $toast.success(t('file.scrapeCompleted', { path: item.path })) } catch (error) { + closeProgressDialog() console.error(error) } } @@ -655,6 +706,7 @@ function handleProgressMessage(event: MessageEvent) { if (progress) { progressText.value = progress.text progressValue.value = progress.value + progressDialogController?.updateProps({ text: progressText.value, value: progressValue.value }) } } @@ -686,6 +738,8 @@ useKeepAliveRefresh(list_files, { onUnmounted(() => { revokeCurrentImgLink() stopLoadingProgress() + closeProgressDialog() + renameDialogController?.close() }) @@ -850,59 +904,5 @@ onUnmounted(() => { {{ t('file.emptyDirectory') }} - - - - - - - - {{ t('file.rename') }} - - - - - - - - - - - - - - - - {{ t('file.autoRecognizeName') }} - - - {{ t('common.confirm') }} - - - - - - - - - - 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 @@ - - - - - - - - - - - - - - {{ props.type === 'online' ? t('app.online') : t('app.offline') }} - - - - {{ statusText }} - - - - - - - {{ retrying ? t('common.checking') : t('common.retry') }} - - - - - - - {{ isOnline ? t('common.networkOnline') : t('common.networkOffline') }} - - - - {{ canPerformNetworkAction ? t('common.serviceAvailable') : t('common.serviceUnavailable') }} - - - - - - - - + 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(() => { - + @@ -774,161 +702,6 @@ onUnmounted(() => { - - - - - - - - - - - - {{ t('theme.custom') }} - - - - - - - - - - - - {{ t('common.save') }} - - - - - - - - - - - - {{ t('theme.transparencyAdjust') }} - - - - - - - - - - {{ t('theme.transparencyOpacity') }} - {{ Math.round(transparencyOpacity * 100) }}% - - - - - - - - {{ t('theme.transparencyBlur') }} - {{ transparencyBlur }}px - - - - - - - - {{ t('theme.backgroundPosterOpacity') }} - {{ Math.round(backgroundPosterOpacity * 100) }}% - - - - - - - - {{ t('theme.backgroundBlur') }} - {{ backgroundBlur }}px - - - - - - - {{ t('common.preset') }} - - - {{ t('theme.transparencyLow') }} - - - {{ t('theme.transparencyMedium') }} - - - {{ t('theme.transparencyHigh') }} - - - - - - - - - - - - {{ t('theme.transparencyReset') }} - - - {{ t('common.confirm') }} - - - - - - - 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, { - - - -
{{ t('discover.dragToReorder') }}
{{ t('login.mfa.selectVerificationMethod') }}
{{ t('login.orUsePasskey') }}
+ {{ statusText }} +
{{ props.text }}
- {{ statusText }} -