diff options
Diffstat (limited to 'src/App.vue')
-rw-r--r-- | src/App.vue | 214 |
1 files changed, 214 insertions, 0 deletions
diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..f7fd34a --- /dev/null +++ b/src/App.vue | |||
@@ -0,0 +1,214 @@ | |||
1 | <template> | ||
2 | <div | ||
3 | id="app" | ||
4 | v-if="config" | ||
5 | :class="[ | ||
6 | `theme-${config.theme}`, | ||
7 | isDark ? 'is-dark' : 'is-light', | ||
8 | !config.footer ? 'no-footer' : '' | ||
9 | ]" | ||
10 | > | ||
11 | <DynamicTheme :themes="config.colors" /> | ||
12 | <div id="bighead"> | ||
13 | <section v-if="config.header" class="first-line"> | ||
14 | <div v-cloak class="container"> | ||
15 | <div class="logo"> | ||
16 | <img v-if="config.logo" :src="config.logo" /> | ||
17 | <i v-if="config.icon" :class="config.icon"></i> | ||
18 | </div> | ||
19 | <div class="dashboard-title"> | ||
20 | <span class="headline">{{ config.subtitle }}</span> | ||
21 | <h1>{{ config.title }}</h1> | ||
22 | </div> | ||
23 | </div> | ||
24 | </section> | ||
25 | |||
26 | <Navbar | ||
27 | :open="showMenu" | ||
28 | :links="config.links" | ||
29 | @navbar:toggle="showMenu = !showMenu" | ||
30 | > | ||
31 | <DarkMode @updated="isDark = $event" /> | ||
32 | |||
33 | <SettingToggle | ||
34 | @updated="vlayout = $event" | ||
35 | name="vlayout" | ||
36 | icon="fa-list" | ||
37 | iconAlt="fa-columns" | ||
38 | /> | ||
39 | |||
40 | <SearchInput | ||
41 | class="navbar-item is-inline-block-mobile" | ||
42 | @input="filterServices" | ||
43 | @search:focus="showMenu = true" | ||
44 | @search:open="navigateToFirstService" | ||
45 | @search:cancel="filterServices" | ||
46 | /> | ||
47 | </Navbar> | ||
48 | </div> | ||
49 | |||
50 | <section id="main-section" class="section"> | ||
51 | <div v-cloak class="container"> | ||
52 | <ConnectivityChecker @network:status-update="offline = $event" /> | ||
53 | <div v-if="!offline"> | ||
54 | <!-- Optional messages --> | ||
55 | <Message :item="config.message" /> | ||
56 | |||
57 | <!-- Horizontal layout --> | ||
58 | <div v-if="!vlayout || filter" class="columns is-multiline"> | ||
59 | <template v-for="group in services"> | ||
60 | <h2 v-if="group.name" class="column is-full group-title"> | ||
61 | <i v-if="group.icon" :class="group.icon"></i> | ||
62 | {{ group.name }} | ||
63 | </h2> | ||
64 | <Service | ||
65 | v-for="item in group.items" | ||
66 | :key="item.url" | ||
67 | v-bind:item="item" | ||
68 | class="column is-one-third-widescreen" | ||
69 | /> | ||
70 | </template> | ||
71 | </div> | ||
72 | |||
73 | <!-- Vertical layout --> | ||
74 | <div | ||
75 | v-if="!filter && vlayout" | ||
76 | class="columns is-multiline layout-vertical" | ||
77 | > | ||
78 | <div | ||
79 | class="column is-one-third-widescreen" | ||
80 | v-for="group in services" | ||
81 | :key="group.name" | ||
82 | > | ||
83 | <h2 v-if="group.name" class="group-title"> | ||
84 | <i v-if="group.icon" :class="group.icon"></i> | ||
85 | {{ group.name }} | ||
86 | </h2> | ||
87 | <Service | ||
88 | v-for="item in group.items" | ||
89 | v-bind:item="item" | ||
90 | :key="item.url" | ||
91 | /> | ||
92 | </div> | ||
93 | </div> | ||
94 | </div> | ||
95 | </div> | ||
96 | </section> | ||
97 | |||
98 | <footer class="footer"> | ||
99 | <div class="container"> | ||
100 | <div | ||
101 | class="content has-text-centered" | ||
102 | v-if="config.footer" | ||
103 | v-html="config.footer" | ||
104 | ></div> | ||
105 | </div> | ||
106 | </footer> | ||
107 | </div> | ||
108 | </template> | ||
109 | |||
110 | <script> | ||
111 | const jsyaml = require("js-yaml"); | ||
112 | const merge = require("lodash.merge"); | ||
113 | |||
114 | import Navbar from "./components/Navbar.vue"; | ||
115 | import ConnectivityChecker from "./components/ConnectivityChecker.vue"; | ||
116 | import Service from "./components/Service.vue"; | ||
117 | import Message from "./components/Message.vue"; | ||
118 | import SearchInput from "./components/SearchInput.vue"; | ||
119 | import SettingToggle from "./components/SettingToggle.vue"; | ||
120 | import DarkMode from "./components/DarkMode.vue"; | ||
121 | import DynamicTheme from "./components/DynamicTheme.vue"; | ||
122 | |||
123 | import defaultConfig from "./assets/defaults.yml"; | ||
124 | |||
125 | export default { | ||
126 | name: "App", | ||
127 | components: { | ||
128 | Navbar, | ||
129 | ConnectivityChecker, | ||
130 | Service, | ||
131 | Message, | ||
132 | SearchInput, | ||
133 | SettingToggle, | ||
134 | DarkMode, | ||
135 | DynamicTheme | ||
136 | }, | ||
137 | data: function () { | ||
138 | return { | ||
139 | config: null, | ||
140 | services: null, | ||
141 | offline: false, | ||
142 | filter: "", | ||
143 | vlayout: true, | ||
144 | isDark: null, | ||
145 | showMenu: false | ||
146 | }; | ||
147 | }, | ||
148 | created: async function () { | ||
149 | try { | ||
150 | const defaults = jsyaml.load(defaultConfig); | ||
151 | let config = await this.getConfig(); | ||
152 | |||
153 | this.config = merge(defaults, config); | ||
154 | |||
155 | console.log(this.config); | ||
156 | this.services = this.config.services; | ||
157 | document.title = `${this.config.title} | ${this.config.subtitle}`; | ||
158 | } catch (error) { | ||
159 | this.offline = true; | ||
160 | } | ||
161 | }, | ||
162 | methods: { | ||
163 | getConfig: function () { | ||
164 | return fetch("config.yml").then(function (response) { | ||
165 | if (response.status != 200) { | ||
166 | return; | ||
167 | } | ||
168 | return response.text().then(function (body) { | ||
169 | return jsyaml.load(body); | ||
170 | }); | ||
171 | }); | ||
172 | }, | ||
173 | matchesFilter: function (item) { | ||
174 | return ( | ||
175 | item.name.toLowerCase().includes(this.filter) || | ||
176 | (item.tag && item.tag.toLowerCase().includes(this.filter)) | ||
177 | ); | ||
178 | }, | ||
179 | navigateToFirstService: function (target) { | ||
180 | try { | ||
181 | const service = this.services[0].items[0]; | ||
182 | window.open(service.url, target || service.target || "_self"); | ||
183 | } catch (error) { | ||
184 | console.warning("fail to open service"); | ||
185 | } | ||
186 | }, | ||
187 | filterServices: function (filter) { | ||
188 | this.filter = filter; | ||
189 | |||
190 | if (!filter) { | ||
191 | this.services = this.config.services; | ||
192 | return; | ||
193 | } | ||
194 | |||
195 | const searchResultItems = []; | ||
196 | for (const group of this.config.services) { | ||
197 | for (const item of group.items) { | ||
198 | if (this.matchesFilter(item)) { | ||
199 | searchResultItems.push(item); | ||
200 | } | ||
201 | } | ||
202 | } | ||
203 | |||
204 | this.services = [ | ||
205 | { | ||
206 | name: filter, | ||
207 | icon: "fas fa-search", | ||
208 | items: searchResultItems | ||
209 | } | ||
210 | ]; | ||
211 | } | ||
212 | } | ||
213 | }; | ||
214 | </script> | ||