删除语言和主题切换组件,整合相关功能至用户个人资料组件中

This commit is contained in:
jxxghp
2025-04-28 15:43:07 +08:00
parent 6be4694327
commit 43460d4198
8 changed files with 315 additions and 306 deletions

View File

@@ -1,64 +0,0 @@
<script setup lang="ts">
import { SUPPORTED_LOCALES, SupportedLocale } from '@/types/i18n'
import { setI18nLanguage, getCurrentLocale } from '@/plugins/i18n'
// 当前语言
const currentLocale = ref<SupportedLocale>(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) {
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 || '🌐'
})
</script>
<template>
<VMenu class="locale-menu" scrim>
<template v-slot:activator="{ props }">
<IconBtn v-bind="props">
<span class="text-xl">{{ getCurrentIcon }}</span>
</IconBtn>
</template>
<VList>
<div class="px-2">
<VListItem
v-for="locale in locales"
:key="locale.value"
@click="changeLocale(locale.value)"
:active="currentLocale === locale.value"
class="mb-1"
>
<template #prepend>
<span class="text-xl me-2">{{ locale.flag }}</span>
</template>
<VListItemTitle>{{ locale.title }}</VListItemTitle>
<template #append v-if="currentLocale === locale.value">
<VIcon icon="mdi-check" color="primary" size="small" />
</template>
</VListItem>
</div>
</VList>
</VMenu>
</template>

View File

@@ -1,184 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useDisplay, useTheme } from 'vuetify'
import type { ThemeSwitcherTheme } from '@layouts/types'
import api from '@/api'
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
import { useToast } from 'vue-toast-notification'
import { saveLocalTheme } from '../utils/theme'
import { useI18n } from 'vue-i18n'
// 显示器宽度
const display = useDisplay()
const props = defineProps<{
themes: ThemeSwitcherTheme[]
}>()
const { name: themeName, global: globalTheme } = useTheme()
const savedTheme = ref(localStorage.getItem('theme') ?? themeName)
const currentThemeName = ref(savedTheme.value)
const getNextThemeName = () => {
const currentIndex = props.themes.findIndex(t => t.name === currentThemeName.value)
const nextIndex = (currentIndex + 1) % props.themes.length
return props.themes[nextIndex].name
}
const $toast = useToast()
const { t } = useI18n()
// 自定义CSS弹窗
const cssDialog = ref(false)
// 自定义 CSS
const customCSS = ref('')
// 编辑器主题
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) {
let nextTheme = theme
if (!theme) nextTheme = getNextThemeName()
currentThemeName.value = nextTheme
// 保存主题到服务端
try {
api.post('/user/config/Layout', {
theme: nextTheme,
})
} catch (e) {
console.error(e)
}
}
// 监听系统主题变化
try {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme)
} catch (e) {
console.error('当前设备不支持监听系统主题变化')
}
// 查询当前主题的图标
const getThemeIcon = computed(() => {
const theme = props.themes.find(t => t.name === currentThemeName.value)
return theme?.icon ?? 'mdi-circle'
})
// 监听设置主题变化
watch(
() => currentThemeName.value,
() => updateTheme(),
)
// 获取自定义 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('自定义CSS保存成功请刷新页面生效')
} catch (e) {
console.error('保存自定义 CSS 到服务端失败')
}
}
onMounted(() => {
getCustomCSS()
})
</script>
<template>
<VMenu v-if="props.themes" class="theme-menu" scrim>
<template v-slot:activator="{ props }">
<IconBtn v-bind="props">
<VIcon :icon="getThemeIcon" />
</IconBtn>
</template>
<VList>
<div class="px-2">
<VListItem
v-for="theme in props.themes"
:key="theme.name"
@click="changeTheme(theme.name)"
:active="currentThemeName === theme.name"
class="mb-1"
>
<template #prepend>
<VIcon :icon="theme.icon" />
</template>
<VListItemTitle>{{ theme.title }}</VListItemTitle>
<template #append v-if="currentThemeName === theme.name">
<VIcon icon="mdi-check" color="primary" size="small" />
</template>
</VListItem>
<VDivider class="my-2" />
<VListItem @click="cssDialog = true">
<template #prepend>
<VIcon icon="mdi-palette" />
</template>
<VListItemTitle>{{ t('theme.custom') }}</VListItemTitle>
</VListItem>
</div>
</VList>
</VMenu>
<!-- 自定义 CSS -- -->
<VDialog v-if="cssDialog" v-model="cssDialog" max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard>
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-palette" class="me-2" />
自定义主题风格
</VCardTitle>
<VDialogCloseBtn @click="cssDialog = false" />
</VCardItem>
<VDivider />
<VAceEditor v-model:value="customCSS" lang="css" :theme="editorTheme" class="w-full min-h-[30rem]" />
<VDivider />
<VCardText class="text-center">
<VBtn @click="saveCustomCSS" class="w-1/2">
<template #prepend>
<VIcon icon="mdi-content-save" />
</template>
保存
</VBtn>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -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(() => {
<ShortcutBar v-if="superUser" />
<!-- 👉 Notification -->
<UserNofification />
<!-- 👉 Theme & Language -->
<NavbarActions />
<!-- 👉 UserProfile -->
<UserProfile />
</div>

View File

@@ -1,44 +0,0 @@
<script setup lang="ts">
import type { ThemeSwitcherTheme } from '@layouts/types'
import { useI18n } from 'vue-i18n'
import LocaleSwitcher from '@/@core/components/LocaleSwitcher.vue'
const { t } = useI18n()
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',
},
]
</script>
<template>
<div class="d-flex align-center">
<!-- 主题切换 -->
<ThemeSwitcher :themes="themes" />
<!-- 语言切换 -->
<LocaleSwitcher class="me-2" />
</div>
</template>

