mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-17 13:42:07 +08:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5bc4e6baf | ||
|
|
15b4ee5893 | ||
|
|
8868403ff3 | ||
|
|
3abff72e25 | ||
|
|
0c56cf0be7 | ||
|
|
ce12d04648 | ||
|
|
efc0ae4df6 | ||
|
|
2530c3bcd9 | ||
|
|
60e2402aff | ||
|
|
1a478f97fb | ||
|
|
33666703af | ||
|
|
cd69172a99 | ||
|
|
61749e3595 | ||
|
|
b658533262 | ||
|
|
d8015b7def |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "2.13.10",
|
||||
"version": "2.13.11",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"bin": "dist/service.js",
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"root":["./build-icons.ts"],"version":"5.8.3"}
|
||||
{"root":["./build-icons.ts"],"errors":true,"version":"6.0.3"}
|
||||
2194
src/components/AgentAssistantWidget.vue
Normal file
2194
src/components/AgentAssistantWidget.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue'
|
||||
import {
|
||||
themeCustomizerPrimaryColors,
|
||||
useThemeCustomizer,
|
||||
@@ -12,20 +11,9 @@ import {
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { VDialog, VNavigationDrawer } from 'vuetify/components'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: boolean
|
||||
}>(),
|
||||
{
|
||||
modelValue: false,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
'close': []
|
||||
}>()
|
||||
|
||||
const customColorInput = ref<HTMLInputElement | null>(null)
|
||||
@@ -45,27 +33,7 @@ const {
|
||||
const { appMode } = usePWA()
|
||||
const { t } = useI18n()
|
||||
const { global: globalTheme } = useTheme()
|
||||
const display = useDisplay()
|
||||
const defaultPrimaryColor = themeCustomizerPrimaryColors[0].value
|
||||
const customizerViewportHeight = ref('100dvh')
|
||||
|
||||
const drawer = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => emit('update:modelValue', value),
|
||||
})
|
||||
|
||||
function getVisibleViewportHeight() {
|
||||
if (typeof window === 'undefined') return '100dvh'
|
||||
|
||||
const height = window.visualViewport?.height || window.innerHeight || document.documentElement.clientHeight
|
||||
|
||||
return height > 0 ? `${Math.round(height)}px` : '100dvh'
|
||||
}
|
||||
|
||||
// iOS 小屏的可见视口会随地址栏和独立模式 safe area 变化,面板高度需要跟随真实可见高度。
|
||||
function syncCustomizerViewportHeight() {
|
||||
customizerViewportHeight.value = getVisibleViewportHeight()
|
||||
}
|
||||
|
||||
// 将主题定制器打开状态同步到根节点,供全局悬浮按钮避让右侧面板。
|
||||
function syncThemeCustomizerOpenState(isOpen: boolean) {
|
||||
@@ -87,57 +55,22 @@ function clearThemeCustomizerOpenState() {
|
||||
syncThemeCustomizerOpenState(false)
|
||||
}
|
||||
|
||||
watch(drawer, syncThemeCustomizerOpenState, { immediate: true })
|
||||
watch(drawer, isOpen => {
|
||||
if (isOpen) nextTick(syncCustomizerViewportHeight)
|
||||
})
|
||||
function handleGlobalKeydown(event: KeyboardEvent) {
|
||||
// 固定侧栏不再依赖 Vuetify overlay,手动补上常见的 Esc 关闭行为。
|
||||
if (event.key === 'Escape') emit('close')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
syncCustomizerViewportHeight()
|
||||
window.addEventListener('resize', syncCustomizerViewportHeight)
|
||||
window.addEventListener('orientationchange', syncCustomizerViewportHeight)
|
||||
window.visualViewport?.addEventListener('resize', syncCustomizerViewportHeight)
|
||||
window.visualViewport?.addEventListener('scroll', syncCustomizerViewportHeight)
|
||||
// 面板一挂载就代表已打开,及时同步根节点状态让全局 FAB 预留右侧空间。
|
||||
syncThemeCustomizerOpenState(true)
|
||||
window.addEventListener('keydown', handleGlobalKeydown)
|
||||
})
|
||||
|
||||
onScopeDispose(clearThemeCustomizerOpenState)
|
||||
onScopeDispose(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
window.removeEventListener('resize', syncCustomizerViewportHeight)
|
||||
window.removeEventListener('orientationchange', syncCustomizerViewportHeight)
|
||||
window.visualViewport?.removeEventListener('resize', syncCustomizerViewportHeight)
|
||||
window.visualViewport?.removeEventListener('scroll', syncCustomizerViewportHeight)
|
||||
})
|
||||
|
||||
const customizerContainer = computed(() => (appMode.value ? VDialog : VNavigationDrawer))
|
||||
|
||||
const customizerContainerStyle = computed<CSSProperties>(() => {
|
||||
if (!appMode.value) return {}
|
||||
|
||||
return {
|
||||
'--theme-customizer-viewport-height': customizerViewportHeight.value,
|
||||
}
|
||||
})
|
||||
|
||||
const customizerContainerProps = computed(() => {
|
||||
if (appMode.value && display.mdAndDown.value) {
|
||||
return {
|
||||
class: 'theme-customizer-dialog-overlay',
|
||||
scrim: true,
|
||||
fullscreen: !display.mdAndUp.value,
|
||||
scrollable: true,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
class: 'theme-customizer-drawer',
|
||||
location: 'right' as const,
|
||||
scrim: false,
|
||||
temporary: true,
|
||||
width: 420,
|
||||
persistent: false,
|
||||
}
|
||||
window.removeEventListener('keydown', handleGlobalKeydown)
|
||||
})
|
||||
|
||||
const themeOptions = computed<Array<{ icon: string; title: string; value: ThemeCustomizerTheme }>>(() => [
|
||||
@@ -273,347 +206,264 @@ async function handleResetSettings() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="theme-customizer-glass">
|
||||
<div
|
||||
v-if="drawer"
|
||||
class="theme-customizer-glass-backdrop"
|
||||
:class="{ 'theme-customizer-glass-backdrop--dialog': appMode }"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<component
|
||||
:is="customizerContainer"
|
||||
v-model="drawer"
|
||||
v-bind="customizerContainerProps"
|
||||
:style="customizerContainerStyle"
|
||||
>
|
||||
<div
|
||||
class="theme-customizer-panel"
|
||||
:class="{ 'theme-customizer-panel--dialog': appMode, 'app-surface': appMode }"
|
||||
>
|
||||
<div class="theme-customizer-header py-5 px-4">
|
||||
<div>
|
||||
<h2 class="theme-customizer-title">{{ t('theme.customizer.title') }}</h2>
|
||||
</div>
|
||||
<div class="theme-customizer-header-actions">
|
||||
<VBadge color="error" dot :model-value="showResetBadge" location="top end" offset-x="2" offset-y="2">
|
||||
<IconBtn :aria-label="t('theme.customizer.reset')" @click="handleResetSettings">
|
||||
<VIcon class="text-high-emphasis" icon="mdi-refresh" />
|
||||
</IconBtn>
|
||||
</VBadge>
|
||||
<IconBtn :aria-label="t('common.close')" @click="drawer = false">
|
||||
<VIcon class="text-high-emphasis" icon="mdi-close" />
|
||||
</IconBtn>
|
||||
</div>
|
||||
<aside
|
||||
class="theme-customizer-panel-host"
|
||||
role="dialog"
|
||||
:aria-label="t('theme.customizer.title')"
|
||||
>
|
||||
<div class="theme-customizer-panel" :class="{ 'theme-customizer-panel--dialog': appMode, 'app-surface': appMode }">
|
||||
<div class="theme-customizer-header py-5 px-4">
|
||||
<div>
|
||||
<h2 class="theme-customizer-title">{{ t('theme.customizer.title') }}</h2>
|
||||
</div>
|
||||
<div class="theme-customizer-header-actions">
|
||||
<VBadge color="error" dot :model-value="showResetBadge" location="top end" offset-x="2" offset-y="2">
|
||||
<IconBtn :aria-label="t('theme.customizer.reset')" @click="handleResetSettings">
|
||||
<VIcon class="text-high-emphasis" icon="mdi-refresh" />
|
||||
</IconBtn>
|
||||
</VBadge>
|
||||
<IconBtn :aria-label="t('common.close')" @click="emit('close')">
|
||||
<VIcon class="text-high-emphasis" icon="mdi-close" />
|
||||
</IconBtn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VDivider />
|
||||
<VDivider />
|
||||
|
||||
<PerfectScrollbar class="theme-customizer-body" :options="{ wheelPropagation: false }">
|
||||
<section class="theme-customizer-section">
|
||||
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.primaryColor') }}</h3>
|
||||
<div class="theme-customizer-color-grid">
|
||||
<div
|
||||
v-for="color in themeCustomizerPrimaryColors"
|
||||
:key="color.value"
|
||||
class="theme-customizer-color-option"
|
||||
:class="{ 'is-active': settings.primaryColor === color.value }"
|
||||
:aria-label="t('theme.customizer.usePrimaryColor', { color: color.name })"
|
||||
@click="setPrimaryColor(color.value)"
|
||||
>
|
||||
<span class="theme-customizer-color-swatch" :style="{ backgroundColor: color.value }" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!appMode"
|
||||
class="theme-customizer-color-option theme-customizer-color-option--picker"
|
||||
:class="{
|
||||
'is-active': !themeCustomizerPrimaryColors.some(color => color.value === settings.primaryColor),
|
||||
}"
|
||||
:aria-label="t('theme.customizer.chooseCustomColor')"
|
||||
@click="openColorPicker"
|
||||
>
|
||||
<VIcon class="theme-customizer-native-icon" icon="mdi-palette-outline" size="30" />
|
||||
<input
|
||||
ref="customColorInput"
|
||||
class="theme-customizer-native-color"
|
||||
type="color"
|
||||
:value="settings.primaryColor"
|
||||
@input="handleCustomColorInput"
|
||||
/>
|
||||
</div>
|
||||
<PerfectScrollbar class="theme-customizer-body" :options="{ wheelPropagation: false }">
|
||||
<section class="theme-customizer-section">
|
||||
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.primaryColor') }}</h3>
|
||||
<div class="theme-customizer-color-grid">
|
||||
<div
|
||||
v-for="color in themeCustomizerPrimaryColors"
|
||||
:key="color.value"
|
||||
class="theme-customizer-color-option"
|
||||
:class="{ 'is-active': settings.primaryColor === color.value }"
|
||||
:aria-label="t('theme.customizer.usePrimaryColor', { color: color.name })"
|
||||
@click="setPrimaryColor(color.value)"
|
||||
>
|
||||
<span class="theme-customizer-color-swatch" :style="{ backgroundColor: color.value }" />
|
||||
</div>
|
||||
|
||||
<h3 class="theme-customizer-section-title">{{ t('common.theme') }}</h3>
|
||||
<div class="theme-customizer-option-grid theme-customizer-option-grid--theme">
|
||||
<div
|
||||
v-for="theme in themeOptions"
|
||||
:key="theme.value"
|
||||
class="theme-customizer-card-option"
|
||||
:class="{ 'is-active': settings.theme === theme.value }"
|
||||
@click="setTheme(theme.value)"
|
||||
>
|
||||
<VIcon class="theme-customizer-theme-icon" :icon="theme.icon" size="36" />
|
||||
<span>{{ theme.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VDivider class="mt-7" />
|
||||
|
||||
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.skins') }}</h3>
|
||||
<div class="theme-customizer-preview-grid theme-customizer-preview-grid--skins">
|
||||
<div
|
||||
v-for="skin in skinOptions"
|
||||
:key="skin.value"
|
||||
class="theme-customizer-preview-option"
|
||||
:class="{ 'is-active': settings.skin === skin.value }"
|
||||
@click="setSkin(skin.value)"
|
||||
>
|
||||
<span class="theme-customizer-mini-layout" :class="`theme-customizer-mini-layout--${skin.value}`">
|
||||
<span class="mini-sidebar">
|
||||
<i />
|
||||
<i />
|
||||
<i />
|
||||
<i />
|
||||
</span>
|
||||
<span class="mini-content">
|
||||
<i />
|
||||
<i />
|
||||
<i />
|
||||
</span>
|
||||
</span>
|
||||
<span>{{ skin.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VDivider class="mt-7" />
|
||||
|
||||
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.radius') }}</h3>
|
||||
<div class="theme-customizer-preview-grid theme-customizer-preview-grid--radius">
|
||||
<div
|
||||
v-for="radius in radiusOptions"
|
||||
:key="radius.value"
|
||||
class="theme-customizer-preview-option"
|
||||
:class="{ 'is-active': settings.radius === radius.value }"
|
||||
@click="setRadius(radius.value)"
|
||||
>
|
||||
<span
|
||||
class="theme-customizer-radius-scene"
|
||||
:style="{ '--theme-customizer-radius-preview': radius.previewRadius }"
|
||||
>
|
||||
<span class="theme-customizer-radius-scene__card">
|
||||
<span class="theme-customizer-radius-scene__badge" />
|
||||
<span class="theme-customizer-radius-scene__line" />
|
||||
<span class="theme-customizer-radius-scene__line theme-customizer-radius-scene__line--short" />
|
||||
</span>
|
||||
</span>
|
||||
<span>{{ radius.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VDivider class="mt-7" />
|
||||
|
||||
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.shadow') }}</h3>
|
||||
<div class="theme-customizer-preview-grid theme-customizer-preview-grid--shadow">
|
||||
<div
|
||||
v-for="shadow in shadowOptions"
|
||||
:key="shadow.value"
|
||||
class="theme-customizer-preview-option"
|
||||
:class="{ 'is-active': settings.shadow === shadow.value }"
|
||||
@click="setShadow(shadow.value)"
|
||||
>
|
||||
<span class="theme-customizer-shadow-scene" :class="`theme-customizer-shadow-scene--${shadow.value}`">
|
||||
<span class="theme-customizer-shadow-scene__panel">
|
||||
<span class="theme-customizer-shadow-scene__panel-line" />
|
||||
<span
|
||||
class="theme-customizer-shadow-scene__panel-line theme-customizer-shadow-scene__panel-line--short"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span class="theme-customizer-shadow-scene__card">
|
||||
<span class="theme-customizer-shadow-scene__badge" />
|
||||
<span class="theme-customizer-shadow-scene__line theme-customizer-shadow-scene__line--short" />
|
||||
<span class="theme-customizer-shadow-scene__line" />
|
||||
</span>
|
||||
</span>
|
||||
<span>{{ shadow.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showSemiDarkMenuOption" class="theme-customizer-semi-dark">
|
||||
<span>{{ t('theme.customizer.semiDarkMenu') }}</span>
|
||||
<VSwitch
|
||||
:model-value="settings.semiDarkMenu"
|
||||
color="primary"
|
||||
inset
|
||||
hide-details
|
||||
@update:model-value="setSemiDarkMenu(Boolean($event))"
|
||||
<div
|
||||
v-if="!appMode"
|
||||
class="theme-customizer-color-option theme-customizer-color-option--picker"
|
||||
:class="{
|
||||
'is-active': !themeCustomizerPrimaryColors.some(color => color.value === settings.primaryColor),
|
||||
}"
|
||||
:aria-label="t('theme.customizer.chooseCustomColor')"
|
||||
@click="openColorPicker"
|
||||
>
|
||||
<VIcon class="theme-customizer-native-icon" icon="mdi-palette-outline" size="30" />
|
||||
<input
|
||||
ref="customColorInput"
|
||||
class="theme-customizer-native-color"
|
||||
type="color"
|
||||
:value="settings.primaryColor"
|
||||
@input="handleCustomColorInput"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<VDivider v-if="showLayoutSection" />
|
||||
|
||||
<section v-if="showLayoutSection" class="theme-customizer-section">
|
||||
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.layout') }}</h3>
|
||||
<div class="theme-customizer-preview-grid">
|
||||
<div
|
||||
v-for="layout in layoutOptions"
|
||||
:key="layout.value"
|
||||
class="theme-customizer-preview-option"
|
||||
:class="{ 'is-active': settings.layout === layout.value, 'is-disabled': appMode }"
|
||||
@click="handleLayoutChange(layout.value)"
|
||||
>
|
||||
<span class="theme-customizer-mini-layout" :class="`theme-customizer-mini-layout--${layout.value}`">
|
||||
<span class="mini-sidebar">
|
||||
<i />
|
||||
<i />
|
||||
<i />
|
||||
</span>
|
||||
<span class="mini-content">
|
||||
<i />
|
||||
<i />
|
||||
<i />
|
||||
</span>
|
||||
</span>
|
||||
<span>{{ layout.title }}</span>
|
||||
</div>
|
||||
<h3 class="theme-customizer-section-title">{{ t('common.theme') }}</h3>
|
||||
<div class="theme-customizer-option-grid theme-customizer-option-grid--theme">
|
||||
<div
|
||||
v-for="theme in themeOptions"
|
||||
:key="theme.value"
|
||||
class="theme-customizer-card-option"
|
||||
:class="{ 'is-active': settings.theme === theme.value }"
|
||||
@click="setTheme(theme.value)"
|
||||
>
|
||||
<VIcon class="theme-customizer-theme-icon" :icon="theme.icon" size="36" />
|
||||
<span>{{ theme.title }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</PerfectScrollbar>
|
||||
</div>
|
||||
</component>
|
||||
</Teleport>
|
||||
</div>
|
||||
|
||||
<VDivider class="mt-7" />
|
||||
|
||||
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.skins') }}</h3>
|
||||
<div class="theme-customizer-preview-grid theme-customizer-preview-grid--skins">
|
||||
<div
|
||||
v-for="skin in skinOptions"
|
||||
:key="skin.value"
|
||||
class="theme-customizer-preview-option"
|
||||
:class="{ 'is-active': settings.skin === skin.value }"
|
||||
@click="setSkin(skin.value)"
|
||||
>
|
||||
<span class="theme-customizer-mini-layout" :class="`theme-customizer-mini-layout--${skin.value}`">
|
||||
<span class="mini-sidebar">
|
||||
<i />
|
||||
<i />
|
||||
<i />
|
||||
<i />
|
||||
</span>
|
||||
<span class="mini-content">
|
||||
<i />
|
||||
<i />
|
||||
<i />
|
||||
</span>
|
||||
</span>
|
||||
<span>{{ skin.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VDivider class="mt-7" />
|
||||
|
||||
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.radius') }}</h3>
|
||||
<div class="theme-customizer-preview-grid theme-customizer-preview-grid--radius">
|
||||
<div
|
||||
v-for="radius in radiusOptions"
|
||||
:key="radius.value"
|
||||
class="theme-customizer-preview-option"
|
||||
:class="{ 'is-active': settings.radius === radius.value }"
|
||||
@click="setRadius(radius.value)"
|
||||
>
|
||||
<span
|
||||
class="theme-customizer-radius-scene"
|
||||
:style="{ '--theme-customizer-radius-preview': radius.previewRadius }"
|
||||
>
|
||||
<span class="theme-customizer-radius-scene__card">
|
||||
<span class="theme-customizer-radius-scene__badge" />
|
||||
<span class="theme-customizer-radius-scene__line" />
|
||||
<span class="theme-customizer-radius-scene__line theme-customizer-radius-scene__line--short" />
|
||||
</span>
|
||||
</span>
|
||||
<span>{{ radius.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VDivider class="mt-7" />
|
||||
|
||||
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.shadow') }}</h3>
|
||||
<div class="theme-customizer-preview-grid theme-customizer-preview-grid--shadow">
|
||||
<div
|
||||
v-for="shadow in shadowOptions"
|
||||
:key="shadow.value"
|
||||
class="theme-customizer-preview-option"
|
||||
:class="{ 'is-active': settings.shadow === shadow.value }"
|
||||
@click="setShadow(shadow.value)"
|
||||
>
|
||||
<span class="theme-customizer-shadow-scene" :class="`theme-customizer-shadow-scene--${shadow.value}`">
|
||||
<span class="theme-customizer-shadow-scene__panel">
|
||||
<span class="theme-customizer-shadow-scene__panel-line" />
|
||||
<span
|
||||
class="theme-customizer-shadow-scene__panel-line theme-customizer-shadow-scene__panel-line--short"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span class="theme-customizer-shadow-scene__card">
|
||||
<span class="theme-customizer-shadow-scene__badge" />
|
||||
<span class="theme-customizer-shadow-scene__line theme-customizer-shadow-scene__line--short" />
|
||||
<span class="theme-customizer-shadow-scene__line" />
|
||||
</span>
|
||||
</span>
|
||||
<span>{{ shadow.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showSemiDarkMenuOption" class="theme-customizer-semi-dark">
|
||||
<span>{{ t('theme.customizer.semiDarkMenu') }}</span>
|
||||
<VSwitch
|
||||
:model-value="settings.semiDarkMenu"
|
||||
color="primary"
|
||||
inset
|
||||
hide-details
|
||||
@update:model-value="setSemiDarkMenu(Boolean($event))"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<VDivider v-if="showLayoutSection" />
|
||||
|
||||
<section v-if="showLayoutSection" class="theme-customizer-section">
|
||||
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.layout') }}</h3>
|
||||
<div class="theme-customizer-preview-grid">
|
||||
<div
|
||||
v-for="layout in layoutOptions"
|
||||
:key="layout.value"
|
||||
class="theme-customizer-preview-option"
|
||||
:class="{ 'is-active': settings.layout === layout.value, 'is-disabled': appMode }"
|
||||
@click="handleLayoutChange(layout.value)"
|
||||
>
|
||||
<span class="theme-customizer-mini-layout" :class="`theme-customizer-mini-layout--${layout.value}`">
|
||||
<span class="mini-sidebar">
|
||||
<i />
|
||||
<i />
|
||||
<i />
|
||||
</span>
|
||||
<span class="mini-content">
|
||||
<i />
|
||||
<i />
|
||||
<i />
|
||||
</span>
|
||||
</span>
|
||||
<span>{{ layout.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</PerfectScrollbar>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
|
||||
.theme-customizer-drawer {
|
||||
.theme-customizer-panel-host {
|
||||
position: fixed !important;
|
||||
z-index: 12000 !important;
|
||||
z-index: 2102 !important;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
block-size: 100dvh !important;
|
||||
border-radius: 0;
|
||||
background: rgb(var(--v-theme-surface));
|
||||
|
||||
/* 背景层保持完整视口高度,避免 iOS 键盘触发 visual viewport resize 后露出底层页面。 */
|
||||
block-size: 100vh !important;
|
||||
border-inline-start: 1px solid rgba(var(--v-theme-on-surface), 0.08) !important;
|
||||
box-shadow: var(--app-surface-shadow) !important;
|
||||
inset-block: 0 !important;
|
||||
inline-size: 420px !important;
|
||||
inset-block-start: 0 !important;
|
||||
inset-inline-end: 0 !important;
|
||||
max-block-size: 100dvh !important;
|
||||
max-block-size: none !important;
|
||||
min-block-size: 100vh !important;
|
||||
}
|
||||
|
||||
.v-navigation-drawer__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
block-size: 100%;
|
||||
@supports (block-size: 100lvh) {
|
||||
.theme-customizer-panel-host {
|
||||
block-size: 100lvh !important;
|
||||
min-block-size: 100lvh !important;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-customizer-dialog-overlay {
|
||||
--theme-customizer-viewport-height: 100dvh;
|
||||
|
||||
z-index: 12000 !important;
|
||||
}
|
||||
|
||||
.theme-customizer-dialog-overlay > .v-overlay__content {
|
||||
overflow: hidden;
|
||||
block-size: var(--theme-customizer-viewport-height);
|
||||
margin-block: 0 !important;
|
||||
max-block-size: var(--theme-customizer-viewport-height);
|
||||
}
|
||||
|
||||
.theme-customizer-panel {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.theme-customizer-panel--dialog {
|
||||
overflow: hidden;
|
||||
background: rgb(var(--v-theme-surface));
|
||||
block-size: var(--theme-customizer-viewport-height, 100dvh);
|
||||
max-block-size: var(--theme-customizer-viewport-height, 100dvh);
|
||||
block-size: 100%;
|
||||
max-block-size: 100%;
|
||||
|
||||
/* fullscreen dialog 会贴到 viewport-fit=cover 顶部,iOS 需要在面板内部避开系统状态栏。 */
|
||||
padding-block-start: env(safe-area-inset-top);
|
||||
/* 独立 App 模式会贴近 viewport-fit=cover 顶部,面板内部需要避开 iOS 状态栏。 */
|
||||
padding-block-start: env(safe-area-inset-top, 0px);
|
||||
}
|
||||
|
||||
.theme-customizer-panel--dialog .theme-customizer-body {
|
||||
block-size: auto;
|
||||
padding-block-end: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.theme-customizer-drawer.v-theme--transparent,
|
||||
.v-theme--transparent .theme-customizer-drawer,
|
||||
html[data-theme='transparent'] .theme-customizer-drawer,
|
||||
.v-theme--transparent .theme-customizer-panel--dialog,
|
||||
html[data-theme='transparent'] .theme-customizer-panel--dialog {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.theme-customizer-glass-backdrop {
|
||||
position: fixed;
|
||||
z-index: 11999;
|
||||
block-size: 100dvh;
|
||||
inline-size: 420px;
|
||||
inset-block: 0;
|
||||
inset-inline-end: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
:is(html[data-theme='transparent'], .v-theme--transparent) .theme-customizer-glass-backdrop {
|
||||
backdrop-filter: blur(var(--transparent-blur-heavy, 16px));
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy, 0.5));
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.theme-customizer-glass-enter-active,
|
||||
.theme-customizer-glass-leave-active {
|
||||
transition:
|
||||
opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.theme-customizer-glass-enter-from,
|
||||
.theme-customizer-glass-leave-to {
|
||||
opacity: 0 !important;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
:is(html[data-theme='transparent'], .v-theme--transparent) .theme-customizer-drawer .v-navigation-drawer__content {
|
||||
background: transparent !important;
|
||||
padding-block-end: env(safe-area-inset-bottom, 0px);
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.theme-customizer-glass-backdrop {
|
||||
inline-size: 100vw;
|
||||
.theme-customizer-panel-host {
|
||||
inline-size: 100vw !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 透明主题的全局 overlay 毛玻璃会影响临时抽屉绘制,主题定制器改由 drawer 自身承担背景。
|
||||
html[data-theme='transparent'] .v-overlay__content:has(.theme-customizer-drawer),
|
||||
.v-theme--transparent .v-overlay__content:has(.theme-customizer-drawer) {
|
||||
border-radius: 0 !important;
|
||||
backdrop-filter: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
:is(html[data-theme='transparent'], .v-theme--transparent) .theme-customizer-card-option .theme-customizer-theme-icon,
|
||||
:is(html[data-theme='transparent'], .v-theme--transparent)
|
||||
.theme-customizer-color-option
|
||||
.theme-customizer-native-icon {
|
||||
backdrop-filter: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.theme-customizer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -638,6 +488,18 @@ html[data-theme='transparent'] .v-overlay__content:has(.theme-customizer-drawer)
|
||||
.theme-customizer-body {
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
-ms-overflow-style: none;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.ps__rail-x),
|
||||
:deep(.ps__rail-y) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-customizer-section {
|
||||
@@ -1030,10 +892,6 @@ html[data-theme='transparent'] .v-overlay__content:has(.theme-customizer-drawer)
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.theme-customizer-drawer {
|
||||
inline-size: min(100vw, 420px) !important;
|
||||
}
|
||||
|
||||
.theme-customizer-header,
|
||||
.theme-customizer-section {
|
||||
padding-inline: 22px;
|
||||
|
||||
@@ -99,9 +99,9 @@ function submitCustomCSS() {
|
||||
<style scoped>
|
||||
.custom-css-dialog {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
max-block-size: calc(100dvh - 2rem);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.custom-css-header {
|
||||
@@ -111,7 +111,7 @@ function submitCustomCSS() {
|
||||
|
||||
.custom-css-editor-body {
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
min-block-size: 240px;
|
||||
}
|
||||
|
||||
.custom-css-editor {
|
||||
@@ -141,8 +141,8 @@ function submitCustomCSS() {
|
||||
|
||||
.custom-css-editor {
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
block-size: auto;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.custom-css-actions {
|
||||
|
||||
@@ -141,4 +141,29 @@ function updateFilter(key: string, values: string[]) {
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
border: 1px solid rgba(var(--v-theme-primary), 0.2);
|
||||
margin: 4px;
|
||||
background-color: rgba(var(--v-theme-primary), 0.1) !important;
|
||||
color: rgba(var(--v-theme-on-surface), 0.9) !important;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.filter-chip:hover {
|
||||
background-color: rgba(var(--v-theme-primary), 0.15) !important;
|
||||
}
|
||||
|
||||
.filter-chip.v-chip--selected {
|
||||
background-color: rgba(var(--v-theme-primary), 0.85) !important;
|
||||
box-shadow: 0 2px 4px rgba(var(--v-theme-primary), 0.3);
|
||||
color: rgb(var(--v-theme-on-primary)) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -142,4 +142,24 @@ function handleDetail(item: Context) {
|
||||
max-block-size: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.chip-season {
|
||||
background-color: #3f51b5;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-free {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-discount {
|
||||
background-color: #ff5722;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-bonus {
|
||||
background-color: #9c27b0;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -85,7 +85,7 @@ function updateFilter(values: string[]) {
|
||||
@update:model-value="updateFilter"
|
||||
>
|
||||
<VChip
|
||||
v-for="option in options"
|
||||
v-for="option in options"
|
||||
:key="option"
|
||||
:value="option"
|
||||
filter
|
||||
@@ -106,3 +106,30 @@ function updateFilter(values: string[]) {
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.filter-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
border: 1px solid rgba(var(--v-theme-primary), 0.2);
|
||||
margin: 4px;
|
||||
background-color: rgba(var(--v-theme-primary), 0.1) !important;
|
||||
color: rgba(var(--v-theme-on-surface), 0.9) !important;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.filter-chip:hover {
|
||||
background-color: rgba(var(--v-theme-primary), 0.15) !important;
|
||||
}
|
||||
|
||||
.filter-chip.v-chip--selected {
|
||||
background-color: rgba(var(--v-theme-primary), 0.85) !important;
|
||||
box-shadow: 0 2px 4px rgba(var(--v-theme-primary), 0.3);
|
||||
color: rgb(var(--v-theme-on-primary)) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -372,7 +372,7 @@ onMounted(() => {
|
||||
:key="key"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
:color="filterForm[key].length > 0 ? 'primary' : undefined"
|
||||
color="primary"
|
||||
:prepend-icon="getFilterIcon(key)"
|
||||
class="filter-btn"
|
||||
rounded="pill"
|
||||
@@ -555,7 +555,7 @@ onMounted(() => {
|
||||
v-for="(title, key) in filterTitles"
|
||||
v-show="filterOptions[key].length > 0"
|
||||
:key="key"
|
||||
variant="text"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
class="filter-btn-mobile"
|
||||
@click="toggleFilterMenu(key)"
|
||||
@@ -575,7 +575,7 @@ onMounted(() => {
|
||||
</VBtn>
|
||||
|
||||
<!-- 全部筛选按钮 -->
|
||||
<VBtn variant="text" color="primary" class="filter-btn-mobile" @click="toggleAllFilterMenu">
|
||||
<VBtn variant="tonal" color="primary" class="filter-btn-mobile" @click="toggleAllFilterMenu">
|
||||
<VIcon icon="mdi-filter-variant" class="filter-icon me-1"></VIcon>
|
||||
<span class="filter-label">
|
||||
{{ t('torrent.allFilters') }}
|
||||
@@ -665,7 +665,6 @@ onMounted(() => {
|
||||
|
||||
.filter-btn {
|
||||
min-inline-size: 0;
|
||||
background: rgba(var(--v-theme-surface-variant), 0.1);
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
@@ -733,7 +732,6 @@ onMounted(() => {
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
border-radius: 8px;
|
||||
background-color: rgba(var(--v-theme-surface-variant), 0.08);
|
||||
block-size: auto;
|
||||
min-block-size: 48px;
|
||||
padding-block: 4px;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { themeManager } from '@/utils/themeManager'
|
||||
|
||||
export const THEME_CUSTOMIZER_STORAGE_KEY = 'moviepilot-theme-customizer'
|
||||
export const THEME_CUSTOMIZER_CHANGE_EVENT = 'moviepilot-theme-customizer-change'
|
||||
export const THEME_CUSTOMIZER_OPEN_EVENT = 'moviepilot-theme-customizer-open'
|
||||
|
||||
export const themeCustomizerPrimaryColors = [
|
||||
{ name: 'Purple', value: '#9155FD' },
|
||||
|
||||
@@ -9,7 +9,9 @@ import ShortcutBar from '@/layouts/components/ShortcutBar.vue'
|
||||
import UserProfile from '@/layouts/components/UserProfile.vue'
|
||||
import QuickAccess from '@/layouts/components/QuickAccess.vue'
|
||||
import HeaderTab from '@/layouts/components/HeaderTab.vue'
|
||||
import { usePluginSidebarNavStore, useUserStore } from '@/stores'
|
||||
import AgentAssistantWidget from '@/components/AgentAssistantWidget.vue'
|
||||
import ThemeCustomizer from '@/components/ThemeCustomizer.vue'
|
||||
import { useGlobalSettingsStore, usePluginSidebarNavStore, useUserStore } from '@/stores'
|
||||
import { getNavMenus } from '@/router/i18n-menu'
|
||||
import { filterPluginSidebarNavEntries } from '@/utils/pluginSidebarNav'
|
||||
import { NavMenu } from '@/@layouts/types'
|
||||
@@ -31,6 +33,7 @@ import { useGlobalOfflineStatus } from '@/composables/useOfflineStatus'
|
||||
import {
|
||||
readThemeCustomizerSettings,
|
||||
THEME_CUSTOMIZER_CHANGE_EVENT,
|
||||
THEME_CUSTOMIZER_OPEN_EVENT,
|
||||
type ThemeCustomizerSettings,
|
||||
} from '@/composables/useThemeCustomizer'
|
||||
import logo from '@images/logo.svg?raw'
|
||||
@@ -42,14 +45,17 @@ const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const themeLayout = ref(readThemeCustomizerSettings().layout)
|
||||
const showThemeCustomizer = ref(false)
|
||||
|
||||
// 用户 Store
|
||||
const userStore = useUserStore()
|
||||
const pluginSidebarNavStore = usePluginSidebarNavStore()
|
||||
const globalSettingsStore = useGlobalSettingsStore()
|
||||
|
||||
// 获取用户权限信息
|
||||
const userPermissions = computed(() => buildUserPermissionContext(userStore.superUser, userStore.permissions))
|
||||
const canAdmin = computed(() => hasPermission(userPermissions.value, 'admin'))
|
||||
const showAgentAssistant = computed(() => globalSettingsStore.get('AI_AGENT_ENABLE') === true)
|
||||
|
||||
// 开始菜单项
|
||||
const startMenus = ref<NavMenu[]>([])
|
||||
@@ -279,6 +285,10 @@ function handleThemeCustomizerChange(event: Event) {
|
||||
themeLayout.value = (event as CustomEvent<ThemeCustomizerSettings>).detail.layout
|
||||
}
|
||||
|
||||
function handleThemeCustomizerOpen() {
|
||||
showThemeCustomizer.value = true
|
||||
}
|
||||
|
||||
function isHorizontalNavActive(item: NavMenu) {
|
||||
const targetPath = normalizeMenuPath(item.to)
|
||||
if (!targetPath) return false
|
||||
@@ -416,6 +426,10 @@ function appendPluginSidebarMenus() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 主题定制器由布局统一承载,监听需要尽早注册,避免异步加载菜单期间丢失打开事件。
|
||||
window.addEventListener(THEME_CUSTOMIZER_CHANGE_EVENT, handleThemeCustomizerChange)
|
||||
window.addEventListener(THEME_CUSTOMIZER_OPEN_EVENT, handleThemeCustomizerOpen)
|
||||
|
||||
// 获取菜单列表
|
||||
startMenus.value = getMenuList(t('menu.start'))
|
||||
discoveryMenus.value = getMenuList(t('menu.discovery'))
|
||||
@@ -431,11 +445,10 @@ onMounted(async () => {
|
||||
navigator.serviceWorker.addEventListener('message', handleServiceWorkerMessage)
|
||||
}
|
||||
|
||||
window.addEventListener(THEME_CUSTOMIZER_CHANGE_EVENT, handleThemeCustomizerChange)
|
||||
|
||||
// 组件卸载时清理监听
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener(THEME_CUSTOMIZER_CHANGE_EVENT, handleThemeCustomizerChange)
|
||||
window.removeEventListener(THEME_CUSTOMIZER_OPEN_EVENT, handleThemeCustomizerOpen)
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.removeEventListener('message', handleServiceWorkerMessage)
|
||||
}
|
||||
@@ -692,6 +705,12 @@ onMounted(async () => {
|
||||
@close="handleClosePluginQuickAccess"
|
||||
@plugin-click="handlePluginClick"
|
||||
/>
|
||||
|
||||
<!-- 👉 Theme Customizer -->
|
||||
<ThemeCustomizer v-if="showThemeCustomizer" @close="showThemeCustomizer = false" />
|
||||
|
||||
<!-- 👉 Agent Assistant -->
|
||||
<AgentAssistantWidget v-if="showAgentAssistant" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
persistPartialThemeCustomizerSettings,
|
||||
readThemeCustomizerSettings,
|
||||
THEME_CUSTOMIZER_CHANGE_EVENT,
|
||||
THEME_CUSTOMIZER_OPEN_EVENT,
|
||||
type ThemeCustomizerSettings,
|
||||
} from '@/composables/useThemeCustomizer'
|
||||
import { buildUserPermissionContext, hasPermission } from '@/utils/permission'
|
||||
@@ -30,7 +31,6 @@ const ProgressDialog = defineAsyncComponent(() => import('@/components/dialog/Pr
|
||||
const TransparencySettingsDialog = defineAsyncComponent(
|
||||
() => import('@/components/dialog/TransparencySettingsDialog.vue'),
|
||||
)
|
||||
const ThemeCustomizer = defineAsyncComponent(() => import('@/components/ThemeCustomizer.vue'))
|
||||
const UserAuthDialog = defineAsyncComponent(() => import('@/components/dialog/UserAuthDialog.vue'))
|
||||
|
||||
// 认证 Store
|
||||
@@ -50,12 +50,12 @@ const $toast = useToast()
|
||||
// UI模式菜单是否显示
|
||||
const showUIModeMenu = ref(false)
|
||||
|
||||
// 用户头像主菜单是否显示;打开布局级面板前需要主动关闭,避免菜单 overlay 残留。
|
||||
const showUserMenu = ref(false)
|
||||
|
||||
// 主题菜单是否显示
|
||||
const showThemeMenu = ref(false)
|
||||
|
||||
// 主题定制器面板是否显示
|
||||
const showThemeCustomizer = ref(false)
|
||||
|
||||
// 语言菜单是否显示
|
||||
const showLanguageMenu = ref(false)
|
||||
|
||||
@@ -442,8 +442,11 @@ function showTransparencySettingsDialog() {
|
||||
|
||||
/** 从用户菜单打开主题定制器,App 模式会在面板内部隐藏布局设置。 */
|
||||
function showThemeCustomizerDrawer() {
|
||||
showUserMenu.value = false
|
||||
showThemeMenu.value = false
|
||||
showThemeCustomizer.value = true
|
||||
|
||||
// 主题定制器由 DefaultLayout 统一挂载
|
||||
window.dispatchEvent(new CustomEvent(THEME_CUSTOMIZER_OPEN_EVENT))
|
||||
}
|
||||
|
||||
/** 保存自定义 CSS。 */
|
||||
@@ -558,6 +561,7 @@ onUnmounted(() => {
|
||||
<VImg :src="avatar" />
|
||||
|
||||
<VMenu
|
||||
v-model="showUserMenu"
|
||||
activator="parent"
|
||||
width="15rem"
|
||||
location="bottom end"
|
||||
@@ -777,7 +781,6 @@ onUnmounted(() => {
|
||||
</VMenu>
|
||||
<!-- !SECTION -->
|
||||
</VAvatar>
|
||||
<ThemeCustomizer v-model="showThemeCustomizer" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -697,6 +697,35 @@ export default {
|
||||
subtitle: 'Scheduled Services',
|
||||
},
|
||||
},
|
||||
agentAssistant: {
|
||||
title: 'AI Assistant',
|
||||
assistant: 'Assistant',
|
||||
ready: 'Ready',
|
||||
thinking: 'Thinking',
|
||||
newChat: 'New Chat',
|
||||
history: 'Chat History',
|
||||
noHistory: 'No chat history yet',
|
||||
deleteHistory: 'Delete chat history',
|
||||
untitledSession: 'Untitled chat',
|
||||
emptyTitle: 'What should we handle today?',
|
||||
emptySubtitle: 'Ask about sites, subscriptions, downloads, or organization tasks.',
|
||||
placeholder: 'Ask MoviePilot...',
|
||||
stop: 'Stop generating',
|
||||
download: 'Download',
|
||||
attachFile: 'Choose image or file',
|
||||
recordVoice: 'Record voice',
|
||||
stopRecording: 'Stop recording ({time})',
|
||||
attachmentMessage: 'Attachment message',
|
||||
removeAttachment: 'Remove attachment',
|
||||
uploadFailed: 'Attachment upload failed',
|
||||
recordUnsupported: 'Voice recording is not supported by this browser',
|
||||
recordPermissionDenied: 'Cannot access the microphone. Please check browser permissions.',
|
||||
recordFailed: 'Voice recording failed. Please try again.',
|
||||
choiceSelected: 'Selected: {option}',
|
||||
choiceExpired: 'This choice expired. Please ask again.',
|
||||
error: 'Assistant response failed',
|
||||
noStream: 'This browser cannot read streaming responses',
|
||||
},
|
||||
workflow: {
|
||||
components: 'Action Components',
|
||||
clickToAdd: 'Click to Add',
|
||||
|
||||
@@ -693,6 +693,35 @@ export default {
|
||||
subtitle: '定时服务',
|
||||
},
|
||||
},
|
||||
agentAssistant: {
|
||||
title: '智能助手',
|
||||
assistant: '助手',
|
||||
ready: '随时待命',
|
||||
thinking: '思考中',
|
||||
newChat: '新会话',
|
||||
history: '历史会话',
|
||||
noHistory: '暂无历史会话',
|
||||
deleteHistory: '删除历史会话',
|
||||
untitledSession: '未命名会话',
|
||||
emptyTitle: '今天想处理什么?',
|
||||
emptySubtitle: '站点、订阅、下载、整理任务,都可以直接问我。',
|
||||
placeholder: '询问 MoviePilot...',
|
||||
stop: '停止生成',
|
||||
download: '下载',
|
||||
attachFile: '选择图片或文件',
|
||||
recordVoice: '录制语音',
|
||||
stopRecording: '停止录音({time})',
|
||||
attachmentMessage: '附件消息',
|
||||
removeAttachment: '移除附件',
|
||||
uploadFailed: '附件上传失败',
|
||||
recordUnsupported: '当前浏览器不支持录音',
|
||||
recordPermissionDenied: '无法访问麦克风,请检查浏览器权限',
|
||||
recordFailed: '录音失败,请重试',
|
||||
choiceSelected: '已选择:{option}',
|
||||
choiceExpired: '该选择已失效,请重新发起选择',
|
||||
error: '智能助手响应失败',
|
||||
noStream: '当前浏览器无法读取流式响应',
|
||||
},
|
||||
workflow: {
|
||||
components: '动作组件',
|
||||
clickToAdd: '点击添加',
|
||||
|
||||
@@ -693,6 +693,35 @@ export default {
|
||||
subtitle: '定時服務',
|
||||
},
|
||||
},
|
||||
agentAssistant: {
|
||||
title: '智能助手',
|
||||
assistant: '助手',
|
||||
ready: '隨時待命',
|
||||
thinking: '思考中',
|
||||
newChat: '新會話',
|
||||
history: '歷史會話',
|
||||
noHistory: '暫無歷史會話',
|
||||
deleteHistory: '刪除歷史會話',
|
||||
untitledSession: '未命名會話',
|
||||
emptyTitle: '今天想處理什麼?',
|
||||
emptySubtitle: '站點、訂閱、下載、整理任務,都可以直接問我。',
|
||||
placeholder: '詢問 MoviePilot...',
|
||||
stop: '停止生成',
|
||||
download: '下載',
|
||||
attachFile: '選擇圖片或文件',
|
||||
recordVoice: '錄製語音',
|
||||
stopRecording: '停止錄音({time})',
|
||||
attachmentMessage: '附件消息',
|
||||
removeAttachment: '移除附件',
|
||||
uploadFailed: '附件上傳失敗',
|
||||
recordUnsupported: '目前瀏覽器不支援錄音',
|
||||
recordPermissionDenied: '無法存取麥克風,請檢查瀏覽器權限',
|
||||
recordFailed: '錄音失敗,請重試',
|
||||
choiceSelected: '已選擇:{option}',
|
||||
choiceExpired: '該選擇已失效,請重新發起選擇',
|
||||
error: '智能助手響應失敗',
|
||||
noStream: '目前瀏覽器無法讀取串流響應',
|
||||
},
|
||||
workflow: {
|
||||
components: '動作組件',
|
||||
clickToAdd: '點擊添加',
|
||||
|
||||
@@ -922,6 +922,7 @@ html[data-theme="transparent"] .app-card-colorful,
|
||||
}
|
||||
|
||||
:root {
|
||||
--agent-assistant-fab-offset: 30rem;
|
||||
--theme-customizer-fab-offset: 420px;
|
||||
}
|
||||
|
||||
@@ -1040,6 +1041,14 @@ html[data-theme="transparent"] .app-card-colorful,
|
||||
html[data-theme-customizer-open='true'] .global-action-buttons {
|
||||
inset-inline-end: calc(var(--theme-customizer-fab-offset) + 2rem);
|
||||
}
|
||||
|
||||
html[data-agent-assistant-open='true'] .compact-fab-stack {
|
||||
inset-inline-end: calc(var(--agent-assistant-fab-offset) + max(1rem, calc(env(safe-area-inset-right) + 1rem)));
|
||||
}
|
||||
|
||||
html[data-agent-assistant-open='true'] .global-action-buttons {
|
||||
inset-inline-end: calc(var(--agent-assistant-fab-offset) + 2rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 601px) and (width <= 768px) {
|
||||
@@ -1048,6 +1057,12 @@ html[data-theme="transparent"] .app-card-colorful,
|
||||
var(--theme-customizer-fab-offset) + max(0.875rem, calc(env(safe-area-inset-right) + 0.875rem))
|
||||
);
|
||||
}
|
||||
|
||||
html[data-agent-assistant-open='true'] .compact-fab-stack {
|
||||
inset-inline-end: calc(
|
||||
var(--agent-assistant-fab-offset) + max(0.875rem, calc(env(safe-area-inset-right) + 0.875rem))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.apexcharts-title-text {
|
||||
|
||||
@@ -130,4 +130,34 @@ html[data-theme="transparent"] {
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity));
|
||||
}
|
||||
}
|
||||
|
||||
// 主题定制器面板
|
||||
.theme-customizer-panel-host {
|
||||
backdrop-filter: blur(var(--transparent-blur-heavy));
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy)) !important;
|
||||
border-inline-start: 1px solid rgba(var(--v-theme-on-surface), 0.08) !important;
|
||||
}
|
||||
|
||||
.theme-customizer-panel {
|
||||
backdrop-filter: blur(var(--transparent-blur));
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
// 智能助手面板
|
||||
.agent-assistant-panel {
|
||||
backdrop-filter: blur(var(--transparent-blur-heavy));
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy)) !important;
|
||||
border-inline-start: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
}
|
||||
|
||||
.agent-assistant-shell {
|
||||
--agent-assistant-panel-bg: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy));
|
||||
--agent-assistant-panel-blur: var(--transparent-blur);
|
||||
--agent-assistant-assistant-bg: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy));
|
||||
}
|
||||
|
||||
.agent-assistant-fab {
|
||||
backdrop-filter: blur(var(--transparent-blur));
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toastification'
|
||||
import api from '@/api'
|
||||
import { useGlobalSettingsStore } from '@/stores'
|
||||
import { DownloaderConf, MediaServerConf } from '@/api/types'
|
||||
import DownloaderCard from '@/components/cards/DownloaderCard.vue'
|
||||
import MediaServerCard from '@/components/cards/MediaServerCard.vue'
|
||||
@@ -17,6 +18,7 @@ const display = useDisplay()
|
||||
const theme = useTheme()
|
||||
|
||||
const isTransparentTheme = computed(() => theme.name.value === 'transparent')
|
||||
const globalSettingsStore = useGlobalSettingsStore()
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -698,6 +700,8 @@ async function saveBasicSettings() {
|
||||
savingBasic.value = true
|
||||
try {
|
||||
if (await saveSystemSetting(SystemSettings.value.Basic)) {
|
||||
// 更新全局设置store,使Web Agent图标实时生效
|
||||
globalSettingsStore.setData({ ...globalSettingsStore.getData, ...SystemSettings.value.Basic })
|
||||
$toast.success(t('setting.system.basicSaveSuccess'))
|
||||
}
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user