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

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
-64
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>
-184
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>
-3
View File
@@ -3,7 +3,6 @@ import VerticalNavSectionTitle from '@/@layouts/components/VerticalNavSectionTit
import VerticalNavLayout from '@layouts/components/VerticalNavLayout.vue' import VerticalNavLayout from '@layouts/components/VerticalNavLayout.vue'
import VerticalNavLink from '@layouts/components/VerticalNavLink.vue' import VerticalNavLink from '@layouts/components/VerticalNavLink.vue'
import Footer from '@/layouts/components/Footer.vue' import Footer from '@/layouts/components/Footer.vue'
import NavbarActions from '@/layouts/components/NavbarActions.vue'
import UserNofification from '@/layouts/components/UserNotification.vue' import UserNofification from '@/layouts/components/UserNotification.vue'
import SearchBar from '@/layouts/components/SearchBar.vue' import SearchBar from '@/layouts/components/SearchBar.vue'
import ShortcutBar from '@/layouts/components/ShortcutBar.vue' import ShortcutBar from '@/layouts/components/ShortcutBar.vue'
@@ -82,8 +81,6 @@ onMounted(() => {
<ShortcutBar v-if="superUser" /> <ShortcutBar v-if="superUser" />
<!-- 👉 Notification --> <!-- 👉 Notification -->
<UserNofification /> <UserNofification />
<!-- 👉 Theme & Language -->
<NavbarActions />
<!-- 👉 UserProfile --> <!-- 👉 UserProfile -->
<UserProfile /> <UserProfile />
</div> </div>
-44
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>
+285 -1
View File
@@ -8,6 +8,12 @@ import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import UserAuthDialog from '@/components/dialog/UserAuthDialog.vue' import UserAuthDialog from '@/components/dialog/UserAuthDialog.vue'
import { useAuthStore, useUserStore } from '@/stores' import { useAuthStore, useUserStore } from '@/stores'
import { useI18n } from 'vue-i18n' 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 // 认证 Store
const authStore = useAuthStore() const authStore = useAuthStore()
@@ -15,6 +21,8 @@ const authStore = useAuthStore()
const userStore = useUserStore() const userStore = useUserStore()
// 国际化 // 国际化
const { t } = useI18n() const { t } = useI18n()
// 显示器
const display = useDisplay()
// 确认框 // 确认框
const createConfirm = useConfirm() const createConfirm = useConfirm()
@@ -31,6 +39,18 @@ const siteAuthDialog = ref(false)
// 重启确认对话框 // 重启确认对话框
const restartDialog = 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() { function logout() {
// 清除登录状态信息 // 清除登录状态信息
@@ -82,13 +102,172 @@ const superUser = computed(() => userStore.superUser)
const userName = computed(() => userStore.userName) const userName = computed(() => userStore.userName)
const avatar = computed(() => userStore.avatar || avatar1) const avatar = computed(() => userStore.avatar || avatar1)
const userLevel = computed(() => userStore.level) 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> </script>
<template> <template>
<VAvatar class="cursor-pointer ms-3" color="primary" variant="tonal"> <VAvatar class="cursor-pointer ms-3" color="primary" variant="tonal">
<VImg :src="avatar" /> <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"> <VList class="pt-0">
<!-- 👉 User Avatar & Name --> <!-- 👉 User Avatar & Name -->
<VListItem class="py-4" bg-color="primary" bg-opacity="0.05"> <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> <VListItemTitle>{{ t('user.siteAuth') }}</VListItemTitle>
</VListItem> </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 --> <!-- 👉 FAQ -->
<VListItem href="https://wiki.movie-pilot.org" target="_blank" class="mb-1 rounded-lg" hover> <VListItem href="https://wiki.movie-pilot.org" target="_blank" class="mb-1 rounded-lg" hover>
<template #prepend> <template #prepend>
@@ -163,6 +416,7 @@ const userLevel = computed(() => userStore.level)
</VMenu> </VMenu>
<!-- !SECTION --> <!-- !SECTION -->
</VAvatar> </VAvatar>
<!-- 重启进度框 --> <!-- 重启进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="t('app.restarting')" /> <ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="t('app.restarting')" />
<!-- 用户认证对话框 --> <!-- 用户认证对话框 -->
@@ -192,4 +446,34 @@ const userLevel = computed(() => userStore.level)
<VDialogCloseBtn @click="restartDialog = false" /> <VDialogCloseBtn @click="restartDialog = false" />
</VCard> </VCard>
</VDialog> </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> </template>
<style lang="scss" scoped>
.v-list-item__prepend {
min-inline-size: 24px !important;
}
</style>
+20 -10
View File
@@ -21,6 +21,8 @@ export default {
create: 'Create', create: 'Create',
saving: 'Saving', saving: 'Saving',
reset: 'Reset', reset: 'Reset',
theme: 'Theme',
language: 'Language',
}, },
mediaType: { mediaType: {
movie: 'Movie', movie: 'Movie',
@@ -36,6 +38,9 @@ export default {
transparent: 'Transparent', transparent: 'Transparent',
purple: 'Purple', purple: 'Purple',
custom: 'Custom Theme', 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: { app: {
moviepilot: 'MoviePilot', moviepilot: 'MoviePilot',
@@ -43,7 +48,6 @@ export default {
subscribeMovie: 'Movie Subscription', subscribeMovie: 'Movie Subscription',
subscribeTv: 'TV Subscription', subscribeTv: 'TV Subscription',
settings: 'Settings', settings: 'Settings',
language: 'Language',
selectLanguage: 'Select Language', selectLanguage: 'Select Language',
logout: 'Logout', logout: 'Logout',
restarting: 'Restarting...', restarting: 'Restarting...',
@@ -137,7 +141,8 @@ export default {
}, },
words: { words: {
title: 'Word Lists', 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: { about: {
title: 'About', title: 'About',
@@ -937,9 +942,10 @@ export default {
season: 'Season {number}', season: 'Season {number}',
title: 'Title', title: 'Title',
description: 'Description', 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', shareUser: 'Share User',
shareUserHint: 'Sharer\'s nickname', shareUserHint: "Sharer's nickname",
confirmShare: 'Confirm Share', confirmShare: 'Confirm Share',
shareSuccess: '{name} shared successfully!', shareSuccess: '{name} shared successfully!',
shareFailed: '{name} share failed: {message}!', shareFailed: '{name} share failed: {message}!',
@@ -960,7 +966,8 @@ export default {
title: 'RClone Configuration', title: 'RClone Configuration',
filePath: 'rclone config file path', filePath: 'rclone config file path',
fileContent: 'rclone config file content', 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', complete: 'Complete',
}, },
alistConfig: { alistConfig: {
@@ -1030,7 +1037,7 @@ export default {
title: 'Update Site Cookie', title: 'Update Site Cookie',
checkHint: 'Checking login status, please wait...', checkHint: 'Checking login status, please wait...',
confirmUpdateTitle: 'Confirm Update', 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...', processing: 'Processing...',
success: 'Cookie updated successfully', success: 'Cookie updated successfully',
failed: 'Failed to update cookie', failed: 'Failed to update cookie',
@@ -1229,7 +1236,8 @@ export default {
mediaCategoryHint: 'Specify category name, leave empty for auto-recognition', mediaCategoryHint: 'Specify category name, leave empty for auto-recognition',
customWords: 'Custom Recognition Words', customWords: 'Custom Recognition Words',
customWordsHint: 'Recognition words only used for this subscription', 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', cancelSubscribe: 'Cancel Subscription',
save: 'Save', save: 'Save',
cancelSubscribeConfirm: 'Are you sure you want to cancel the subscription?', cancelSubscribeConfirm: 'Are you sure you want to cancel the subscription?',
@@ -1409,9 +1417,11 @@ export default {
otpEnableSuccess: 'Two-factor authentication enabled successfully!', otpEnableSuccess: 'Two-factor authentication enabled successfully!',
otpEnableFailed: 'Failed to enable OTP: {message}!', otpEnableFailed: 'Failed to enable OTP: {message}!',
authenticatorApp: 'Authenticator App', 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.', authenticatorAppDescription:
secretKeyTip: 'If you\'re having trouble with the QR code, select manual entry in your app and enter the code above.', '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', enterVerificationCode: 'Enter verification code to confirm enabling two-factor authentication',
avatarFormatTip: 'JPG, PNG, GIF, WEBP formats allowed, maximum size 800KB.', avatarFormatTip: 'JPG, PNG, GIF, WEBP formats allowed, maximum size 800KB.',
}, },
} }
+5
View File
@@ -21,6 +21,8 @@ export default {
create: '新建', create: '新建',
saving: '保存中', saving: '保存中',
reset: '重置', reset: '重置',
theme: '主题',
language: '语言',
}, },
mediaType: { mediaType: {
movie: '电影', movie: '电影',
@@ -36,6 +38,9 @@ export default {
transparent: '透明', transparent: '透明',
purple: '幻紫', purple: '幻紫',
custom: '自定义主题', custom: '自定义主题',
customCssSaveSuccess: '自定义CSS保存成功,请刷新页面生效!',
customCssSaveFailed: '保存自定义CSS到服务端失败',
deviceNotSupport: '当前设备不支持监听系统主题变化',
}, },
app: { app: {
moviepilot: 'MoviePilot', moviepilot: 'MoviePilot',
+5
View File
@@ -21,6 +21,8 @@ export default {
create: '新建', create: '新建',
saving: '保存中', saving: '保存中',
reset: '重置', reset: '重置',
theme: '主題',
language: '語言',
}, },
mediaType: { mediaType: {
movie: '電影', movie: '電影',
@@ -36,6 +38,9 @@ export default {
transparent: '透明', transparent: '透明',
purple: '幻紫', purple: '幻紫',
custom: '自定義主題', custom: '自定義主題',
customCssSaveSuccess: '自定義CSS保存成功,請刷新頁面生效!',
customCssSaveFailed: '保存自定義CSS到服務端失敗',
deviceNotSupport: '當前設備不支持監聽系統主題變化',
}, },
app: { app: {
moviepilot: 'MoviePilot', moviepilot: 'MoviePilot',