| <script setup lang="ts"> |
| import { ref, computed, watch, onMounted } from 'vue'; |
| import { useCustomizerStore } from '@/stores/customizer'; |
| import axios from 'axios'; |
| import Logo from '@/components/shared/Logo.vue'; |
| import { md5 } from 'js-md5'; |
| import { useAuthStore } from '@/stores/auth'; |
| import { useCommonStore } from '@/stores/common'; |
| import { MarkdownRender, enableKatex, enableMermaid } from 'markstream-vue'; |
| import 'markstream-vue/index.css'; |
| import 'katex/dist/katex.min.css'; |
| import 'highlight.js/styles/github.css'; |
| import { useI18n } from '@/i18n/composables'; |
| import { router } from '@/router'; |
| import { useRoute } from 'vue-router'; |
| import { useTheme } from 'vuetify'; |
| import StyledMenu from '@/components/shared/StyledMenu.vue'; |
| import { useLanguageSwitcher } from '@/i18n/composables'; |
| import type { Locale } from '@/i18n/types'; |
| import AboutPage from '@/views/AboutPage.vue'; |
| import { getDesktopRuntimeInfo } from '@/utils/desktopRuntime'; |
| |
| enableKatex(); |
| enableMermaid(); |
| |
| const customizer = useCustomizerStore(); |
| const theme = useTheme(); |
| const { t } = useI18n(); |
| const route = useRoute(); |
| const LAST_BOT_ROUTE_KEY = 'astrbot:last_bot_route'; |
| let dialog = ref(false); |
| let accountWarning = ref(false) |
| let updateStatusDialog = ref(false); |
| let aboutDialog = ref(false); |
| const username = localStorage.getItem('user'); |
| let password = ref(''); |
| let newPassword = ref(''); |
| let confirmPassword = ref(''); |
| let newUsername = ref(''); |
| let status = ref(''); |
| let updateStatus = ref('') |
| let releaseMessage = ref(''); |
| let hasNewVersion = ref(false); |
| let botCurrVersion = ref(''); |
| let dashboardHasNewVersion = ref(false); |
| let dashboardCurrentVersion = ref(''); |
| let version = ref(''); |
| let releases = ref([]); |
| let updatingDashboardLoading = ref(false); |
| let installLoading = ref(false); |
| const isDesktopReleaseMode = ref( |
| typeof window !== 'undefined' && !!window.astrbotDesktop?.isDesktop |
| ); |
| const desktopUpdateDialog = ref(false); |
| const desktopUpdateChecking = ref(false); |
| const desktopUpdateInstalling = ref(false); |
| const desktopUpdateHasNewVersion = ref(false); |
| const desktopUpdateCurrentVersion = ref('-'); |
| const desktopUpdateLatestVersion = ref('-'); |
| const desktopUpdateStatus = ref(''); |
| |
| const getAppUpdaterBridge = (): AstrBotAppUpdaterBridge | null => { |
| if (typeof window === 'undefined') { |
| return null; |
| } |
| const bridge = window.astrbotAppUpdater; |
| if ( |
| bridge && |
| typeof bridge.checkForAppUpdate === 'function' && |
| typeof bridge.installAppUpdate === 'function' |
| ) { |
| return bridge; |
| } |
| return null; |
| }; |
| |
| const getSelectedGitHubProxy = () => { |
| if (typeof window === "undefined" || !window.localStorage) return ""; |
| return localStorage.getItem("githubProxyRadioValue") === "1" |
| ? localStorage.getItem("selectedGitHubProxy") || "" |
| : ""; |
| }; |
| |
| |
| let releaseNotesDialog = ref(false); |
| let selectedReleaseNotes = ref(''); |
| let selectedReleaseTag = ref(''); |
| |
| const releasesHeader = computed(() => [ |
| { title: t('core.header.updateDialog.table.tag'), key: 'tag_name' }, |
| { title: t('core.header.updateDialog.table.publishDate'), key: 'published_at' }, |
| { title: t('core.header.updateDialog.table.content'), key: 'body' }, |
| { title: t('core.header.updateDialog.table.sourceUrl'), key: 'zipball_url' }, |
| { title: t('core.header.updateDialog.table.actions'), key: 'switch' } |
| ]); |
| |
| const formValid = ref(true); |
| const passwordRules = computed(() => [ |
| (v: string) => !!v || t('core.header.accountDialog.validation.passwordRequired'), |
| (v: string) => v.length >= 8 || t('core.header.accountDialog.validation.passwordMinLength') |
| ]); |
| const confirmPasswordRules = computed(() => [ |
| (v: string) => !newPassword.value || !!v || t('core.header.accountDialog.validation.passwordRequired'), |
| (v: string) => !newPassword.value || v === newPassword.value || t('core.header.accountDialog.validation.passwordMatch') |
| ]); |
| const usernameRules = computed(() => [ |
| (v: string) => !v || v.length >= 3 || t('core.header.accountDialog.validation.usernameMinLength') |
| ]); |
| |
| |
| const showPassword = ref(false); |
| const showNewPassword = ref(false); |
| const showConfirmPassword = ref(false); |
| |
| |
| const accountEditStatus = ref({ |
| loading: false, |
| success: false, |
| error: false, |
| message: '' |
| }); |
| |
| function cancelDesktopUpdate() { |
| if (desktopUpdateInstalling.value) { |
| return; |
| } |
| desktopUpdateDialog.value = false; |
| } |
| |
| async function openDesktopUpdateDialog() { |
| desktopUpdateDialog.value = true; |
| desktopUpdateChecking.value = true; |
| desktopUpdateInstalling.value = false; |
| desktopUpdateHasNewVersion.value = false; |
| desktopUpdateCurrentVersion.value = '-'; |
| desktopUpdateLatestVersion.value = '-'; |
| desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.checking'); |
| |
| const bridge = getAppUpdaterBridge(); |
| if (!bridge) { |
| desktopUpdateChecking.value = false; |
| desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.checkFailed'); |
| return; |
| } |
| |
| try { |
| const result = await bridge.checkForAppUpdate(); |
| if (!result?.ok) { |
| desktopUpdateCurrentVersion.value = result?.currentVersion || '-'; |
| desktopUpdateLatestVersion.value = |
| result?.latestVersion || result?.currentVersion || '-'; |
| desktopUpdateStatus.value = |
| result?.reason || t('core.header.updateDialog.desktopApp.checkFailed'); |
| return; |
| } |
| |
| desktopUpdateCurrentVersion.value = result.currentVersion || '-'; |
| desktopUpdateLatestVersion.value = |
| result.latestVersion || result.currentVersion || '-'; |
| desktopUpdateHasNewVersion.value = !!result.hasUpdate; |
| desktopUpdateStatus.value = result.hasUpdate |
| ? t('core.header.updateDialog.desktopApp.hasNewVersion') |
| : t('core.header.updateDialog.desktopApp.isLatest'); |
| } catch (error) { |
| console.error(error); |
| desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.checkFailed'); |
| } finally { |
| desktopUpdateChecking.value = false; |
| } |
| } |
| |
| async function confirmDesktopUpdate() { |
| if (!desktopUpdateHasNewVersion.value || desktopUpdateInstalling.value) { |
| return; |
| } |
| |
| const bridge = getAppUpdaterBridge(); |
| if (!bridge) { |
| desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.installFailed'); |
| return; |
| } |
| |
| desktopUpdateInstalling.value = true; |
| desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.installing'); |
| |
| try { |
| const result = await bridge.installAppUpdate(); |
| if (result?.ok) { |
| desktopUpdateDialog.value = false; |
| return; |
| } |
| desktopUpdateStatus.value = |
| result?.reason || t('core.header.updateDialog.desktopApp.installFailed'); |
| } catch (error) { |
| console.error(error); |
| desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.installFailed'); |
| } finally { |
| desktopUpdateInstalling.value = false; |
| } |
| } |
| |
| function handleUpdateClick() { |
| if (isDesktopReleaseMode.value) { |
| void openDesktopUpdateDialog(); |
| return; |
| } |
| checkUpdate(); |
| getReleases(); |
| updateStatusDialog.value = true; |
| } |
| |
| |
| const isPreRelease = (version: string) => { |
| const preReleaseKeywords = ['alpha', 'beta', 'rc', 'pre', 'preview', 'dev']; |
| const lowerVersion = version.toLowerCase(); |
| return preReleaseKeywords.some(keyword => lowerVersion.includes(keyword)); |
| }; |
| |
| |
| function accountEdit() { |
| accountEditStatus.value.loading = true; |
| accountEditStatus.value.error = false; |
| accountEditStatus.value.success = false; |
| |
| const passwordHash = password.value ? md5(password.value) : ''; |
| const newPasswordHash = newPassword.value ? md5(newPassword.value) : ''; |
| const confirmPasswordHash = confirmPassword.value ? md5(confirmPassword.value) : ''; |
| |
| axios.post('/api/auth/account/edit', { |
| password: passwordHash, |
| new_password: newPasswordHash, |
| confirm_password: confirmPasswordHash, |
| new_username: newUsername.value ? newUsername.value : username |
| }) |
| .then((res) => { |
| if (res.data.status == 'error') { |
| accountEditStatus.value.error = true; |
| accountEditStatus.value.message = res.data.message; |
| password.value = ''; |
| newPassword.value = ''; |
| confirmPassword.value = ''; |
| return; |
| } |
| accountEditStatus.value.success = true; |
| accountEditStatus.value.message = res.data.message; |
| setTimeout(() => { |
| dialog.value = !dialog.value; |
| const authStore = useAuthStore(); |
| authStore.logout(); |
| }, 2000); |
| }) |
| .catch((err) => { |
| console.log(err); |
| accountEditStatus.value.error = true; |
| accountEditStatus.value.message = typeof err === 'string' ? err : t('core.header.accountDialog.messages.updateFailed'); |
| password.value = ''; |
| newPassword.value = ''; |
| confirmPassword.value = ''; |
| }) |
| .finally(() => { |
| accountEditStatus.value.loading = false; |
| }); |
| } |
| |
| function getVersion() { |
| axios.get('/api/stat/version') |
| .then((res) => { |
| botCurrVersion.value = "v" + res.data.data.version; |
| dashboardCurrentVersion.value = res.data.data?.dashboard_version; |
| let change_pwd_hint = res.data.data?.change_pwd_hint; |
| if (change_pwd_hint) { |
| dialog.value = true; |
| accountWarning.value = true; |
| localStorage.setItem('change_pwd_hint', 'true'); |
| } else { |
| localStorage.removeItem('change_pwd_hint'); |
| } |
| }) |
| .catch((err) => { |
| console.log(err); |
| }); |
| } |
| |
| function checkUpdate() { |
| updateStatus.value = t('core.header.updateDialog.status.checking'); |
| axios.get('/api/update/check') |
| .then((res) => { |
| hasNewVersion.value = res.data.data.has_new_version; |
| |
| if (res.data.data.has_new_version) { |
| releaseMessage.value = res.data.message; |
| updateStatus.value = t('core.header.version.hasNewVersion'); |
| } else { |
| updateStatus.value = res.data.message; |
| } |
| dashboardHasNewVersion.value = isDesktopReleaseMode.value |
| ? false |
| : res.data.data.dashboard_has_new_version; |
| }) |
| .catch((err) => { |
| if (err.response && err.response.status == 401) { |
| console.log("401"); |
| const authStore = useAuthStore(); |
| authStore.logout(); |
| return; |
| } |
| console.log(err); |
| updateStatus.value = err |
| }); |
| } |
| |
| function getReleases() { |
| return axios.get('/api/update/releases') |
| .then((res) => { |
| releases.value = res.data.data.map((item: any) => { |
| item.published_at = new Date(item.published_at).toLocaleString(); |
| return item; |
| }) |
| }) |
| .catch((err) => { |
| console.log(err); |
| }); |
| } |
| |
| |
| |
| function switchVersion(version: string) { |
| updateStatus.value = t('core.header.updateDialog.status.switching'); |
| installLoading.value = true; |
| axios.post('/api/update/do', { |
| version: version, |
| proxy: getSelectedGitHubProxy() |
| }) |
| .then((res) => { |
| updateStatus.value = res.data.message; |
| if (res.data.status == 'ok') { |
| setTimeout(() => { |
| window.location.reload(); |
| }, 1000); |
| } |
| }) |
| .catch((err) => { |
| console.log(err); |
| updateStatus.value = err |
| }).finally(() => { |
| installLoading.value = false; |
| }); |
| } |
| |
| function updateDashboard() { |
| updatingDashboardLoading.value = true; |
| updateStatus.value = t('core.header.updateDialog.status.updating'); |
| axios.post('/api/update/dashboard') |
| .then((res) => { |
| updateStatus.value = res.data.message; |
| if (res.data.status == 'ok') { |
| setTimeout(() => { |
| window.location.reload(); |
| }, 1000); |
| } |
| }) |
| .catch((err) => { |
| console.log(err); |
| updateStatus.value = err |
| }).finally(() => { |
| updatingDashboardLoading.value = false; |
| }); |
| } |
| |
| function toggleDarkMode() { |
| const newTheme = customizer.uiTheme === 'PurpleThemeDark' ? 'PurpleTheme' : 'PurpleThemeDark'; |
| customizer.SET_UI_THEME(newTheme); |
| theme.global.name.value = newTheme; |
| } |
| |
| function openReleaseNotesDialog(body: string, tag: string) { |
| selectedReleaseNotes.value = body; |
| selectedReleaseTag.value = tag; |
| releaseNotesDialog.value = true; |
| } |
| |
| function handleLogoClick() { |
| if (customizer.viewMode === 'chat') { |
| aboutDialog.value = true; |
| } else { |
| router.push('/about'); |
| } |
| } |
| |
| getVersion(); |
| checkUpdate(); |
| |
| const commonStore = useCommonStore(); |
| commonStore.createEventSource(); |
| commonStore.getStartTime(); |
| |
| |
| const viewMode = computed({ |
| get: () => customizer.viewMode, |
| set: (value: 'bot' | 'chat') => { |
| customizer.SET_VIEW_MODE(value); |
| } |
| }); |
| |
| |
| |
| |
| watch(() => route.fullPath, (newPath) => { |
| if (customizer.viewMode === 'bot' && typeof window !== 'undefined') { |
| try { |
| localStorage.setItem(LAST_BOT_ROUTE_KEY, newPath); |
| } catch (e) { |
| console.error('Failed to save last bot route to localStorage:', e); |
| } |
| } |
| }); |
| |
| |
| watch(() => customizer.viewMode, (newMode, oldMode) => { |
| if (newMode === 'bot' && oldMode === 'chat' && typeof window !== 'undefined') { |
| // 從 chat 切換回 bot,跳轉到最後一次的 bot 路由 |
| let lastBotRoute = '/'; |
| try { |
| lastBotRoute = localStorage.getItem(LAST_BOT_ROUTE_KEY) || '/'; |
| } catch (e) { |
| console.error('Failed to read last bot route from localStorage:', e); |
| } |
| router.push(lastBotRoute); |
| } |
| }); |
| |
| |
| const isChristmas = computed(() => { |
| const today = new Date(); |
| const month = today.getMonth() + 1; // getMonth() 返回 0-11 |
| const day = today.getDate(); |
| return month === 12 && day === 25; |
| }); |
| |
| |
| const { languageOptions, currentLanguage, switchLanguage, locale } = useLanguageSwitcher(); |
| const languages = computed(() => |
| languageOptions.value.map(lang => ({ |
| code: lang.value, |
| name: lang.label, |
| flag: lang.flag |
| })) |
| ); |
| const currentLocale = computed(() => locale.value); |
| const changeLanguage = async (langCode: string) => { |
| await switchLanguage(langCode as Locale); |
| }; |
| |
| onMounted(async () => { |
| const runtimeInfo = await getDesktopRuntimeInfo(); |
| isDesktopReleaseMode.value = runtimeInfo.isDesktopRuntime; |
| if (isDesktopReleaseMode.value) { |
| dashboardHasNewVersion.value = false; |
| } |
| }); |
| |
| </script> |
| |
| <template> |
| <v-app-bar elevation="0" height="50" class="top-header"> |
| |
| |
| <v-btn v-if="customizer.viewMode === 'bot' && useCustomizerStore().uiTheme === 'PurpleTheme'" style="margin-left: 16px;" |
| class="hidden-md-and-down" icon rounded="sm" variant="flat" |
| @click.stop="customizer.SET_MINI_SIDEBAR(!customizer.mini_sidebar)"> |
| <v-icon>mdi-menu</v-icon> |
| </v-btn> |
| <v-btn v-else-if="customizer.viewMode === 'bot'" |
| style="margin-left: 22px;" |
| class="hidden-md-and-down" icon rounded="sm" variant="flat" |
| @click.stop="customizer.SET_MINI_SIDEBAR(!customizer.mini_sidebar)"> |
| <v-icon>mdi-menu</v-icon> |
| </v-btn> |
| |
| <v-btn v-if="customizer.viewMode === 'bot' && useCustomizerStore().uiTheme === 'PurpleTheme'" class="hidden-lg-and-up ms-3" |
| icon rounded="sm" variant="flat" @click.stop="customizer.SET_SIDEBAR_DRAWER"> |
| <v-icon>mdi-menu</v-icon> |
| </v-btn> |
| <v-btn v-else-if="customizer.viewMode === 'bot'" class="hidden-lg-and-up ms-3" icon rounded="sm" variant="flat" |
| @click.stop="customizer.SET_SIDEBAR_DRAWER"> |
| <v-icon>mdi-menu</v-icon> |
| </v-btn> |
| |
| |
| <v-btn v-if="customizer.viewMode === 'chat'" class="hidden-lg-and-up ms-1" icon rounded="sm" variant="flat" |
| @click.stop="customizer.TOGGLE_CHAT_SIDEBAR()"> |
| <v-icon>mdi-menu</v-icon> |
| </v-btn> |
| |
| <div class="logo-container" :class="{ 'mobile-logo': $vuetify.display.xs, 'chat-mode-logo': customizer.viewMode === 'chat' }" @click="handleLogoClick"> |
| <span class="logo-text Outfit">Astr<span class="logo-text bot-text-wrapper">Bot |
| <img v-if="isChristmas" src="@/assets/images/xmas-hat.png" alt="Christmas hat" class="xmas-hat" /> |
| </span></span> |
| <span class="logo-text logo-text-light Outfit" style="color: grey;" v-if="customizer.viewMode === 'chat'">ChatUI</span> |
| <span class="version-text hidden-xs">{{ botCurrVersion }}</span> |
| </div> |
| |
| <v-spacer /> |
| |
| |
| <div class="mr-4 hidden-xs"> |
| <small v-if="hasNewVersion"> |
| {{ t('core.header.version.hasNewVersion') }} |
| </small> |
| <small v-else-if="dashboardHasNewVersion && !isDesktopReleaseMode"> |
| {{ t('core.header.version.dashboardHasNewVersion') }} |
| </small> |
| </div> |
| |
| |
| <v-btn-toggle |
| v-model="viewMode" |
| mandatory |
| variant="outlined" |
| density="compact" |
| class="mr-4 hidden-xs" |
| color="primary" |
| > |
| <v-btn value="bot" size="small"> |
| <v-icon start>mdi-robot</v-icon> |
| Bot |
| </v-btn> |
| <v-btn value="chat" size="small"> |
| <v-icon start>mdi-chat</v-icon> |
| Chat |
| </v-btn> |
| </v-btn-toggle> |
| |
| |
| |
| <StyledMenu offset="12" location="bottom end"> |
| <template v-slot:activator="{ props: activatorProps }"> |
| <v-btn |
| v-bind="activatorProps" |
| size="small" |
| class="action-btn mr-4" |
| color="var(--v-theme-surface)" |
| variant="flat" |
| rounded="sm" |
| icon |
| > |
| <v-icon>mdi-dots-vertical</v-icon> |
| </v-btn> |
| </template> |
| |
| |
| <template v-if="$vuetify.display.xs"> |
| <div class="mobile-mode-toggle-wrapper"> |
| <v-btn-toggle |
| v-model="viewMode" |
| mandatory |
| variant="outlined" |
| density="compact" |
| color="primary" |
| class="mobile-mode-toggle" |
| > |
| <v-btn value="bot" size="small"> |
| <v-icon start>mdi-robot</v-icon> |
| Bot |
| </v-btn> |
| <v-btn value="chat" size="small"> |
| <v-icon start>mdi-chat</v-icon> |
| Chat |
| </v-btn> |
| </v-btn-toggle> |
| </div> |
| <v-divider class="my-1" /> |
| </template> |
| |
| |
| <v-list-item |
| v-for="lang in languages" |
| :key="lang.code" |
| :value="lang.code" |
| @click="changeLanguage(lang.code)" |
| :class="{ 'styled-menu-item-active': currentLocale === lang.code }" |
| class="styled-menu-item" |
| rounded="md" |
| > |
| <template v-slot:prepend> |
| <span class="language-flag">{{ lang.flag }}</span> |
| </template> |
| <v-list-item-title>{{ lang.name }}</v-list-item-title> |
| </v-list-item> |
| |
| |
| <v-list-item |
| @click="toggleDarkMode()" |
| class="styled-menu-item" |
| rounded="md" |
| > |
| <template v-slot:prepend> |
| <v-icon> |
| {{ useCustomizerStore().uiTheme === 'PurpleThemeDark' ? 'mdi-weather-night' : 'mdi-white-balance-sunny' }} |
| </v-icon> |
| </template> |
| <v-list-item-title> |
| {{ useCustomizerStore().uiTheme === 'PurpleThemeDark' ? t('core.header.buttons.theme.light') : t('core.header.buttons.theme.dark') }} |
| </v-list-item-title> |
| </v-list-item> |
| |
| |
| <v-list-item |
| @click="handleUpdateClick" |
| class="styled-menu-item" |
| rounded="md" |
| > |
| <template v-slot:prepend> |
| <v-icon>mdi-arrow-up-circle</v-icon> |
| </template> |
| <v-list-item-title>{{ t('core.header.updateDialog.title') }}</v-list-item-title> |
| <template v-slot:append v-if="hasNewVersion || (dashboardHasNewVersion && !isDesktopReleaseMode)"> |
| <v-chip size="x-small" color="primary" variant="tonal" class="ml-2">!</v-chip> |
| </template> |
| </v-list-item> |
| |
| |
| <v-list-item |
| @click="dialog = true" |
| class="styled-menu-item" |
| rounded="md" |
| > |
| <template v-slot:prepend> |
| <v-icon>mdi-account</v-icon> |
| </template> |
| <v-list-item-title>{{ t('core.header.accountDialog.title') }}</v-list-item-title> |
| </v-list-item> |
| </StyledMenu> |
| |
| |
| <v-dialog v-model="updateStatusDialog" :width="$vuetify.display.smAndDown ? '100%' : '1200'" |
| :fullscreen="$vuetify.display.xs"> |
| <v-card> |
| <v-card-title class="mobile-card-title"> |
| <span class="text-h5">{{ t('core.header.updateDialog.title') }}</span> |
| <v-btn v-if="$vuetify.display.xs" icon @click="updateStatusDialog = false"> |
| <v-icon>mdi-close</v-icon> |
| </v-btn> |
| </v-card-title> |
| <v-card-text> |
| <v-container> |
| <v-progress-linear v-show="installLoading" class="mb-4" indeterminate color="primary"></v-progress-linear> |
| |
| <div> |
| <h1 style="display:inline-block;">{{ botCurrVersion }}</h1> |
| <small style="margin-left: 4px;">{{ updateStatus }}</small> |
| </div> |
| |
| <div v-if="releaseMessage" |
| style="background-color: #646cff24; padding: 16px; border-radius: 10px; font-size: 14px; max-height: 400px; overflow-y: auto;"> |
| <MarkdownRender :content="releaseMessage" :typewriter="false" class="markdown-content" /> |
| </div> |
| |
| <div class="mb-4 mt-4"> |
| <small>{{ t('core.header.updateDialog.tip') }} |
| {{ t('core.header.updateDialog.tipContinue') }}</small> |
| </div> |
| |
| |
| <div> |
| <div class="mb-4"> |
| <small>{{ t('core.header.updateDialog.dockerTip') }} <a |
| href="https://containrrr.dev/watchtower/usage-overview/">{{ |
| t('core.header.updateDialog.dockerTipLink') |
| }}</a> {{ t('core.header.updateDialog.dockerTipContinue') }}</small> |
| </div> |
| |
| <v-alert v-if="releases.some((item: any) => isPreRelease(item['tag_name']))" type="warning" variant="tonal" |
| border="start"> |
| <template v-slot:prepend> |
| <v-icon>mdi-alert-circle-outline</v-icon> |
| </template> |
| <div class="text-body-2"> |
| <strong>{{ t('core.header.updateDialog.preReleaseWarning.title') }}</strong> |
| <br> |
| {{ t('core.header.updateDialog.preReleaseWarning.description') }} |
| <a href="https://github.com/AstrBotDevs/AstrBot/issues" target="_blank" class="text-decoration-none"> |
| {{ t('core.header.updateDialog.preReleaseWarning.issueLink') }} |
| </a> |
| </div> |
| </v-alert> |
| |
| <v-data-table :headers="releasesHeader" :items="releases" item-key="name" :items-per-page="8"> |
| <template v-slot:item.tag_name="{ item }: { item: any }"> |
| <div class="d-flex align-center"> |
| <span>{{ item.tag_name }}</span> |
| <v-chip v-if="isPreRelease(item.tag_name)" size="x-small" color="warning" variant="tonal" |
| class="ml-2"> |
| {{ t('core.header.updateDialog.preRelease') }} |
| </v-chip> |
| </div> |
| </template> |
| <template v-slot:item.body="{ item }: { item: { body: string; tag_name: string } }"> |
| <v-btn @click="openReleaseNotesDialog(item.body, item.tag_name)" rounded="xl" variant="tonal" |
| color="primary" size="x-small">{{ |
| t('core.header.updateDialog.table.view') }}</v-btn> |
| </template> |
| <template v-slot:item.switch="{ item }: { item: { tag_name: string } }"> |
| <v-btn @click="switchVersion(item.tag_name)" rounded="xl" variant="plain" color="primary"> |
| {{ t('core.header.updateDialog.table.switch') }} |
| </v-btn> |
| </template> |
| </v-data-table> |
| </div> |
| |
| <v-divider class="mt-4 mb-4"></v-divider> |
| <div style="margin-top: 16px;"> |
| <h3 class="mb-4">{{ t('core.header.updateDialog.dashboardUpdate.title') }}</h3> |
| <div class="mb-4"> |
| <small>{{ t('core.header.updateDialog.dashboardUpdate.currentVersion') }} {{ dashboardCurrentVersion |
| }}</small> |
| <br> |
| |
| </div> |
| |
| <div class="mb-4"> |
| <p v-if="dashboardHasNewVersion"> |
| {{ t('core.header.updateDialog.dashboardUpdate.hasNewVersion') }} |
| </p> |
| <p v-else="dashboardHasNewVersion"> |
| {{ t('core.header.updateDialog.dashboardUpdate.isLatest') }} |
| </p> |
| </div> |
| |
| <v-btn color="primary" style="border-radius: 10px;" @click="updateDashboard()" |
| :disabled="!dashboardHasNewVersion" :loading="updatingDashboardLoading"> |
| {{ t('core.header.updateDialog.dashboardUpdate.downloadAndUpdate') }} |
| </v-btn> |
| </div> |
| </v-container> |
| </v-card-text> |
| <v-card-actions> |
| <v-spacer></v-spacer> |
| <v-btn color="blue-darken-1" variant="text" @click="updateStatusDialog = false"> |
| {{ t('core.common.close') }} |
| </v-btn> |
| </v-card-actions> |
| </v-card> |
| </v-dialog> |
| |
| |
| <v-dialog v-model="releaseNotesDialog" max-width="800"> |
| <v-card> |
| <v-card-title class="text-h5"> |
| {{ t('core.header.updateDialog.releaseNotes.title') }}: {{ selectedReleaseTag }} |
| </v-card-title> |
| <v-card-text |
| style="font-size: 14px; max-height: 400px; overflow-y: auto;"> |
| <MarkdownRender :content="selectedReleaseNotes" :typewriter="false" class="markdown-content" /> |
| </v-card-text> |
| <v-card-actions> |
| <v-spacer></v-spacer> |
| <v-btn color="blue-darken-1" variant="text" @click="releaseNotesDialog = false"> |
| {{ t('core.common.close') }} |
| </v-btn> |
| </v-card-actions> |
| </v-card> |
| </v-dialog> |
| |
| <v-dialog v-model="desktopUpdateDialog" max-width="460"> |
| <v-card> |
| <v-card-title class="text-h3 pa-4 pl-6 pb-0"> |
| {{ t('core.header.updateDialog.desktopApp.title') }} |
| </v-card-title> |
| <v-card-text> |
| <div class="mb-3"> |
| {{ t('core.header.updateDialog.desktopApp.message') }} |
| </div> |
| <v-alert type="info" variant="tonal" density="compact"> |
| <div> |
| {{ t('core.header.updateDialog.desktopApp.currentVersion') }} |
| <strong>{{ desktopUpdateCurrentVersion }}</strong> |
| </div> |
| <div> |
| {{ t('core.header.updateDialog.desktopApp.latestVersion') }} |
| <strong v-if="!desktopUpdateChecking">{{ desktopUpdateLatestVersion }}</strong> |
| <v-progress-circular v-else indeterminate size="16" width="2" class="ml-1" /> |
| </div> |
| </v-alert> |
| <div class="text-caption mt-3"> |
| {{ desktopUpdateStatus }} |
| </div> |
| </v-card-text> |
| <v-card-actions> |
| <v-spacer></v-spacer> |
| <v-btn color="grey" variant="text" @click="cancelDesktopUpdate" :disabled="desktopUpdateInstalling"> |
| {{ t('core.common.dialog.cancelButton') }} |
| </v-btn> |
| <v-btn color="primary" variant="flat" @click="confirmDesktopUpdate" |
| :loading="desktopUpdateInstalling" |
| :disabled="desktopUpdateChecking || desktopUpdateInstalling || !desktopUpdateHasNewVersion"> |
| {{ t('core.common.dialog.confirmButton') }} |
| </v-btn> |
| </v-card-actions> |
| </v-card> |
| </v-dialog> |
| |
| |
| <v-dialog v-model="dialog" persistent :max-width="$vuetify.display.xs ? '90%' : '500'"> |
| <v-card class="account-dialog"> |
| <v-card-text class="py-6"> |
| <div class="d-flex flex-column align-center mb-6"> |
| <logo :title="t('core.header.logoTitle')" :subtitle="t('core.header.accountDialog.title')"></logo> |
| </div> |
| <v-alert v-if="accountWarning" type="warning" variant="tonal" border="start" class="mb-4"> |
| <strong>{{ t('core.header.accountDialog.securityWarning') }}</strong> |
| </v-alert> |
| |
| <v-alert v-if="accountEditStatus.success" type="success" variant="tonal" border="start" class="mb-4"> |
| {{ accountEditStatus.message }} |
| </v-alert> |
| |
| <v-alert v-if="accountEditStatus.error" type="error" variant="tonal" border="start" class="mb-4"> |
| {{ accountEditStatus.message }} |
| </v-alert> |
| |
| <v-form v-model="formValid" @submit.prevent="accountEdit"> |
| <v-text-field v-model="password" :append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'" |
| :type="showPassword ? 'text' : 'password'" :label="t('core.header.accountDialog.form.currentPassword')" |
| variant="outlined" required clearable @click:append-inner="showPassword = !showPassword" |
| prepend-inner-icon="mdi-lock-outline" hide-details="auto" class="mb-4"></v-text-field> |
| |
| <v-text-field v-model="newPassword" :append-inner-icon="showNewPassword ? 'mdi-eye-off' : 'mdi-eye'" |
| :type="showNewPassword ? 'text' : 'password'" :rules="passwordRules" |
| :label="t('core.header.accountDialog.form.newPassword')" variant="outlined" clearable |
| @click:append-inner="showNewPassword = !showNewPassword" prepend-inner-icon="mdi-lock-plus-outline" |
| :hint="t('core.header.accountDialog.form.passwordHint')" persistent-hint class="mb-4"></v-text-field> |
| |
| <v-text-field v-model="confirmPassword" :append-inner-icon="showConfirmPassword ? 'mdi-eye-off' : 'mdi-eye'" |
| :type="showConfirmPassword ? 'text' : 'password'" :rules="confirmPasswordRules" |
| :label="t('core.header.accountDialog.form.confirmPassword')" variant="outlined" clearable |
| @click:append-inner="showConfirmPassword = !showConfirmPassword" prepend-inner-icon="mdi-lock-check-outline" |
| :hint="t('core.header.accountDialog.form.confirmPasswordHint')" persistent-hint class="mb-4"></v-text-field> |
| |
| <v-text-field v-model="newUsername" :rules="usernameRules" |
| :label="t('core.header.accountDialog.form.newUsername')" variant="outlined" clearable |
| prepend-inner-icon="mdi-account-edit-outline" :hint="t('core.header.accountDialog.form.usernameHint')" |
| persistent-hint class="mb-3"></v-text-field> |
| </v-form> |
| |
| <div class="text-caption text-medium-emphasis mt-2"> |
| {{ t('core.header.accountDialog.form.defaultCredentials') }} |
| </div> |
| </v-card-text> |
| |
| <v-divider></v-divider> |
| |
| <v-card-actions class="pa-4"> |
| <v-spacer></v-spacer> |
| <v-btn v-if="!accountWarning" variant="tonal" color="secondary" @click="dialog = false" |
| :disabled="accountEditStatus.loading"> |
| {{ t('core.header.accountDialog.actions.cancel') }} |
| </v-btn> |
| <v-btn color="primary" @click="accountEdit" :loading="accountEditStatus.loading" :disabled="!formValid" |
| prepend-icon="mdi-content-save"> |
| {{ t('core.header.accountDialog.actions.save') }} |
| </v-btn> |
| </v-card-actions> |
| </v-card> |
| </v-dialog> |
| |
| |
| <v-dialog v-model="aboutDialog" |
| width="600"> |
| <v-card> |
| <v-card-text style="overflow-y: auto;"> |
| <AboutPage /> |
| </v-card-text> |
| </v-card> |
| </v-dialog> |
| </v-app-bar> |
| </template> |
| |
| <style> |
| .markdown-content h1 { |
| font-size: 1.3em; |
| } |
| |
| .markdown-content ol { |
| padding-left: 24px; |
| /* Adds indentation to ordered lists */ |
| margin-top: 8px; |
| margin-bottom: 8px; |
| } |
| |
| .markdown-content ul { |
| padding-left: 24px; |
| /* Adds indentation to unordered lists */ |
| margin-top: 8px; |
| margin-bottom: 8px; |
| } |
| |
| .account-dialog .v-card-text { |
| padding-top: 24px; |
| padding-bottom: 24px; |
| } |
| |
| .account-dialog .v-alert { |
| margin-bottom: 20px; |
| } |
| |
| .account-dialog .v-btn { |
| text-transform: none; |
| font-weight: 500; |
| border-radius: 8px; |
| } |
| |
| .account-dialog .v-avatar { |
| transition: transform 0.3s ease; |
| } |
| |
| .account-dialog .v-avatar:hover { |
| transform: scale(1.05); |
| } |
| |
| |
| .logo-container { |
| margin-left: 10px; |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| cursor: pointer; |
| } |
| |
| .mobile-logo { |
| margin-left: 8px; |
| gap: 4px; |
| } |
| |
| .chat-mode-logo { |
| margin-left: 22px; |
| } |
| |
| .mobile-logo.chat-mode-logo { |
| margin-left: 4px; |
| } |
| |
| .logo-text { |
| font-size: 24px; |
| font-weight: 1000; |
| } |
| |
| .logo-text-light { |
| font-weight: normal; |
| } |
| |
| .bot-text-wrapper { |
| position: relative; |
| display: inline-block; |
| } |
| |
| .xmas-hat { |
| position: absolute; |
| top: -3px; |
| right: -14px; |
| width: 24px; |
| height: 24px; |
| z-index: 1; |
| } |
| |
| .version-text { |
| font-size: 12px; |
| color: gray; |
| margin-left: 4px; |
| } |
| |
| .action-btn { |
| margin-right: 6px; |
| } |
| |
| .language-flag { |
| font-size: 16px; |
| margin-right: 8px; |
| } |
| |
| .mobile-mode-toggle-wrapper { |
| display: flex; |
| justify-content: center; |
| padding: 8px 12px 4px; |
| } |
| |
| .mobile-mode-toggle { |
| width: 100%; |
| } |
| |
| .mobile-mode-toggle .v-btn { |
| flex: 1; |
| } |
| |
| |
| .mobile-card-title { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| |
| |
| @media (max-width: 600px) { |
| .logo-text { |
| font-size: 20px; |
| } |
| |
| .action-btn { |
| margin-right: 4px; |
| min-width: 32px !important; |
| width: 32px; |
| } |
| |
| .v-card-title { |
| padding: 12px 16px; |
| } |
| |
| .v-card-text { |
| padding: 16px; |
| } |
| |
| .v-tabs .v-tab { |
| padding: 0 10px; |
| font-size: 0.9rem; |
| } |
| |
| |
| .v-btn-toggle { |
| margin-right: 8px; |
| } |
| |
| .v-btn-toggle .v-btn { |
| font-size: 0.75rem; |
| padding: 0 8px; |
| } |
| |
| .v-btn-toggle .v-icon { |
| font-size: 16px; |
| } |
| } |
| </style> |
| |