mirror of https://github.com/bastienwirtz/homer
feat/greedyjohnny: added "expand" and "collapse" functions for card blocks
parent
a421a6ba12
commit
95a961b0da
274
src/App.vue
274
src/App.vue
|
@ -1,25 +1,33 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="config"
|
|
||||||
id="app"
|
id="app"
|
||||||
|
v-if="config"
|
||||||
:class="[
|
:class="[
|
||||||
`theme-${config.theme}`,
|
'theme-' + config.theme,
|
||||||
`page-${currentPage}`,
|
'page-' + currentPage,
|
||||||
isDark ? 'dark' : 'light',
|
isDark ? 'is-dark' : 'is-light',
|
||||||
!config.footer ? 'no-footer' : '',
|
!config.footer ? 'no-footer' : '',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<DynamicTheme v-if="config.colors" :themes="config.colors" />
|
<DynamicTheme :themes="config.colors" />
|
||||||
|
|
||||||
<div id="bighead">
|
<div id="bighead">
|
||||||
<section v-if="config.header" class="first-line">
|
<section v-if="config.header" class="first-line">
|
||||||
<div v-cloak class="container">
|
<div v-cloak class="container">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<a href="#">
|
<a href="#">
|
||||||
<img v-if="config.logo" :src="config.logo" alt="dashboard logo" />
|
<img
|
||||||
|
v-if="config.logo"
|
||||||
|
:src="config.logo"
|
||||||
|
alt="dashboard logo"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
<i v-if="config.icon" :class="config.icon"></i>
|
<i v-if="config.icon" :class="config.icon"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-title">
|
<div
|
||||||
|
class="dashboard-title"
|
||||||
|
:class="{ 'no-logo': !config.icon || !config.logo }"
|
||||||
|
>
|
||||||
<span class="headline">{{ config.subtitle }}</span>
|
<span class="headline">{{ config.subtitle }}</span>
|
||||||
<h1>{{ config.title }}</h1>
|
<h1>{{ config.title }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,16 +40,16 @@
|
||||||
@navbar-toggle="showMenu = !showMenu"
|
@navbar-toggle="showMenu = !showMenu"
|
||||||
>
|
>
|
||||||
<DarkMode
|
<DarkMode
|
||||||
:default-value="config.defaults.colorTheme"
|
|
||||||
@updated="isDark = $event"
|
@updated="isDark = $event"
|
||||||
|
:defaultValue="config.defaults.colorTheme"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingToggle
|
<SettingToggle
|
||||||
|
@updated="vlayout = $event"
|
||||||
name="vlayout"
|
name="vlayout"
|
||||||
icon="fa-list"
|
icon="fa-list"
|
||||||
icon-alt="fa-columns"
|
iconAlt="fa-columns"
|
||||||
:default-value="config.defaults.layout == 'columns'"
|
:defaultValue="config.defaults.layout === 'columns'"
|
||||||
@updated="vlayout = $event"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SearchInput
|
<SearchInput
|
||||||
|
@ -49,8 +57,8 @@
|
||||||
:hotkey="searchHotkey()"
|
:hotkey="searchHotkey()"
|
||||||
@input="filterServices($event)"
|
@input="filterServices($event)"
|
||||||
@search-focus="showMenu = true"
|
@search-focus="showMenu = true"
|
||||||
@search-open="navigateToFirstService"
|
@search-open="navigateToFirstService($event?.target?.value)"
|
||||||
@search-cancel="filterServices()"
|
@search-cancel="resetFilter()"
|
||||||
/>
|
/>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
</div>
|
</div>
|
||||||
|
@ -65,65 +73,115 @@
|
||||||
<GetStarted v-if="configurationNeeded" />
|
<GetStarted v-if="configurationNeeded" />
|
||||||
|
|
||||||
<div v-if="!offline">
|
<div v-if="!offline">
|
||||||
<!-- Optional messages -->
|
|
||||||
<Message :item="config.message" />
|
<Message :item="config.message" />
|
||||||
|
|
||||||
<!-- Horizontal layout -->
|
<div
|
||||||
<div v-if="!vlayout || filter" class="columns is-multiline">
|
v-if="services.some(g => g.name)"
|
||||||
<template v-for="(group, groupIndex) in services">
|
class="mb-3"
|
||||||
<h2
|
style="text-align: right;"
|
||||||
v-if="group.name"
|
>
|
||||||
:key="`header-${groupIndex}`"
|
<button
|
||||||
class="column is-full group-title"
|
class="button is-info is-small"
|
||||||
:class="group.class"
|
@click="toggleAllGroups"
|
||||||
|
>
|
||||||
|
{{ allCollapsed ? 'Expand All' : 'Collapse All' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!vlayout || filter" class="columns is-multiline">
|
||||||
|
<template v-for="(group, groupIndex) in filteredServices" :key="groupIndex + '-horizontal'">
|
||||||
|
<h2 v-if="group.name" class="column is-full group-title">
|
||||||
|
<span
|
||||||
|
style="cursor: pointer;"
|
||||||
|
@click="group.collapsed = !group.collapsed"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
:class="[
|
||||||
|
'fa-fw',
|
||||||
|
group.icon ? group.icon : 'fas',
|
||||||
|
group.collapsed ? 'fa-chevron-down' : 'fa-chevron-up',
|
||||||
|
]"
|
||||||
|
style="margin-right:6px;"
|
||||||
|
></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<i
|
||||||
|
v-if="group.icon && !group.logo"
|
||||||
|
:class="['fa-fw', group.icon]"
|
||||||
|
></i>
|
||||||
|
<div
|
||||||
|
v-else-if="group.logo"
|
||||||
|
class="group-logo media-left"
|
||||||
>
|
>
|
||||||
<i v-if="group.icon" :class="['fa-fw', group.icon]"></i>
|
|
||||||
<div v-else-if="group.logo" class="group-logo media-left">
|
|
||||||
<figure class="image is-48x48">
|
<figure class="image is-48x48">
|
||||||
<img :src="group.logo" :alt="`${group.name} logo`" />
|
<img
|
||||||
|
:src="group.logo"
|
||||||
|
:alt="group.name + ' logo'"
|
||||||
|
/>
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
{{ group.name }}
|
{{ group.name }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<Service
|
<Service
|
||||||
v-for="(item, index) in group.items"
|
v-for="(item, index) in group.items"
|
||||||
:key="`service-${groupIndex}-${index}`"
|
:key="'service-' + groupIndex + '-' + index"
|
||||||
:item="item"
|
:item="item"
|
||||||
:proxy="config.proxy"
|
:proxy="config.proxy"
|
||||||
:class="[
|
:class="['column', 'is-' + (12 / config.columns)]"
|
||||||
'column',
|
v-show="!group.collapsed"
|
||||||
`is-${12 / config.columns}`,
|
|
||||||
`${item.class || group.class || ''}`,
|
|
||||||
]"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Vertical layout -->
|
|
||||||
<div
|
<div
|
||||||
v-if="!filter && vlayout"
|
v-if="!filter && vlayout"
|
||||||
class="columns is-multiline layout-vertical"
|
class="columns is-multiline layout-vertical"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(group, groupIndex) in services"
|
v-for="(group, groupIndex) in filteredServices"
|
||||||
:key="groupIndex"
|
:key="groupIndex + '-vertical'"
|
||||||
:class="['column', `is-${12 / config.columns}`]"
|
:class="['column', 'is-' + (12 / config.columns)]"
|
||||||
|
>
|
||||||
|
<h2 v-if="group.name" class="group-title">
|
||||||
|
<span
|
||||||
|
style="cursor: pointer;"
|
||||||
|
@click="group.collapsed = !group.collapsed"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
:class="[
|
||||||
|
'fa-fw',
|
||||||
|
group.icon ? group.icon : 'fas',
|
||||||
|
group.collapsed ? 'fa-chevron-down' : 'fa-chevron-up',
|
||||||
|
]"
|
||||||
|
style="margin-right:6px;"
|
||||||
|
></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<i
|
||||||
|
v-if="group.icon && !group.logo"
|
||||||
|
:class="['fa-fw', group.icon]"
|
||||||
|
></i>
|
||||||
|
<div
|
||||||
|
v-else-if="group.logo"
|
||||||
|
class="group-logo media-left"
|
||||||
>
|
>
|
||||||
<h2 v-if="group.name" class="group-title" :class="group.class">
|
|
||||||
<i v-if="group.icon" :class="['fa-fw', group.icon]"></i>
|
|
||||||
<div v-else-if="group.logo" class="group-logo media-left">
|
|
||||||
<figure class="image is-48x48">
|
<figure class="image is-48x48">
|
||||||
<img :src="group.logo" :alt="`${group.name} logo`" />
|
<img
|
||||||
|
:src="group.logo"
|
||||||
|
:alt="group.name + ' logo'"
|
||||||
|
/>
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
{{ group.name }}
|
{{ group.name }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<Service
|
<Service
|
||||||
v-for="(item, index) in group.items"
|
v-for="(item, index) in group.items"
|
||||||
:key="index"
|
:key="index"
|
||||||
:item="item"
|
:item="item"
|
||||||
:proxy="config.proxy"
|
:proxy="config.proxy"
|
||||||
:class="item.class || group.class"
|
v-show="!group.collapsed"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -134,8 +192,8 @@
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div
|
<div
|
||||||
v-if="config.footer"
|
|
||||||
class="content has-text-centered"
|
class="content has-text-centered"
|
||||||
|
v-if="config.footer"
|
||||||
v-html="config.footer"
|
v-html="config.footer"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -172,35 +230,35 @@ export default {
|
||||||
DarkMode,
|
DarkMode,
|
||||||
DynamicTheme,
|
DynamicTheme,
|
||||||
},
|
},
|
||||||
data: function () {
|
data() {
|
||||||
return {
|
return {
|
||||||
loaded: false,
|
loaded: false,
|
||||||
currentPage: null,
|
currentPage: null,
|
||||||
configNotFound: false,
|
configNotFound: false,
|
||||||
config: null,
|
config: null,
|
||||||
services: null,
|
services: null,
|
||||||
|
filteredServices: null,
|
||||||
offline: false,
|
offline: false,
|
||||||
filter: "",
|
filter: "",
|
||||||
vlayout: true,
|
vlayout: true,
|
||||||
isDark: null,
|
isDark: null,
|
||||||
showMenu: false,
|
showMenu: false,
|
||||||
|
allCollapsed: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
configurationNeeded: function () {
|
configurationNeeded() {
|
||||||
return (this.loaded && !this.services) || this.configNotFound;
|
return (this.loaded && !this.services) || this.configNotFound;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created: async function () {
|
created() {
|
||||||
this.buildDashboard();
|
this.buildDashboard();
|
||||||
window.onhashchange = this.buildDashboard;
|
window.onhashchange = this.buildDashboard;
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
searchHotkey() {
|
searchHotkey() {
|
||||||
if (this.config.hotkey && this.config.hotkey.search) {
|
return this.config?.hotkey?.search || "/";
|
||||||
return this.config.hotkey.search;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
buildDashboard: async function () {
|
buildDashboard: async function () {
|
||||||
const defaults = parse(defaultConfig);
|
const defaults = parse(defaultConfig);
|
||||||
|
@ -210,116 +268,98 @@ export default {
|
||||||
this.currentPage = window.location.hash.substring(1) || "default";
|
this.currentPage = window.location.hash.substring(1) || "default";
|
||||||
|
|
||||||
if (this.currentPage !== "default") {
|
if (this.currentPage !== "default") {
|
||||||
let pageConfig = await this.getConfig(
|
const pageConfig = await this.getConfig(`assets/${this.currentPage}.yml`);
|
||||||
`assets/${this.currentPage}.yml`,
|
|
||||||
);
|
|
||||||
config = Object.assign(config, pageConfig);
|
config = Object.assign(config, pageConfig);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
config = this.handleErrors("⚠️ Error loading configuration", error);
|
config = this.handleErrors("⚠️ Error loading configuration", error);
|
||||||
}
|
}
|
||||||
this.config = merge(defaults, config);
|
|
||||||
this.services = this.config.services;
|
|
||||||
|
|
||||||
document.title =
|
this.config = merge(defaults, config);
|
||||||
this.config.documentTitle ||
|
this.services = this.config.services.map((group) => ({
|
||||||
`${this.config.title} | ${this.config.subtitle}`;
|
...group,
|
||||||
|
collapsed: false,
|
||||||
|
}));
|
||||||
|
this.filteredServices = this.services;
|
||||||
|
|
||||||
|
document.title = this.config.documentTitle || `${this.config.title} | ${this.config.subtitle}`;
|
||||||
|
|
||||||
if (this.config.stylesheet) {
|
if (this.config.stylesheet) {
|
||||||
let stylesheet = "";
|
let stylesheet = "";
|
||||||
let addtionnal_styles = this.config.stylesheet;
|
for (const file of this.config.stylesheet) {
|
||||||
if (!Array.isArray(this.config.stylesheet)) {
|
|
||||||
addtionnal_styles = [addtionnal_styles];
|
|
||||||
}
|
|
||||||
for (const file of addtionnal_styles) {
|
|
||||||
stylesheet += `@import "${file}";`;
|
stylesheet += `@import "${file}";`;
|
||||||
}
|
}
|
||||||
this.createStylesheet(stylesheet);
|
this.createStylesheet(stylesheet);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getConfig: function (path = "assets/config.yml") {
|
getConfig(path = "assets/config.yml") {
|
||||||
return fetch(path).then((response) => {
|
return fetch(path).then((response) => {
|
||||||
if (response.status == 404 || response.redirected) {
|
if (response.status == 404 || response.redirected) {
|
||||||
this.configNotFound = true;
|
this.configNotFound = true;
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw Error(`${response.statusText}: ${response.body}`);
|
throw Error(`${response.statusText}: ${response.body}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const that = this;
|
return response.text().then((body) => parse(body, { merge: true }));
|
||||||
return response
|
|
||||||
.text()
|
|
||||||
.then((body) => {
|
|
||||||
return parse(body, { merge: true });
|
|
||||||
})
|
|
||||||
.then(function (config) {
|
|
||||||
if (config.externalConfig) {
|
|
||||||
return that.getConfig(config.externalConfig);
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
matchesFilter: function (item) {
|
filterServices(filter) {
|
||||||
const needle = this.filter?.toLowerCase();
|
this.filter = filter?.toLowerCase() || "";
|
||||||
return (
|
if (!this.filter) {
|
||||||
item.name.toLowerCase().includes(needle) ||
|
this.filteredServices = this.services;
|
||||||
(item.subtitle && item.subtitle.toLowerCase().includes(needle)) ||
|
|
||||||
(item.tag && item.tag.toLowerCase().includes(needle)) ||
|
|
||||||
(item.keywords && item.keywords.toLowerCase().includes(needle))
|
|
||||||
);
|
|
||||||
},
|
|
||||||
navigateToFirstService: function (target) {
|
|
||||||
try {
|
|
||||||
const service = this.services[0].items[0];
|
|
||||||
window.open(service.url, target || service.target || "_self");
|
|
||||||
} catch {
|
|
||||||
console.warn("fail to open service");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
filterServices: function (filter) {
|
|
||||||
this.filter = filter;
|
|
||||||
|
|
||||||
if (!filter) {
|
|
||||||
this.services = this.config.services;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchResultItems = [];
|
this.filteredServices = this.services
|
||||||
for (const group of this.config.services) {
|
.map((group) => {
|
||||||
if (group.items !== null) {
|
const items = group.items.filter((item) =>
|
||||||
for (const item of group.items) {
|
[item.name, item.subtitle, item.tag, item.keywords]
|
||||||
if (this.matchesFilter(item)) {
|
.filter(Boolean)
|
||||||
searchResultItems.push(item);
|
.some((text) => text.toLowerCase().includes(this.filter))
|
||||||
}
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.services = [
|
return items.length > 0 ? { ...group, items, collapsed: false } : null;
|
||||||
{
|
})
|
||||||
name: filter,
|
.filter(Boolean);
|
||||||
icon: "fas fa-search",
|
|
||||||
items: searchResultItems,
|
|
||||||
},
|
},
|
||||||
];
|
resetFilter() {
|
||||||
|
this.filter = "";
|
||||||
|
this.filteredServices = this.services;
|
||||||
},
|
},
|
||||||
handleErrors: function (title, content) {
|
toggleAllGroups() {
|
||||||
|
this.allCollapsed = !this.allCollapsed;
|
||||||
|
this.filteredServices.forEach((group) => {
|
||||||
|
group.collapsed = this.allCollapsed;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleErrors(title, content) {
|
||||||
return {
|
return {
|
||||||
message: {
|
message: {
|
||||||
title: title,
|
title,
|
||||||
style: "is-danger",
|
style: "is-danger",
|
||||||
content: content,
|
content,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
createStylesheet: function (css) {
|
createStylesheet(css) {
|
||||||
let style = document.createElement("style");
|
const style = document.createElement("style");
|
||||||
style.appendChild(document.createTextNode(css));
|
style.appendChild(document.createTextNode(css));
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.group-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.group-logo {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import "./assets/app.scss";
|
|
||||||
import { createApp, h } from "vue";
|
import { createApp, h } from "vue";
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
|
|
||||||
|
import "@fortawesome/fontawesome-free/css/all.css";
|
||||||
|
import "./assets/app.scss";
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
app.component("DynamicStyle", (_props, context) => {
|
app.component("DynamicStyle", (_props, context) => {
|
||||||
|
|
Loading…
Reference in New Issue