diff --git a/shims.d.ts b/shims.d.ts
index 35041540..c044f34a 100644
--- a/shims.d.ts
+++ b/shims.d.ts
@@ -12,3 +12,4 @@ declare module 'vue-prism-component' {
export default component
}
declare module 'vue-shepherd';
+declare module 'colorthief';
diff --git a/src/@core/scss/_mixins.scss b/src/@core/scss/_mixins.scss
index bdf641e9..301ed153 100644
--- a/src/@core/scss/_mixins.scss
+++ b/src/@core/scss/_mixins.scss
@@ -1,5 +1,5 @@
@use "sass:map";
-@use "vuetify/lib/styles/settings" as vuetify_settings;
+@use "vuetify/lib/styles/settings/_index.sass" as vuetify_settings;
@use "@styles/variables/_vuetify.scss" as vuetify;
@mixin themed($property, $light-value, $dark-value) {
diff --git a/src/@validators/index.ts b/src/@validators/index.ts
index 58cef60b..0ce5a144 100644
--- a/src/@validators/index.ts
+++ b/src/@validators/index.ts
@@ -1,4 +1,4 @@
-import type { ValidationRule } from 'vuetify/types/services/validation'
+type ValidationRule = (value: any) => string | boolean
// 必输校验
export const requiredValidator: ValidationRule = (value: any) => {
diff --git a/src/components/FileBrowser.vue b/src/components/FileBrowser.vue
index c8f1817c..eec6b6a5 100644
--- a/src/components/FileBrowser.vue
+++ b/src/components/FileBrowser.vue
@@ -4,6 +4,7 @@ import FileToolbar from './filebrowser/FileToolbar.vue'
import FileNavigator from './filebrowser/FileNavigator.vue'
import type { EndPoints, FileItem, StorageConf } from '@/api/types'
import { storageIconDict } from '@/api/constants'
+import type { AxiosInstance } from 'axios'
// LocalStorage keys
const SORT_KEY = 'fileBrowser.sort'
@@ -16,7 +17,7 @@ const props = defineProps({
tree: Boolean,
endpoints: Object as PropType
,
axios: {
- type: Function,
+ type: Object as PropType,
required: true,
},
axiosconfig: Object,
diff --git a/src/components/dialog/AboutDialog.vue b/src/components/dialog/AboutDialog.vue
index a1a80c37..853ea2a7 100644
--- a/src/components/dialog/AboutDialog.vue
+++ b/src/components/dialog/AboutDialog.vue
@@ -1,12 +1,18 @@
+
+
diff --git a/src/composables/usePWA.ts b/src/composables/usePWA.ts
index 22077622..7007cd7e 100644
--- a/src/composables/usePWA.ts
+++ b/src/composables/usePWA.ts
@@ -12,6 +12,16 @@ const globalPwaStatus = ref<{
const globalLoading = ref(false)
let initPromise: Promise | null = null
+// UI模式设置
+export type UIMode = 'auto' | 'desktop' | 'app'
+const uiMode = ref((localStorage.getItem('ui-mode') as UIMode) || 'auto')
+
+// 设置UI模式
+function setUIMode(mode: UIMode) {
+ uiMode.value = mode
+ localStorage.setItem('ui-mode', mode)
+}
+
// 全局初始化函数
async function initializePWAGlobally() {
if (initPromise) return initPromise
@@ -50,6 +60,8 @@ export function usePWA() {
})
const appMode = computed(() => {
+ if (uiMode.value === 'app') return true
+ if (uiMode.value === 'desktop') return false
return pwaMode.value && display.mdAndDown.value
})
@@ -70,6 +82,8 @@ export function usePWA() {
pwaMode,
appMode,
pwaStatus,
+ uiMode,
+ setUIMode,
loading: globalLoading,
initializePWA: initializePWAGlobally,
}
diff --git a/src/composables/usePullDownGesture.ts b/src/composables/usePullDownGesture.ts
index 787329bb..439b3947 100644
--- a/src/composables/usePullDownGesture.ts
+++ b/src/composables/usePullDownGesture.ts
@@ -236,16 +236,15 @@ export function usePullDownGesture(options: PullDownOptions = {}) {
}
}
- // PWA状态确定后,一次性决定是否添加事件监听器
+ // 监听 appMode 变化动态添加/移除事件监听器
onMounted(() => {
- // 等待PWA检测完成后添加事件监听器
- const stopWatcher = watch(
+ watch(
appMode,
newValue => {
if (newValue) {
addEventListeners()
- // PWA状态确定后停止监听
- stopWatcher()
+ } else {
+ removeEventListeners()
}
},
{ immediate: true },
diff --git a/src/composables/useVersionChecker.ts b/src/composables/useVersionChecker.ts
new file mode 100644
index 00000000..2f2d1756
--- /dev/null
+++ b/src/composables/useVersionChecker.ts
@@ -0,0 +1,113 @@
+import { ref, computed, h } from 'vue'
+import { useToast } from 'vue-toastification'
+import i18n from '@/plugins/i18n'
+import VersionUpdateToast from '@/components/toast/VersionUpdateToast.vue'
+
+// 声明全局变量类型
+declare const __APP_VERSION__: string
+
+// 全局状态
+const currentVersion = ref(__APP_VERSION__)
+const serverVersion = ref(null)
+const versionChecked = ref(false)
+const needsUpdate = computed(() => {
+ return serverVersion.value !== null && serverVersion.value !== currentVersion.value
+})
+
+/**
+ * 清除所有缓存和 Service Worker
+ */
+export const clearCachesAndServiceWorker = async (): Promise => {
+ try {
+ // 1. 清除所有缓存
+ if ('caches' in window) {
+ const cacheNames = await caches.keys()
+ await Promise.all(cacheNames.map(name => caches.delete(name)))
+ console.log('[VersionChecker] 已清除所有缓存')
+ }
+
+ // 2. 注销 Service Worker
+ if ('serviceWorker' in navigator) {
+ const registrations = await navigator.serviceWorker.getRegistrations()
+ await Promise.all(registrations.map(registration => registration.unregister()))
+ console.log('[VersionChecker] 已注销所有 Service Worker')
+ }
+ } catch (error) {
+ console.error('[VersionChecker] 清除缓存失败:', error)
+ }
+}
+
+/**
+ * 版本检查 Composable
+ *
+ * 功能:
+ * - 检查前端版本与服务端版本是否一致
+ * - 检测到版本更新时清除缓存和 Service Worker
+ * - 显示持久化更新通知
+ */
+export function useVersionChecker() {
+ const toast = useToast()
+
+ /**
+ * 显示版本更新通知(带刷新按钮)
+ */
+ const showUpdateNotification = (): void => {
+ // 使用自定义 Vue 组件作为 toast 内容,传递翻译后的文本作为 props
+ const component = h(VersionUpdateToast, {
+ message: i18n.global.t('common.newVersionAvailable'),
+ refreshText: i18n.global.t('common.refresh'),
+ })
+
+ toast.info(component, {
+ timeout: false, // 不自动消失
+ closeButton: false,
+ closeOnClick: false,
+ draggable: false,
+ })
+ }
+
+ /**
+ * 检查版本并在需要时显示更新通知
+ * @param latestVersion 服务端返回的最新版本号
+ */
+ const checkVersion = async (latestVersion: string): Promise => {
+ // 如果已经检查过,则跳过
+ if (versionChecked.value) {
+ return
+ }
+
+ // 更新服务端版本
+ serverVersion.value = latestVersion
+
+ // 版本不同,且尚未显示通知
+ if (needsUpdate.value) {
+ versionChecked.value = true
+ console.log(`[VersionChecker] 检测到版本更新: ${currentVersion.value} -> ${latestVersion}`)
+
+ // 清除缓存和 Service Worker
+ await clearCachesAndServiceWorker()
+
+ // 显示持久化通知
+ showUpdateNotification()
+ }
+ }
+
+ /**
+ * 重置版本检查状态(用于测试或特殊场景)
+ */
+ const resetVersionCheck = (): void => {
+ versionChecked.value = false
+ serverVersion.value = null
+ }
+
+ return {
+ // 状态
+ currentVersion: computed(() => currentVersion.value),
+ serverVersion: computed(() => serverVersion.value),
+ needsUpdate,
+ versionChecked: computed(() => versionChecked.value),
+ // 方法
+ checkVersion,
+ resetVersionCheck,
+ }
+}
diff --git a/src/layouts/components/Footer.vue b/src/layouts/components/Footer.vue
index 7d54be75..2009389b 100644
--- a/src/layouts/components/Footer.vue
+++ b/src/layouts/components/Footer.vue
@@ -328,6 +328,7 @@ const showDynamicButton = computed(() => {
block-size: auto;
inline-size: auto;
min-block-size: 0;
+ max-width: 60px; // 限制最大宽度以便动画
.footer-card-content {
padding: 3px;
@@ -358,14 +359,12 @@ const showDynamicButton = computed(() => {
transition: all 0.3s cubic-bezier(0.25, 1, 0.5, 1);
}
-.fade-slide-enter-from {
- opacity: 0;
- transform: translateX(20px);
-}
-
+.fade-slide-enter-from,
.fade-slide-leave-to {
opacity: 0;
transform: translateX(20px);
+ max-width: 0;
+ margin-inline-start: 0 !important;
}
@keyframes fade-in {
diff --git a/src/layouts/components/UserProfile.vue b/src/layouts/components/UserProfile.vue
index 233626d4..80ad008c 100644
--- a/src/layouts/components/UserProfile.vue
+++ b/src/layouts/components/UserProfile.vue
@@ -16,6 +16,7 @@ import { saveLocalTheme } from '@/@core/utils/theme'
import type { ThemeSwitcherTheme } from '@layouts/types'
import { useConfirm } from '@/composables/useConfirm'
import { themeManager } from '@/utils/themeManager'
+import { usePWA, type UIMode } from '@/composables/usePWA'
// 认证 Store
const authStore = useAuthStore()
@@ -27,6 +28,8 @@ const globalSettingsStore = useGlobalSettingsStore()
const { t } = useI18n()
// 显示器
const display = useDisplay()
+// PWA
+const { uiMode, setUIMode } = usePWA()
// 提示框
const $toast = useToast()
@@ -40,6 +43,9 @@ const siteAuthDialog = ref(false)
// 自定义CSS弹窗
const cssDialog = ref(false)
+// UI模式菜单是否显示
+const showUIModeMenu = ref(false)
+
// 主题菜单是否显示
const showThemeMenu = ref(false)
@@ -233,6 +239,37 @@ const isAdvancedMode = computed(() => {
return globalSettingsStore.get('ADVANCED_MODE') !== false
})
+// UI模式相关
+const uiModes = computed(() => [
+ {
+ name: 'auto',
+ title: t('theme.autoUI'),
+ icon: 'mdi-devices',
+ },
+ {
+ name: 'desktop',
+ title: t('pwa.platforms.desktop'),
+ icon: 'mdi-monitor',
+ },
+ {
+ name: 'app',
+ title: t('pwa.platforms.mobile'),
+ icon: 'mdi-cellphone',
+ },
+])
+
+// 切换UI模式
+function changeUIMode(mode: UIMode) {
+ setUIMode(mode)
+ showUIModeMenu.value = false
+}
+
+// 获取当前UI模式图标
+const getUIModeIcon = computed(() => {
+ const mode = uiModes.value.find(m => m.name === uiMode.value)
+ return mode?.icon || 'mdi-devices'
+})
+
// 主题相关功能
const { name: themeName, global: globalTheme } = useTheme()
const savedTheme = ref(localStorage.getItem('theme') ?? themeName)
@@ -546,6 +583,41 @@ onUnmounted(() => {
{{ t('user.siteAuth') }}
+
+
+
+
+
+
+
+ {{ t('common.uiMode') }}
+
+ {{ uiModes.find(m => m.name === uiMode)?.title || t('theme.autoUI') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ mode.title }}
+
+
+
+
+
+
+
@@ -553,9 +625,10 @@ onUnmounted(() => {
-
- {{ themes.find(t => t.name === currentThemeName)?.title || t('common.theme') }}
-
+ {{ t('common.theme') }}
+
+ {{ themes.find(t => t.name === currentThemeName)?.title || t('theme.auto') }}
+
diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts
index 73660dbb..70bcf524 100644
--- a/src/locales/en-US.ts
+++ b/src/locales/en-US.ts
@@ -30,6 +30,7 @@ export default {
saving: 'Saving',
reset: 'Reset',
theme: 'Theme',
+ uiMode: 'UI Layout',
language: 'Language',
pleaseWait: 'Please wait...',
viewDetails: 'View Details',
@@ -66,6 +67,7 @@ export default {
serviceUnavailable: 'Service Unavailable',
status: 'Status',
preset: 'Preset',
+ refresh: 'Refresh',
newVersionAvailable: 'New version detected, please refresh the page to get the latest features',
},
mediaType: {
@@ -130,6 +132,7 @@ export default {
light: 'Light',
dark: 'Dark',
auto: 'Follow System',
+ autoUI: 'Auto',
transparent: 'Transparent',
purple: 'Purple',
custom: 'Custom Style',
@@ -1226,6 +1229,7 @@ export default {
title: 'About MoviePilot',
softwareVersion: 'Software Version',
frontendVersion: 'Frontend Version',
+ browserVersion: 'Browser Cached Version',
authVersion: 'Auth Resource Version',
indexerVersion: 'Indexer Resource Version',
configDir: 'Config Directory',
@@ -1245,6 +1249,7 @@ export default {
dataDirectory: '/moviepilot',
expand: 'Expand',
collapse: 'Collapse',
+ cleanCache: 'Clear Cache',
},
system: {
custom: 'Custom',
diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts
index 114010f1..21bd8ad2 100644
--- a/src/locales/zh-CN.ts
+++ b/src/locales/zh-CN.ts
@@ -30,6 +30,7 @@ export default {
saving: '保存中',
reset: '重置',
theme: '主题',
+ uiMode: '界面布局',
language: '语言',
pleaseWait: '请稍候...',
viewDetails: '查看详情',
@@ -66,6 +67,7 @@ export default {
serviceUnavailable: '服务不可用',
status: '状态',
preset: '预设',
+ refresh: '刷新',
newVersionAvailable: '检测到新版本,请刷新页面以获取最新功能',
},
mediaType: {
@@ -130,6 +132,7 @@ export default {
light: '浅色',
dark: '深色',
auto: '跟随系统',
+ autoUI: '自动',
transparent: '透明',
purple: '幻紫',
custom: '附加样式',
@@ -1223,6 +1226,7 @@ export default {
title: '关于 MoviePilot',
softwareVersion: '软件版本',
frontendVersion: '前端版本',
+ browserVersion: '浏览器缓存版本',
authVersion: '认证资源版本',
indexerVersion: '站点资源版本',
configDir: '配置目录',
@@ -1230,7 +1234,7 @@ export default {
timezone: '时区',
latest: '最新',
supportingSites: '支持站点',
- support: '支援',
+ support: '支持',
documentation: '文档',
feedback: '问题反馈',
channel: '发布频道',
@@ -1242,6 +1246,7 @@ export default {
dataDirectory: '/moviepilot',
expand: '展开',
collapse: '收起',
+ cleanCache: '清除缓存',
},
system: {
custom: '自定义',
diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts
index da49cc34..9991a2ae 100644
--- a/src/locales/zh-TW.ts
+++ b/src/locales/zh-TW.ts
@@ -30,6 +30,7 @@ export default {
saving: '保存中',
reset: '重置',
theme: '主題',
+ uiMode: '界面佈局',
language: '語言',
pleaseWait: '請稍候...',
viewDetails: '查看詳情',
@@ -66,6 +67,7 @@ export default {
serviceUnavailable: '服務不可用',
status: '狀態',
preset: '預設',
+ refresh: '刷新',
newVersionAvailable: '檢測到新版本,請刷新頁面以獲取最新功能',
},
mediaType: {
@@ -130,6 +132,7 @@ export default {
light: '淺色',
dark: '深色',
auto: '跟隨系統',
+ autoUI: '自動',
transparent: '透明',
purple: '幻紫',
custom: '附加樣式',
@@ -1211,6 +1214,7 @@ export default {
title: '關於 MoviePilot',
softwareVersion: '軟件版本',
frontendVersion: '前端版本',
+ browserVersion: '瀏覽器緩存版本',
authVersion: '認證資源版本',
indexerVersion: '站點資源版本',
configDir: '配置目錄',
@@ -1230,6 +1234,7 @@ export default {
dataDirectory: '/moviepilot',
expand: '展開',
collapse: '收起',
+ cleanCache: '清除緩存',
},
system: {
custom: '自定義',
diff --git a/src/plugins/vuetify/icons.ts b/src/plugins/vuetify/icons.ts
index 6015c736..605a1a81 100644
--- a/src/plugins/vuetify/icons.ts
+++ b/src/plugins/vuetify/icons.ts
@@ -1,5 +1,5 @@
import { Icon } from '@iconify/vue'
-import { aliases } from 'vuetify/lib/iconsets/mdi'
+import { aliases } from 'vuetify/iconsets/mdi'
const alertTypeIcon = {
success: 'mdi-check-circle-outline',
diff --git a/src/stores/global.ts b/src/stores/global.ts
index 13eac221..50cce208 100644
--- a/src/stores/global.ts
+++ b/src/stores/global.ts
@@ -1,6 +1,7 @@
import { defineStore } from 'pinia'
import type { globalSettingsState } from '@/stores/types'
import { fetchGlobalSettings } from '@/utils/globalSetting'
+import { useVersionChecker } from '@/composables/useVersionChecker'
export const useGlobalSettingsStore = defineStore('globalSettings', {
state: (): globalSettingsState => ({
@@ -18,6 +19,12 @@ export const useGlobalSettingsStore = defineStore('globalSettings', {
const result = await fetchGlobalSettings()
this.data = result || {}
this.initialized = true
+
+ // 检查版本更新
+ if (result.FRONTEND_VERSION) {
+ const { checkVersion } = useVersionChecker()
+ await checkVersion(result.FRONTEND_VERSION)
+ }
} catch (error) {
console.error('Failed to initialize global settings', error)
} finally {
diff --git a/src/utils/globalSetting.ts b/src/utils/globalSetting.ts
index bcde0e96..f1586b02 100644
--- a/src/utils/globalSetting.ts
+++ b/src/utils/globalSetting.ts
@@ -1,58 +1,8 @@
import api from '@/api'
-import { useToast } from 'vue-toastification'
-import i18n from '@/plugins/i18n'
// 创建一个专用的AbortController,用于globalSetting请求
const globalSettingController = new AbortController()
-// 声明全局变量类型
-declare const __APP_VERSION__: string
-
-// 当前前端版本号(由 Vite 在编译时注入)
-const CURRENT_FRONTEND_VERSION = __APP_VERSION__
-console.log(`[VersionChecker] 当前前端版本: ${CURRENT_FRONTEND_VERSION}`)
-
-// 标记是否已经显示过版本更新通知
-let versionNotificationShown = false
-
-/**
- * 检查版本并在需要时显示更新通知
- */
-async function checkVersionAndNotify(serverVersion: string): Promise {
- // 版本不同,且尚未显示通知
- if (serverVersion !== CURRENT_FRONTEND_VERSION && !versionNotificationShown) {
- versionNotificationShown = true
- console.log(`[VersionChecker] 检测到版本更新: ${CURRENT_FRONTEND_VERSION} -> ${serverVersion}`)
-
- try {
- // 1. 清除所有缓存
- if ('caches' in window) {
- const cacheNames = await caches.keys()
- await Promise.all(cacheNames.map(name => caches.delete(name)))
- console.log('[VersionChecker] 已清除所有缓存')
- }
-
- // 2. 注销Service Worker
- if ('serviceWorker' in navigator) {
- const registrations = await navigator.serviceWorker.getRegistrations()
- await Promise.all(registrations.map(registration => registration.unregister()))
- console.log('[VersionChecker] 已注销所有 Service Worker')
- }
- } catch (error) {
- console.error('[VersionChecker] 清除缓存失败:', error)
- }
-
- // 3. 显示持久化通知,提示用户刷新
- const toast = useToast()
- toast.info(i18n.global.t('common.newVersionAvailable'), {
- timeout: false, // 不自动消失
- closeButton: false,
- closeOnClick: false,
- draggable: false,
- })
- }
-}
-
export async function fetchGlobalSettings() {
try {
const result: { [key: string]: any } = await api.get('system/global', {
@@ -62,15 +12,7 @@ export async function fetchGlobalSettings() {
// 手动设置signal,防止reqestOptimizer添加可中断的controller
signal: globalSettingController.signal,
})
-
- const data = result.data || {}
-
- // 检查版本更新
- if (data.FRONTEND_VERSION) {
- await checkVersionAndNotify(data.FRONTEND_VERSION)
- }
-
- return data
+ return result.data || {}
} catch (error) {
console.error('Failed to fetch global settings', error)
throw error
diff --git a/vite.config.ts b/vite.config.ts
index b3eaba14..8e6dc8ac 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -331,6 +331,7 @@ export default defineConfig({
css: {
preprocessorOptions: {
scss: {
+ api: 'modern-compiler',
quietDeps: true,
},
},
diff --git a/yarn.lock b/yarn.lock
index fb893731..a8a89b83 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3017,9 +3017,9 @@ camelcase-css@^2.0.1:
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
caniuse-lite@^1.0.30001688, caniuse-lite@^1.0.30001702:
- version "1.0.30001715"
- resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz"
- integrity sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==
+ version "1.0.30001761"
+ resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz"
+ integrity sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==
chalk@^4.0.0, chalk@^4.0.2:
version "4.1.2"
@@ -6856,6 +6856,7 @@ std-env@^3.9.0:
integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.3:
+ name string-width-cjs
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -6941,6 +6942,7 @@ stringify-object@^3.3.0:
is-regexp "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+ name strip-ansi-cjs
version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==