feat/greedyjohnny: added "expand" and "collapse" functions for card blocks

pull/863/head
greedyjohnny-yt 2025-01-30 13:19:52 +03:00 committed by GitHub
parent a421a6ba12
commit 95a961b0da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 161 additions and 119 deletions

View File

@ -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
v-if="services.some(g => g.name)"
class="mb-3"
style="text-align: right;"
>
<button
class="button is-info is-small"
@click="toggleAllGroups"
>
{{ allCollapsed ? 'Expand All' : 'Collapse All' }}
</button>
</div>
<div v-if="!vlayout || filter" class="columns is-multiline"> <div v-if="!vlayout || filter" class="columns is-multiline">
<template v-for="(group, groupIndex) in services"> <template v-for="(group, groupIndex) in filteredServices" :key="groupIndex + '-horizontal'">
<h2 <h2 v-if="group.name" class="column is-full group-title">
v-if="group.name" <span
:key="`header-${groupIndex}`" style="cursor: pointer;"
class="column is-full group-title" @click="group.collapsed = !group.collapsed"
:class="group.class" >
> <i
<i v-if="group.icon" :class="['fa-fw', group.icon]"></i> :class="[
<div v-else-if="group.logo" class="group-logo media-left"> '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"
>
<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" :class="group.class"> <h2 v-if="group.name" class="group-title">
<i v-if="group.icon" :class="['fa-fw', group.icon]"></i> <span
<div v-else-if="group.logo" class="group-logo media-left"> 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"
>
<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,
},
];
}, },
handleErrors: function (title, content) { resetFilter() {
this.filter = "";
this.filteredServices = this.services;
},
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>

View File

@ -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) => {