diff --git a/src/@core/components/LocaleSwitcher.vue b/src/@core/components/LocaleSwitcher.vue
deleted file mode 100644
index 48346469..00000000
--- a/src/@core/components/LocaleSwitcher.vue
+++ /dev/null
@@ -1,64 +0,0 @@
-
-
-
-
-
diff --git a/src/@core/components/ThemeSwitcher.vue b/src/@core/components/ThemeSwitcher.vue
deleted file mode 100644
index 3484e090..00000000
--- a/src/@core/components/ThemeSwitcher.vue
+++ /dev/null
@@ -1,184 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- 自定义主题风格
-
-
-
-
-
-
-
-
-
-
-
- 保存
-
-
-
-
-
diff --git a/src/layouts/components/DefaultLayout.vue b/src/layouts/components/DefaultLayout.vue
index 5542a21d..d55c26bb 100644
--- a/src/layouts/components/DefaultLayout.vue
+++ b/src/layouts/components/DefaultLayout.vue
@@ -3,7 +3,6 @@ import VerticalNavSectionTitle from '@/@layouts/components/VerticalNavSectionTit
import VerticalNavLayout from '@layouts/components/VerticalNavLayout.vue'
import VerticalNavLink from '@layouts/components/VerticalNavLink.vue'
import Footer from '@/layouts/components/Footer.vue'
-import NavbarActions from '@/layouts/components/NavbarActions.vue'
import UserNofification from '@/layouts/components/UserNotification.vue'
import SearchBar from '@/layouts/components/SearchBar.vue'
import ShortcutBar from '@/layouts/components/ShortcutBar.vue'
@@ -82,8 +81,6 @@ onMounted(() => {
-
-
diff --git a/src/layouts/components/NavbarActions.vue b/src/layouts/components/NavbarActions.vue
deleted file mode 100644
index ad7c8277..00000000
--- a/src/layouts/components/NavbarActions.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/src/layouts/components/UserProfile.vue b/src/layouts/components/UserProfile.vue
index be53d2c8..4ba2420f 100644
--- a/src/layouts/components/UserProfile.vue
+++ b/src/layouts/components/UserProfile.vue
@@ -8,6 +8,12 @@ import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import UserAuthDialog from '@/components/dialog/UserAuthDialog.vue'
import { useAuthStore, useUserStore } from '@/stores'
import { useI18n } from 'vue-i18n'
+import { useDisplay, useTheme } from 'vuetify'
+import { SUPPORTED_LOCALES, SupportedLocale } from '@/types/i18n'
+import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
+import { getCurrentLocale, setI18nLanguage } from '@/plugins/i18n'
+import { saveLocalTheme } from '@/@core/utils/theme'
+import type { ThemeSwitcherTheme } from '@layouts/types'
// 认证 Store
const authStore = useAuthStore()
@@ -15,6 +21,8 @@ const authStore = useAuthStore()
const userStore = useUserStore()
// 国际化
const { t } = useI18n()
+// 显示器
+const display = useDisplay()
// 确认框
const createConfirm = useConfirm()
@@ -31,6 +39,18 @@ const siteAuthDialog = ref(false)
// 重启确认对话框
const restartDialog = ref(false)
+// 自定义CSS弹窗
+const cssDialog = ref(false)
+
+// 主题菜单是否显示
+const showThemeMenu = ref(false)
+
+// 语言菜单是否显示
+const showLanguageMenu = ref(false)
+
+// 自定义CSS
+const customCSS = ref('')
+
// 执行注销操作
function logout() {
// 清除登录状态信息
@@ -82,13 +102,172 @@ const superUser = computed(() => userStore.superUser)
const userName = computed(() => userStore.userName)
const avatar = computed(() => userStore.avatar || avatar1)
const userLevel = computed(() => userStore.level)
+
+// 主题相关功能
+const { name: themeName, global: globalTheme } = useTheme()
+const savedTheme = ref(localStorage.getItem('theme') ?? themeName)
+const currentThemeName = ref(savedTheme.value)
+
+const themes: ThemeSwitcherTheme[] = [
+ {
+ name: 'auto',
+ title: t('theme.auto'),
+ icon: 'mdi-laptop',
+ },
+ {
+ name: 'light',
+ title: t('theme.light'),
+ icon: 'mdi-weather-sunny',
+ },
+ {
+ name: 'dark',
+ title: t('theme.dark'),
+ icon: 'mdi-weather-night',
+ },
+ {
+ name: 'purple',
+ title: t('theme.purple'),
+ icon: 'mdi-brightness-4',
+ },
+ {
+ name: 'transparent',
+ title: t('theme.transparent'),
+ icon: 'mdi-gradient-horizontal',
+ },
+]
+
+// 编辑器主题
+const editorTheme = computed(() => (currentThemeName.value === 'light' ? 'github' : 'monokai'))
+
+// 更新主题
+function updateTheme() {
+ const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
+ const theme = currentThemeName.value === 'auto' ? autoTheme : currentThemeName.value
+ globalTheme.name.value = theme
+ // 保存原始主题设置,而不是计算后的值
+ savedTheme.value = currentThemeName.value
+ // 保存主题到本地
+ saveLocalTheme(currentThemeName.value, globalTheme)
+ // 刷新页面
+ location.reload()
+}
+
+// 切换主题
+function changeTheme(theme: string) {
+ currentThemeName.value = theme
+ showThemeMenu.value = false
+ // 保存主题到服务端
+ try {
+ api.post('/user/config/Layout', {
+ theme,
+ })
+ } catch (e) {
+ console.error(e)
+ }
+}
+
+// 获取自定义 CSS
+async function getCustomCSS() {
+ try {
+ const result: { [key: string]: any } = await api.get('system/setting/UserCustomCSS')
+ if (result && result.success && result.data?.value) {
+ customCSS.value = result.data?.value ?? ''
+ if (customCSS.value) {
+ const style = document.createElement('style')
+ style.innerHTML = result.data?.value ?? ''
+ document.head.appendChild(style)
+ }
+ }
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+// 保存自定义 CSS
+async function saveCustomCSS() {
+ cssDialog.value = false
+ try {
+ const result: { [key: string]: any } = await api.post('system/setting/UserCustomCSS', customCSS.value, {
+ headers: {
+ 'Content-Type': 'text/plain',
+ },
+ })
+
+ if (result.success) $toast.success(t('theme.customCssSaveSuccess'))
+ } catch (e) {
+ console.error(t('theme.customCssSaveFailed'))
+ }
+}
+
+// 监听主题变化
+watch(
+ () => currentThemeName.value,
+ () => updateTheme(),
+)
+
+// 监听系统主题变化
+try {
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme)
+} catch (e) {
+ console.error(t('theme.deviceNotSupport'))
+}
+
+// 语言相关功能
+const currentLocale = ref(getCurrentLocale())
+
+// 支持的语言列表
+const locales = computed(() => {
+ return Object.entries(SUPPORTED_LOCALES).map(([key, locale]) => ({
+ value: key as SupportedLocale,
+ title: locale.title,
+ flag: locale.flag,
+ icon: `flag-${key.split('-')[0]}`,
+ }))
+})
+
+// 切换语言
+async function changeLocale(locale: SupportedLocale) {
+ showLanguageMenu.value = false
+ try {
+ await setI18nLanguage(locale)
+ currentLocale.value = locale
+ // 刷新页面
+ window.location.reload()
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+// 获取当前语言图标
+const getCurrentIcon = computed(() => {
+ const locale = locales.value.find(l => l.value === currentLocale.value)
+ return locale?.flag || '🌐'
+})
+
+// 获取当前主题图标
+const getThemeIcon = computed(() => {
+ const theme = themes.find(t => t.name === currentThemeName.value)
+ return theme?.icon || 'mdi-laptop'
+})
+
+onMounted(() => {
+ getCustomCSS()
+})
-
+
@@ -192,4 +446,34 @@ const userLevel = computed(() => userStore.level)
+
+
+
+
+
+
+
+ {{ t('theme.custom') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('common.save') }}
+
+
+
+
+
+
diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts
index 6ea8179f..e851b18e 100644
--- a/src/locales/en-US.ts
+++ b/src/locales/en-US.ts
@@ -21,6 +21,8 @@ export default {
create: 'Create',
saving: 'Saving',
reset: 'Reset',
+ theme: 'Theme',
+ language: 'Language',
},
mediaType: {
movie: 'Movie',
@@ -36,6 +38,9 @@ export default {
transparent: 'Transparent',
purple: 'Purple',
custom: 'Custom Theme',
+ customCssSaveSuccess: 'Custom CSS saved successfully, please refresh the page to take effect!',
+ customCssSaveFailed: 'Failed to save custom CSS to server',
+ deviceNotSupport: 'Current device does not support monitoring system theme changes',
},
app: {
moviepilot: 'MoviePilot',
@@ -43,7 +48,6 @@ export default {
subscribeMovie: 'Movie Subscription',
subscribeTv: 'TV Subscription',
settings: 'Settings',
- language: 'Language',
selectLanguage: 'Select Language',
logout: 'Logout',
restarting: 'Restarting...',
@@ -137,7 +141,8 @@ export default {
},
words: {
title: 'Word Lists',
- description: 'Custom recognition words, custom production/subtitle groups, custom placeholders, file organization block words',
+ description:
+ 'Custom recognition words, custom production/subtitle groups, custom placeholders, file organization block words',
},
about: {
title: 'About',
@@ -937,9 +942,10 @@ export default {
season: 'Season {number}',
title: 'Title',
description: 'Description',
- descriptionHint: 'Add a description about this subscription. Search terms, recognition words, etc. will be included in the share by default',
+ descriptionHint:
+ 'Add a description about this subscription. Search terms, recognition words, etc. will be included in the share by default',
shareUser: 'Share User',
- shareUserHint: 'Sharer\'s nickname',
+ shareUserHint: "Sharer's nickname",
confirmShare: 'Confirm Share',
shareSuccess: '{name} shared successfully!',
shareFailed: '{name} share failed: {message}!',
@@ -960,7 +966,8 @@ export default {
title: 'RClone Configuration',
filePath: 'rclone config file path',
fileContent: 'rclone config file content',
- defaultContent: '# Please fill in your rclone config file content here \n# Please refer to https://rclone.org/docs/ \n# Storage node name must be: MP',
+ defaultContent:
+ '# Please fill in your rclone config file content here \n# Please refer to https://rclone.org/docs/ \n# Storage node name must be: MP',
complete: 'Complete',
},
alistConfig: {
@@ -1030,7 +1037,7 @@ export default {
title: 'Update Site Cookie',
checkHint: 'Checking login status, please wait...',
confirmUpdateTitle: 'Confirm Update',
- confirmUpdateMessage: 'Do you want to update this site\'s cookie with the local cookie?',
+ confirmUpdateMessage: "Do you want to update this site's cookie with the local cookie?",
processing: 'Processing...',
success: 'Cookie updated successfully',
failed: 'Failed to update cookie',
@@ -1229,7 +1236,8 @@ export default {
mediaCategoryHint: 'Specify category name, leave empty for auto-recognition',
customWords: 'Custom Recognition Words',
customWordsHint: 'Recognition words only used for this subscription',
- customWordsPlaceholder: 'Block word\nReplaced word => Replacement word\nPrefix <> Suffix >> Episode offset (EP)\nReplaced word => Replacement word && Prefix <> Suffix >> Episode offset (EP)\nReplacement word supports format: { tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx } to directly specify TMDBID/Douban ID recognition, where s, e are season and episode numbers (optional)',
+ customWordsPlaceholder:
+ 'Block word\nReplaced word => Replacement word\nPrefix <> Suffix >> Episode offset (EP)\nReplaced word => Replacement word && Prefix <> Suffix >> Episode offset (EP)\nReplacement word supports format: { tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx } to directly specify TMDBID/Douban ID recognition, where s, e are season and episode numbers (optional)',
cancelSubscribe: 'Cancel Subscription',
save: 'Save',
cancelSubscribeConfirm: 'Are you sure you want to cancel the subscription?',
@@ -1409,9 +1417,11 @@ export default {
otpEnableSuccess: 'Two-factor authentication enabled successfully!',
otpEnableFailed: 'Failed to enable OTP: {message}!',
authenticatorApp: 'Authenticator App',
- authenticatorAppDescription: 'Use an authenticator app like Google Authenticator, Microsoft Authenticator, Authy, or 1Password to scan the QR code. It will generate a 6-digit code for you to enter below.',
- secretKeyTip: 'If you\'re having trouble with the QR code, select manual entry in your app and enter the code above.',
+ authenticatorAppDescription:
+ 'Use an authenticator app like Google Authenticator, Microsoft Authenticator, Authy, or 1Password to scan the QR code. It will generate a 6-digit code for you to enter below.',
+ secretKeyTip:
+ "If you're having trouble with the QR code, select manual entry in your app and enter the code above.",
enterVerificationCode: 'Enter verification code to confirm enabling two-factor authentication',
avatarFormatTip: 'JPG, PNG, GIF, WEBP formats allowed, maximum size 800KB.',
},
-}
+}
diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts
index 81f3f1b9..62bfcfc8 100644
--- a/src/locales/zh-CN.ts
+++ b/src/locales/zh-CN.ts
@@ -21,6 +21,8 @@ export default {
create: '新建',
saving: '保存中',
reset: '重置',
+ theme: '主题',
+ language: '语言',
},
mediaType: {
movie: '电影',
@@ -36,6 +38,9 @@ export default {
transparent: '透明',
purple: '幻紫',
custom: '自定义主题',
+ customCssSaveSuccess: '自定义CSS保存成功,请刷新页面生效!',
+ customCssSaveFailed: '保存自定义CSS到服务端失败',
+ deviceNotSupport: '当前设备不支持监听系统主题变化',
},
app: {
moviepilot: 'MoviePilot',
diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts
index 12ba377c..6787fc2a 100644
--- a/src/locales/zh-TW.ts
+++ b/src/locales/zh-TW.ts
@@ -21,6 +21,8 @@ export default {
create: '新建',
saving: '保存中',
reset: '重置',
+ theme: '主題',
+ language: '語言',
},
mediaType: {
movie: '電影',
@@ -36,6 +38,9 @@ export default {
transparent: '透明',
purple: '幻紫',
custom: '自定義主題',
+ customCssSaveSuccess: '自定義CSS保存成功,請刷新頁面生效!',
+ customCssSaveFailed: '保存自定義CSS到服務端失敗',
+ deviceNotSupport: '當前設備不支持監聽系統主題變化',
},
app: {
moviepilot: 'MoviePilot',