feat: add theme selector support, load themes in a layer.
This commit is contained in:
parent
dceb509ab9
commit
87913a6ff3
|
|
@ -124,7 +124,10 @@ actions:
|
|||
#
|
||||
# Docs: https://docs.olivetin.app/reference/reference_themes_for_users.html
|
||||
- title: Get OliveTin Theme
|
||||
shell: olivetin-get-theme {{ themeGitRepo }} {{ themeFolderName }}
|
||||
exec:
|
||||
- "olivetin-get-theme"
|
||||
- "{{ themeGitRepo }}"
|
||||
- "{{ themeFolderName }}"
|
||||
icon: theme
|
||||
arguments:
|
||||
- name: themeGitRepo
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
<title>OliveTin</title>
|
||||
|
||||
<link rel = "stylesheet" type = "text/css" href = "/theme.css" />
|
||||
<link rel = "stylesheet" href = "node_modules/@xterm/xterm/css/xterm.css" />
|
||||
|
||||
<link rel = "shortcut icon" type = "image/png" href = "OliveTinLogo.png" />
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"standard": "^17.1.2",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "^7.3.1",
|
||||
"vue": "^3.5.26",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1451,6 +1451,13 @@ export declare type InitResponse = Message<"olivetin.api.v1.InitResponse"> & {
|
|||
* @generated from field: bool login_required = 23;
|
||||
*/
|
||||
loginRequired: boolean;
|
||||
|
||||
/**
|
||||
* List of available theme names
|
||||
*
|
||||
* @generated from field: repeated string available_themes = 24;
|
||||
*/
|
||||
availableThemes: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -318,109 +318,111 @@ watch(
|
|||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.action-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
<style>
|
||||
|
||||
.action-button button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
padding: 0.5em;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 0 .6em #aaa;
|
||||
font-size: .85em;
|
||||
border-radius: .7em;
|
||||
}
|
||||
@layer components {
|
||||
.action-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.action-button button:hover:not(:disabled) {
|
||||
background: #f5f5f5;
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
.action-button button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-button button .icon {
|
||||
font-size: 3em;
|
||||
flex-grow: 1;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.action-button button .title {
|
||||
font-weight: 500;
|
||||
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
.action-button button .rate-limit-message {
|
||||
font-size: 0.75em;
|
||||
color: #856404;
|
||||
padding: 0.2em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* Animation classes */
|
||||
.action-button button.action-timeout {
|
||||
background: #fff3cd;
|
||||
border-color: #ffeaa7;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.action-button button.action-blocked {
|
||||
background: #f8d7da !important;
|
||||
border-color: #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.action-button button.action-nonzero-exit {
|
||||
background: #f8d7da !important;
|
||||
border-color: #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.action-button button.action-success {
|
||||
background: #d4edda !important;
|
||||
border-color: #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.action-button-footer {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.navigate-on-start-container {
|
||||
position: relative;
|
||||
margin-left: auto;
|
||||
height: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.action-button button {
|
||||
background: #111;
|
||||
border-color: #000;
|
||||
box-shadow: 0 0 6px #000;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
padding: 0.5em;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 0 .6em #aaa;
|
||||
font-size: .85em;
|
||||
border-radius: .7em;
|
||||
}
|
||||
|
||||
.action-button button:hover:not(:disabled) {
|
||||
background: #222;
|
||||
border-color: #000;
|
||||
box-shadow: 0 0 6px #444;
|
||||
color: #fff;
|
||||
background: #f5f5f5;
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
.action-button button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-button button .icon {
|
||||
font-size: 3em;
|
||||
flex-grow: 1;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.action-button button .title {
|
||||
font-weight: 500;
|
||||
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
.action-button button .rate-limit-message {
|
||||
font-size: 0.75em;
|
||||
color: #856404;
|
||||
padding: 0.2em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* Animation classes */
|
||||
.action-button button.action-timeout {
|
||||
background: #fff3cd;
|
||||
border-color: #ffeaa7;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.action-button button.action-blocked {
|
||||
background: #f8d7da !important;
|
||||
border-color: #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.action-button button.action-nonzero-exit {
|
||||
background: #f8d7da !important;
|
||||
border-color: #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.action-button button.action-success {
|
||||
background: #d4edda !important;
|
||||
border-color: #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.action-button-footer {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.navigate-on-start-container {
|
||||
position: relative;
|
||||
margin-left: auto;
|
||||
height: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.action-button button {
|
||||
background: #111;
|
||||
border-color: #000;
|
||||
box-shadow: 0 0 6px #000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-button button:hover:not(:disabled) {
|
||||
background: #222;
|
||||
border-color: #000;
|
||||
box-shadow: 0 0 6px #444;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@
|
|||
<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>
|
||||
|
|
@ -55,7 +59,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<dialog ref="languageDialog" class="language-dialog" @click="handleDialogClick">
|
||||
<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">
|
||||
|
|
@ -73,6 +77,21 @@
|
|||
</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>
|
||||
|
|
@ -117,6 +136,12 @@ const initialLanguagePreference = typeof window !== 'undefined' ? localStorage.g
|
|||
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',
|
||||
|
|
@ -136,6 +161,14 @@ const currentLanguageName = computed(() => {
|
|||
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'
|
||||
|
|
@ -191,12 +224,14 @@ function updateHeaderFromInit() {
|
|||
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
|
||||
}
|
||||
|
||||
renderNavigation()
|
||||
applyTheme()
|
||||
|
||||
if (window.initResponse.loginRequired) {
|
||||
router.push('/login')
|
||||
|
|
@ -280,13 +315,63 @@ function changeLanguage() {
|
|||
closeLanguageDialog()
|
||||
}
|
||||
|
||||
function handleDialogClick(event) {
|
||||
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)
|
||||
}
|
||||
|
||||
if (themePreference.value && themePreference.value !== '') {
|
||||
themeStyle.textContent = `@import url('/custom-webui/themes/${themePreference.value}/theme.css') layer(theme);`
|
||||
} else {
|
||||
themeStyle.textContent = ''
|
||||
}
|
||||
}
|
||||
|
||||
function handleThemeDialogClick(event) {
|
||||
if (event.target === themeDialog.value) {
|
||||
closeThemeDialog()
|
||||
}
|
||||
}
|
||||
|
||||
window.updateHeaderFromInit = updateHeaderFromInit
|
||||
|
||||
onMounted(() => {
|
||||
|
|
@ -296,6 +381,9 @@ onMounted(() => {
|
|||
// 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
|
||||
}
|
||||
|
|
@ -316,7 +404,8 @@ onMounted(() => {
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.language-dialog {
|
||||
.language-dialog,
|
||||
.theme-dialog {
|
||||
border: 1px solid var(--border-color, #ccc);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0;
|
||||
|
|
@ -324,7 +413,8 @@ onMounted(() => {
|
|||
width: 90%;
|
||||
}
|
||||
|
||||
.language-dialog::backdrop {
|
||||
.language-dialog::backdrop,
|
||||
.theme-dialog::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
@layer components, karma, theme;
|
||||
|
||||
header {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,15 @@ export default defineConfig({
|
|||
target: 'http://localhost:1337',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
'/theme.css': {
|
||||
target: 'http://localhost:1337',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
"/custom-webui": {
|
||||
target: "http://localhost:1337",
|
||||
changeOrigin: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -49,6 +49,9 @@
|
|||
"raise-issue": "Ein Problem melden auf GitHub",
|
||||
"return-to-index": "Zurück zur Startseite",
|
||||
"search-filter": "Filter aktuelle Seite",
|
||||
"theme-dialog.close": "Schließen",
|
||||
"theme-dialog.default": "Standard-Design",
|
||||
"theme-dialog.title": "Design auswählen",
|
||||
"welcome": "Willkommen bei OliveTin"
|
||||
},
|
||||
"en": {
|
||||
|
|
@ -99,6 +102,9 @@
|
|||
"raise-issue": "Raise an issue on GitHub",
|
||||
"return-to-index": "Return to index",
|
||||
"search-filter": "Filter current page",
|
||||
"theme-dialog.close": "Close",
|
||||
"theme-dialog.default": "Default Theme",
|
||||
"theme-dialog.title": "Select Theme",
|
||||
"welcome": "Welcome to OliveTin"
|
||||
},
|
||||
"es-ES": {
|
||||
|
|
@ -149,6 +155,9 @@
|
|||
"raise-issue": "Reportar un problema en GitHub",
|
||||
"return-to-index": "Volver a la página principal",
|
||||
"search-filter": "Filtrar página actual",
|
||||
"theme-dialog.close": "Cerrar",
|
||||
"theme-dialog.default": "Tema Predeterminado",
|
||||
"theme-dialog.title": "Seleccionar tema",
|
||||
"welcome": "Bienvenido a OliveTin"
|
||||
},
|
||||
"it-IT": {
|
||||
|
|
@ -199,6 +208,9 @@
|
|||
"raise-issue": "Segnala un problema su GitHub",
|
||||
"return-to-index": "Torna alla pagina principale",
|
||||
"search-filter": "Filtra la pagina corrente",
|
||||
"theme-dialog.close": "Chiudi",
|
||||
"theme-dialog.default": "Tema Predefinito",
|
||||
"theme-dialog.title": "Seleziona tema",
|
||||
"welcome": "Benvenuto in OliveTin"
|
||||
},
|
||||
"zh-Hans-CN": {
|
||||
|
|
@ -249,6 +261,9 @@
|
|||
"raise-issue": "在 GitHub 上报告问题",
|
||||
"return-to-index": "返回首页",
|
||||
"search-filter": "过滤当前页面",
|
||||
"theme-dialog.close": "关闭",
|
||||
"theme-dialog.default": "默认主题",
|
||||
"theme-dialog.title": "选择主题",
|
||||
"welcome": "欢迎使用 OliveTin"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,3 +48,6 @@ translations:
|
|||
language-dialog.browser-languages: Browser-Sprachen
|
||||
language-dialog.not-available: Nicht verfügbar
|
||||
language-dialog.close: Schließen
|
||||
theme-dialog.title: Design auswählen
|
||||
theme-dialog.default: Standard-Design
|
||||
theme-dialog.close: Schließen
|
||||
|
|
@ -48,3 +48,6 @@ translations:
|
|||
language-dialog.browser-languages: Browser languages
|
||||
language-dialog.not-available: Not available
|
||||
language-dialog.close: Close
|
||||
theme-dialog.title: Select Theme
|
||||
theme-dialog.default: Default Theme
|
||||
theme-dialog.close: Close
|
||||
|
|
@ -48,3 +48,6 @@ translations:
|
|||
language-dialog.browser-languages: Idiomas del navegador
|
||||
language-dialog.not-available: No disponible
|
||||
language-dialog.close: Cerrar
|
||||
theme-dialog.title: Seleccionar tema
|
||||
theme-dialog.default: Tema Predeterminado
|
||||
theme-dialog.close: Cerrar
|
||||
|
|
@ -48,3 +48,6 @@ translations:
|
|||
language-dialog.browser-languages: Lingue del browser
|
||||
language-dialog.not-available: Non disponibile
|
||||
language-dialog.close: Chiudi
|
||||
theme-dialog.title: Seleziona tema
|
||||
theme-dialog.default: Tema Predefinito
|
||||
theme-dialog.close: Chiudi
|
||||
|
|
@ -22,6 +22,9 @@ translations:
|
|||
language-dialog.browser-languages: 浏览器语言
|
||||
language-dialog.not-available: 不可用
|
||||
language-dialog.close: 关闭
|
||||
theme-dialog.title: 选择主题
|
||||
theme-dialog.default: 默认主题
|
||||
theme-dialog.close: 关闭
|
||||
logs.timed-out: 超时
|
||||
logs.blocked: 阻塞
|
||||
logs.exit-code: 退出代码
|
||||
|
|
|
|||
|
|
@ -332,6 +332,7 @@ message InitResponse {
|
|||
bool show_diagnostics = 21;
|
||||
bool show_log_list = 22;
|
||||
bool login_required = 23;
|
||||
repeated string available_themes = 24; // List of available theme names
|
||||
}
|
||||
|
||||
message AdditionalLink {
|
||||
|
|
|
|||
|
|
@ -3236,6 +3236,7 @@ type InitResponse struct {
|
|||
ShowDiagnostics bool `protobuf:"varint,21,opt,name=show_diagnostics,json=showDiagnostics,proto3" json:"show_diagnostics,omitempty"`
|
||||
ShowLogList bool `protobuf:"varint,22,opt,name=show_log_list,json=showLogList,proto3" json:"show_log_list,omitempty"`
|
||||
LoginRequired bool `protobuf:"varint,23,opt,name=login_required,json=loginRequired,proto3" json:"login_required,omitempty"`
|
||||
AvailableThemes []string `protobuf:"bytes,24,rep,name=available_themes,json=availableThemes,proto3" json:"available_themes,omitempty"` // List of available theme names
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
|
@ -3431,6 +3432,13 @@ func (x *InitResponse) GetLoginRequired() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (x *InitResponse) GetAvailableThemes() []string {
|
||||
if x != nil {
|
||||
return x.AvailableThemes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AdditionalLink struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
|
||||
|
|
@ -4088,7 +4096,7 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
|
|||
"\x16GetDiagnosticsResponse\x12 \n" +
|
||||
"\vSshFoundKey\x18\x01 \x01(\tR\vSshFoundKey\x12&\n" +
|
||||
"\x0eSshFoundConfig\x18\x02 \x01(\tR\x0eSshFoundConfig\"\r\n" +
|
||||
"\vInitRequest\"\xa2\b\n" +
|
||||
"\vInitRequest\"\xcd\b\n" +
|
||||
"\fInitResponse\x12\x1e\n" +
|
||||
"\n" +
|
||||
"showFooter\x18\x01 \x01(\bR\n" +
|
||||
|
|
@ -4116,7 +4124,8 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
|
|||
"banner_css\x18\x14 \x01(\tR\tbannerCss\x12)\n" +
|
||||
"\x10show_diagnostics\x18\x15 \x01(\bR\x0fshowDiagnostics\x12\"\n" +
|
||||
"\rshow_log_list\x18\x16 \x01(\bR\vshowLogList\x12%\n" +
|
||||
"\x0elogin_required\x18\x17 \x01(\bR\rloginRequired\"8\n" +
|
||||
"\x0elogin_required\x18\x17 \x01(\bR\rloginRequired\x12)\n" +
|
||||
"\x10available_themes\x18\x18 \x03(\tR\x0favailableThemes\"8\n" +
|
||||
"\x0eAdditionalLink\x12\x14\n" +
|
||||
"\x05title\x18\x01 \x01(\tR\x05title\x12\x10\n" +
|
||||
"\x03url\x18\x02 \x01(\tR\x03url\"L\n" +
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package api
|
|||
import (
|
||||
ctx "context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
|
|
@ -902,11 +904,44 @@ func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitReq
|
|||
ShowDiagnostics: user.EffectivePolicy.ShowDiagnostics,
|
||||
ShowLogList: user.EffectivePolicy.ShowLogList,
|
||||
LoginRequired: loginRequired,
|
||||
AvailableThemes: discoverAvailableThemes(api.cfg),
|
||||
}
|
||||
|
||||
return connect.NewResponse(res), nil
|
||||
}
|
||||
|
||||
// discoverAvailableThemes finds all available themes in the custom-webui/themes directory.
|
||||
// A theme is considered available if it has a theme.css file.
|
||||
func discoverAvailableThemes(cfg *config.Config) []string {
|
||||
themesDir := path.Join(cfg.GetDir(), "custom-webui", "themes")
|
||||
|
||||
entries, err := os.ReadDir(themesDir)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"themesDir": themesDir,
|
||||
"error": err,
|
||||
}).Tracef("Could not read themes directory")
|
||||
return []string{}
|
||||
}
|
||||
|
||||
var themes []string
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
themeName := entry.Name()
|
||||
themeCssPath := path.Join(themesDir, themeName, "theme.css")
|
||||
|
||||
if _, err := os.Stat(themeCssPath); err == nil {
|
||||
themes = append(themes, themeName)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(themes)
|
||||
return themes
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) buildRootDashboards(user *authpublic.AuthenticatedUser, dashboards []*config.DashboardComponent) []string {
|
||||
var rootDashboards []string
|
||||
dashboardRenderRequest := api.createDashboardRenderRequest(user, "", "")
|
||||
|
|
|
|||
Loading…
Reference in New Issue