View File

@@ -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<SupportedLocale>(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()
})
</script>
<template>
<VAvatar class="cursor-pointer ms-3" color="primary" variant="tonal">
<VImg :src="avatar" />
<VMenu activator="parent" width="230" location="bottom end" offset="14px" class="user-menu" scrim>
<VMenu
activator="parent"
width="15rem"
location="bottom end"
offset="14px"
class="user-menu"
:close-on-content-click="true"
scrim
>
<VList class="pt-0">
<!-- 👉 User Avatar & Name -->
<VListItem class="py-4" bg-color="primary" bg-opacity="0.05">
@@ -131,6 +310,80 @@ const userLevel = computed(() => userStore.level)
<VListItemTitle>{{ t('user.siteAuth') }}</VListItemTitle>
</VListItem>
<!-- 👉 主题设置 - 使用嵌套菜单 -->
<VMenu location="end" offset-x min-width="200" v-model="showThemeMenu" :close-on-content-click="true">
<template v-slot:activator="{ props: menuProps }">
<VListItem v-bind="menuProps" class="mb-1 rounded-lg" hover>
<template #prepend>
<VIcon :icon="getThemeIcon" />
</template>
<VListItemTitle>
{{ themes.find(t => t.name === currentThemeName)?.title || t('common.theme') }}
</VListItemTitle>
<template #append>
<VIcon icon="mdi-chevron-right" size="small" />
</template>
</VListItem>
</template>
<VList>
<VListItem
v-for="theme in themes"
:key="theme.name"
@click="changeTheme(theme.name)"
:active="currentThemeName === theme.name"
class="mb-1"
>
<template #prepend>
<VIcon :icon="theme.icon" />
</template>
<VListItemTitle>{{ theme.title }}</VListItemTitle>
<template #append v-if="currentThemeName === theme.name">
<VIcon icon="mdi-check" color="primary" size="small" />
</template>
</VListItem>
<VListItem @click="cssDialog = true">
<template #prepend>
<VIcon icon="mdi-palette" />
</template>
<VListItemTitle>{{ t('theme.custom') }}</VListItemTitle>
</VListItem>
</VList>
</VMenu>
<!-- 👉 语言设置 - 使用嵌套菜单 -->
<VMenu location="end" offset-x min-width="200" v-model="showLanguageMenu" :close-on-content-click="true">
<template v-slot:activator="{ props: menuProps }">
<VListItem v-bind="menuProps" class="mb-1 rounded-lg" hover>
<template #prepend>
<span class="me-4">{{ getCurrentIcon }}</span>
</template>
<VListItemTitle>
{{ locales.find(l => l.value === currentLocale)?.title || t('common.language') }}
</VListItemTitle>
<template #append>
<VIcon icon="mdi-chevron-right" size="small" />
</template>
</VListItem>
</template>
<VList>
<VListItem
v-for="locale in locales"
:key="locale.value"
@click="changeLocale(locale.value)"
:active="currentLocale === locale.value"
class="mb-1"
>
<template #prepend>
<span class="text-xl me-2">{{ locale.flag }}</span>
</template>
<VListItemTitle>{{ locale.title }}</VListItemTitle>
<template #append v-if="currentLocale === locale.value">
<VIcon icon="mdi-check" color="primary" size="small" />
</template>
</VListItem>
</VList>
</VMenu>
<!-- 👉 FAQ -->
<VListItem href="https://wiki.movie-pilot.org" target="_blank" class="mb-1 rounded-lg" hover>
<template #prepend>
@@ -163,6 +416,7 @@ const userLevel = computed(() => userStore.level)
</VMenu>
<!-- !SECTION -->
</VAvatar>
<!-- 重启进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="t('app.restarting')" />
<!-- 用户认证对话框 -->
@@ -192,4 +446,34 @@ const userLevel = computed(() => userStore.level)
<VDialogCloseBtn @click="restartDialog = false" />
</VCard>
</VDialog>
<!-- 自定义 CSS -->
<VDialog v-if="cssDialog" v-model="cssDialog" max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard>
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-palette" class="me-2" />
{{ t('theme.custom') }}
</VCardTitle>
<VDialogCloseBtn @click="cssDialog = false" />
</VCardItem>
<VDivider />
<VAceEditor v-model:value="customCSS" lang="css" :theme="editorTheme" class="w-full min-h-[30rem]" />
<VDivider />
<VCardText class="text-center">
<VBtn @click="saveCustomCSS" class="w-1/2">
<template #prepend>
<VIcon icon="mdi-content-save" />
</template>
{{ t('common.save') }}
</VBtn>
</VCardText>
</VCard>
</VDialog>
</template>
<style lang="scss" scoped>
.v-list-item__prepend {
min-inline-size: 24px !important;
}
</style>

View File

@@ -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: &#123; tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx &#125; 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: &#123; tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx &#125; 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.',
},
}
}

View File

@@ -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',

View File

@@ -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',