| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482 |
- <template>
- <Header :title="pageTitle" :logoUrl="logoUrl" @toggleSidebar="toggleSidebar" :sidebarEnabled="sidebarEnabled" :topBarEnabled="topbarEnabled" :navigation="navigation">
- <template #toolbar>
- <div id="banner" v-if="bannerMessage" :style="bannerCss">
- <p>{{ bannerMessage }}</p>
- </div>
- </template>
- <template #user-info>
- <div class="flex-row user-info" style="gap: .5em;">
- <span id="link-login" v-if="!isLoggedIn && showLoginLink"><router-link to="/login">{{ t('login-button') }}</router-link></span>
- <router-link v-else to="/user" class="user-link" v-if="isLoggedIn">
- <span id="username-text">{{ username }}</span>
- </router-link>
- <HugeiconsIcon :icon="UserCircle02Icon" width = "1.5em" height = "1.5em" v-if="isLoggedIn" />
- </div>
- </template>
- </Header>
- <div id="layout">
- <Navigation ref="navigation">
- <Sidebar ref="sidebar" id = "mainnav" v-if="sidebarEnabled && showNavigation"/>
- </Navigation>
- <div id="content" initial-martial-complete="{{ hasLoaded }}">
- <main title="Main content">
- <router-view :key="$route.fullPath" />
- </main>
- <footer title="footer" v-if="showFooter">
- <p>
- <img title="application icon" :src="logoUrl" alt="OliveTin logo" style="height: 1em;" class="logo" />
- OliveTin {{ currentVersion }}
- </p>
- <p>
- <span>
- <a href="https://docs.olivetin.app" target="_new">{{ t('docs') }}</a>
- </span>
- <span>
- <a href="https://github.com/OliveTin/OliveTin/issues/new/choose" target="_new">{{ t('raise-issue') }}</a>
- </span>
- <span>
- <a href="#" @click.prevent="openLanguageDialog">{{ currentLanguageName }}</a>
- </span>
- <span v-if="availableThemes.length > 1">
- <a href="#" @click.prevent="openThemeDialog">{{ currentThemeName }}</a>
- </span>
- <span>{{ t('connected') }}</span>
- </p>
- <p>
- <a id="available-version" href="http://olivetin.app" target="_blank" hidden>?</a>
- </p>
- </footer>
- </div>
- </div>
- <dialog ref="languageDialog" class="language-dialog" @click="handleLanguageDialogClick">
- <div class="dialog-content" @click.stop>
- <h2>{{ t('language-dialog.title') }}</h2>
- <select v-model="selectedLanguage" @change="changeLanguage" class="language-select">
- <option v-for="(name, code) in availableLanguages" :key="code" :value="code">
- {{ code === 'auto' ? name : `${name} (${code})` }}
- </option>
- </select>
- <p class="browser-languages">
- {{ t('language-dialog.browser-languages') }}:
- <span v-if="browserLanguages.length > 0">{{ browserLanguages.join(', ') }}</span>
- <span v-else>{{ t('language-dialog.not-available') }}</span>
- </p>
- <div class="dialog-buttons">
- <button @click="closeLanguageDialog">{{ t('language-dialog.close') }}</button>
- </div>
- </div>
- </dialog>
- <dialog ref="themeDialog" class="theme-dialog" @click="handleThemeDialogClick">
- <div class="dialog-content" @click.stop>
- <h2>{{ t('theme-dialog.title') }}</h2>
- <select v-model="selectedTheme" @change="changeTheme" class="language-select">
- <option value="">{{ t('theme-dialog.default') }}</option>
- <option v-for="theme in availableThemes" :key="theme" :value="theme">
- {{ theme }}
- </option>
- </select>
- <div class="dialog-buttons">
- <button @click="closeThemeDialog">{{ t('theme-dialog.close') }}</button>
- </div>
- </div>
- </dialog>
- </template>
- <script setup>
- import { ref, onMounted, computed } from 'vue';
- import { useRouter } from 'vue-router';
- import Sidebar from 'picocrank/vue/components/Sidebar.vue';
- import Navigation from 'picocrank/vue/components/Navigation.vue';
- import Header from 'picocrank/vue/components/Header.vue';
- import { HugeiconsIcon } from '@hugeicons/vue'
- import { Menu01Icon } from '@hugeicons/core-free-icons'
- import { UserCircle02Icon } from '@hugeicons/core-free-icons'
- import { DashboardSquare01Icon } from '@hugeicons/core-free-icons'
- import logoUrl from '../../OliveTinLogo.png';
- import { useI18n } from 'vue-i18n';
- import combinedTranslations from '../../../lang/combined_output.json';
- const { t, locale } = useI18n();
- const router = useRouter();
- const sidebar = ref(null);
- const navigation = ref(null);
- const username = ref('notset');
- const isLoggedIn = ref(false);
- const serverConnection = ref(true);
- const currentVersion = ref('?');
- const pageTitle = ref('OliveTin');
- const bannerMessage = ref('');
- const bannerCss = ref('');
- const hasLoaded = ref(false);
- const showFooter = ref(true)
- const showNavigation = ref(true)
- const showLogs = ref(true)
- const showDiagnostics = ref(true)
- const showLoginLink = ref(true)
- const sectionNavigationStyle = ref('sidebar')
- const languageDialog = ref(null)
- const browserLanguages = ref([])
- const initialLanguagePreference = typeof window !== 'undefined' ? localStorage.getItem('olivetin-language') : null
- const languagePreference = ref(initialLanguagePreference || 'auto')
- const selectedLanguage = ref(languagePreference.value)
- const themeDialog = ref(null)
- const availableThemes = ref([])
- const initialThemePreference = typeof window !== 'undefined' ? localStorage.getItem('olivetin-theme') : null
- const themePreference = ref(initialThemePreference || '')
- const selectedTheme = ref(themePreference.value)
- // Available languages with display names
- const availableLanguages = {
- 'auto': 'Browser Language',
- 'en': 'English',
- 'de-DE': 'Deutsch',
- 'es-ES': 'Español',
- 'it-IT': 'Italiano',
- 'zh-Hans-CN': '简体中文'
- }
- // Computed property to get current language display name
- const currentLanguageName = computed(() => {
- if (languagePreference.value === 'auto') {
- return availableLanguages['auto']
- }
- return availableLanguages[languagePreference.value] || languagePreference.value
- })
- // Computed property to get current theme display name
- const currentThemeName = computed(() => {
- if (!themePreference.value || themePreference.value === '') {
- return t('theme-dialog.default')
- }
- return themePreference.value
- })
- // Computed properties for navigation style
- const topbarEnabled = computed(() => {
- return sectionNavigationStyle.value === 'topbar'
- })
- const sidebarEnabled = computed(() => {
- return sectionNavigationStyle.value !== 'topbar' && showNavigation.value
- })
- function normalizeBrowserLanguage() {
- const available = Object.keys(combinedTranslations.messages || {})
- if (navigator.languages && navigator.languages.length > 0) {
- for (const candidate of navigator.languages) {
- const lowerCandidate = candidate.toLowerCase()
-
- // Try exact match (case-insensitive)
- const exact = available.find(locale => locale.toLowerCase() === lowerCandidate)
- if (exact) {
- return exact
- }
- // Try prefix match (e.g., "zh-CN" -> "zh-Hans-CN")
- const prefix = available.find(locale => locale.toLowerCase().startsWith(lowerCandidate.split('-')[0] + '-'))
- if (prefix) {
- return prefix
- }
- }
- }
- return 'en'
- }
- function toggleSidebar() {
- if (sidebar.value && showNavigation.value) {
- sidebar.value.toggle()
- }
- }
- function updateHeaderFromInit() {
- if (!window.initResponse) {
- return
- }
- username.value = window.initResponse.authenticatedUser
- isLoggedIn.value = window.initResponse.authenticatedUser !== '' && window.initResponse.authenticatedUser !== 'guest'
- currentVersion.value = window.initResponse.currentVersion
- pageTitle.value = window.initResponse.pageTitle || 'OliveTin'
- bannerMessage.value = window.initResponse.bannerMessage || ''
- bannerCss.value = window.initResponse.bannerCss || ''
- showFooter.value = window.initResponse.showFooter
- showNavigation.value = window.initResponse.showNavigation
- showLogs.value = window.initResponse.showLogList
- showDiagnostics.value = window.initResponse.showDiagnostics
- sectionNavigationStyle.value = window.initResponse.sectionNavigationStyle || 'sidebar'
- availableThemes.value = window.initResponse.availableThemes || []
- if (!window.initResponse.authLocalLogin && window.initResponse.oAuth2Providers.length === 0) {
- showLoginLink.value = false
- }
- applyStyleMods()
- loadCustomJsIfEnabled()
- renderNavigation()
- applyTheme()
- if (window.initResponse.loginRequired) {
- router.push('/login')
- return
- }
- }
- function renderNavigation() {
- if (!navigation.value) {
- return
- }
- const rootDashboards = window.initResponse?.rootDashboards || []
- if (typeof navigation.value.clear === 'function') {
- navigation.value.clear()
- }
- for (const rootDashboard of rootDashboards) {
- navigation.value.addNavigationLink({
- id: rootDashboard,
- name: rootDashboard,
- title: rootDashboard,
- path: rootDashboard === 'Actions' ? '/' : `/dashboards/${rootDashboard}`,
- icon: DashboardSquare01Icon,
- })
- }
- navigation.value.addSeparator()
- navigation.value.addRouterLink('Entities', t('nav.entities'))
- if (showLogs.value) {
- navigation.value.addRouterLink('Logs', t('nav.logs'))
- }
- if (showDiagnostics.value) {
- navigation.value.addRouterLink('Diagnostics', t('nav.diagnostics'))
- }
- }
- function openLanguageDialog() {
- selectedLanguage.value = languagePreference.value
-
- if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
- browserLanguages.value = navigator.languages
- } else {
- browserLanguages.value = []
- }
- if (languageDialog.value) {
- languageDialog.value.showModal()
- }
- }
- function closeLanguageDialog() {
- if (languageDialog.value) {
- languageDialog.value.close()
- }
- }
- function changeLanguage() {
- if (!window.i18n || !selectedLanguage.value) {
- return
- }
- if (selectedLanguage.value === 'auto') {
- localStorage.removeItem('olivetin-language')
- languagePreference.value = 'auto'
- window.i18n.locale.value = normalizeBrowserLanguage()
- } else {
- window.i18n.locale.value = selectedLanguage.value
- localStorage.setItem('olivetin-language', selectedLanguage.value)
- languagePreference.value = selectedLanguage.value
- }
- // Update navigation with new translations
- if (navigation.value) {
- renderNavigation()
- }
- closeLanguageDialog()
- }
- function handleLanguageDialogClick(event) {
- // Close dialog when clicking on the backdrop
- if (event.target === languageDialog.value) {
- closeLanguageDialog()
- }
- }
- function openThemeDialog() {
- selectedTheme.value = themePreference.value || ''
-
- if (themeDialog.value) {
- themeDialog.value.showModal()
- }
- }
- function closeThemeDialog() {
- if (themeDialog.value) {
- themeDialog.value.close()
- }
- }
- function changeTheme() {
- if (!selectedTheme.value || selectedTheme.value === '') {
- localStorage.removeItem('olivetin-theme')
- themePreference.value = ''
- } else {
- localStorage.setItem('olivetin-theme', selectedTheme.value)
- themePreference.value = selectedTheme.value
- }
- applyTheme()
- closeThemeDialog()
- }
- function applyTheme() {
- let themeStyle = document.getElementById('theme-style')
-
- if (!themeStyle) {
- themeStyle = document.createElement('style')
- themeStyle.id = 'theme-style'
- themeStyle.type = 'text/css'
- document.head.appendChild(themeStyle)
- }
- // Load theme into @layer theme so it takes precedence over @layer components
- if (themePreference.value && themePreference.value !== '') {
- themeStyle.textContent = `@import url('/custom-webui/themes/${themePreference.value}/theme.css') layer(theme);`
- } else {
- themeStyle.textContent = `@import url('/theme.css') layer(theme);`
- }
- }
- function loadCustomJsIfEnabled() {
- if (!window.initResponse?.enableCustomJs || document.getElementById('olivetin-custom-js')) {
- return
- }
- const script = document.createElement('script')
- script.src = '/custom-webui/custom.js'
- script.async = true
- script.id = 'olivetin-custom-js'
- document.head.appendChild(script)
- }
- function applyStyleMods() {
- if (!window.initResponse || !window.initResponse.styleMods) {
- return
- }
- for (const styleMod of window.initResponse.styleMods) {
- if (styleMod) {
- document.body.classList.add(styleMod)
- }
- }
- }
- function handleThemeDialogClick(event) {
- if (event.target === themeDialog.value) {
- closeThemeDialog()
- }
- }
- window.updateHeaderFromInit = updateHeaderFromInit
- onMounted(() => {
- serverConnection.value = true;
- updateHeaderFromInit()
-
- // Initialize selected language from stored preference
- selectedLanguage.value = languagePreference.value
-
- // Initialize selected theme from stored preference
- selectedTheme.value = themePreference.value || ''
- if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
- browserLanguages.value = navigator.languages
- }
- })
- </script>
- <style scoped>
- .user-info span {
- margin-left: 1em;
- }
- .user-link {
- text-decoration: none;
- color: inherit;
- }
- .user-link:hover {
- text-decoration: underline;
- }
- .language-dialog,
- .theme-dialog {
- border: 1px solid var(--border-color, #ccc);
- border-radius: 0.5rem;
- padding: 0;
- max-width: 400px;
- width: 90%;
- }
- .language-dialog::backdrop,
- .theme-dialog::backdrop {
- background-color: rgba(0, 0, 0, 0.5);
- }
- .dialog-content {
- padding: 1.5rem;
- }
- .dialog-content h2 {
- margin-top: 0;
- margin-bottom: 1rem;
- }
- .language-select {
- width: 100%;
- padding: 0.5rem;
- margin-bottom: 1rem;
- font-size: 1rem;
- border: 1px solid var(--border-color, #ccc);
- border-radius: 0.25rem;
- }
- .dialog-buttons {
- display: flex;
- justify-content: flex-end;
- gap: 0.5rem;
- }
- .dialog-buttons button {
- padding: 0.5rem 1rem;
- cursor: pointer;
- }
- .browser-languages {
- font-size: 0.875rem;
- color: var(--fg2, #555);
- margin-bottom: 1rem;
- }
- </style>
|