Merge branch 'master' into update-simple-icons
This commit is contained in:
@@ -30,7 +30,7 @@
|
||||
<!-- License -->
|
||||
<h3>{{ $t('app-info.license') }}</h3>
|
||||
{{ $t('app-info.license-under') }} <a href="https://github.com/Lissy93/dashy/blob/master/LICENSE">MIT X11</a>.
|
||||
Copyright <a href="https://aliciasykes.com">Alicia Sykes</a> © 2021.<br>
|
||||
Copyright <a href="https://aliciasykes.com">Alicia Sykes</a> © {{new Date().getFullYear()}}.<br>
|
||||
{{ $t('app-info.licence-third-party') }} <a href="https://github.com/Lissy93/dashy/blob/master/.github/LEGAL.md">{{ $t('app-info.licence-third-party-link') }}</a>.<br>
|
||||
{{ $t('app-info.list-contributors') }} <a href="https://github.com/Lissy93/dashy/blob/master/docs/credits.md">{{ $t('app-info.list-contributors-link') }}</a>.
|
||||
<!-- App Version -->
|
||||
|
||||
@@ -155,12 +155,23 @@ export default {
|
||||
},
|
||||
/* When restored data is revieved, then save to local storage, and apply it in state */
|
||||
applyRestoredData(config, backupId) {
|
||||
// Store restored data in local storage
|
||||
localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(config.sections));
|
||||
localStorage.setItem(localStorageKeys.APP_CONFIG, JSON.stringify(config.appConfig));
|
||||
localStorage.setItem(localStorageKeys.PAGE_INFO, JSON.stringify(config.pageInfo));
|
||||
if (config.appConfig.theme) {
|
||||
localStorage.setItem(localStorageKeys.THEME, config.appConfig.theme);
|
||||
const isSubPage = !!this.$store.state.currentConfigInfo.confId;
|
||||
if (isSubPage) { // Apply to sub-page only
|
||||
const subConfigId = this.$store.state.currentConfigInfo.confId;
|
||||
const sectionStorageKey = `${localStorageKeys.CONF_SECTIONS}-${subConfigId}`;
|
||||
const pageInfoStorageKey = `${localStorageKeys.PAGE_INFO}-${subConfigId}`;
|
||||
const themeStoreKey = `${localStorageKeys.THEME}-${subConfigId}`;
|
||||
localStorage.setItem(sectionStorageKey, JSON.stringify(config.sections));
|
||||
localStorage.setItem(pageInfoStorageKey, JSON.stringify(config.pageInfo));
|
||||
localStorage.setItem(themeStoreKey, config.appConfig.theme);
|
||||
} else { // Apply to main config
|
||||
localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(config.sections));
|
||||
localStorage.setItem(localStorageKeys.APP_CONFIG, JSON.stringify(config.appConfig));
|
||||
localStorage.setItem(localStorageKeys.PAGE_INFO, JSON.stringify(config.pageInfo));
|
||||
localStorage.setItem(localStorageKeys.CONF_PAGES, JSON.stringify(config.pages || []));
|
||||
if (config.appConfig.theme) {
|
||||
localStorage.setItem(localStorageKeys.THEME, config.appConfig.theme);
|
||||
}
|
||||
}
|
||||
// Save hashed token in local storage
|
||||
this.setBackupIdLocally(backupId, this.restorePassword);
|
||||
|
||||
@@ -47,16 +47,17 @@
|
||||
</Button>
|
||||
<!-- Display app version and language -->
|
||||
<p class="language">{{ getLanguage() }}</p>
|
||||
<p v-if="$store.state.currentConfigInfo" class="config-location">
|
||||
Using Config From<br>
|
||||
{{ $store.state.currentConfigInfo.confPath }}
|
||||
<!-- Display location of config file -->
|
||||
<p class="config-location">
|
||||
Using config from
|
||||
<a :href="configPath">{{ configPath }}</a>
|
||||
</p>
|
||||
<AppVersion />
|
||||
</div>
|
||||
<!-- Display note if Config disabled, or if on mobile -->
|
||||
<p v-if="!enableConfig" class="config-disabled-note">{{ $t('config.disabled-note') }}</p>
|
||||
<p class="small-screen-note" style="display: none;">{{ $t('config.small-screen-note') }}</p>
|
||||
<div class="config-note">
|
||||
<div class="config-note" @click="openExportConfigModal">
|
||||
<span>{{ $t('config.backup-note') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,6 +117,11 @@ export default {
|
||||
enableConfig() {
|
||||
return this.$store.getters.permissions.allowViewConfig;
|
||||
},
|
||||
configPath() {
|
||||
return this.$store.state.currentConfigInfo?.confPath
|
||||
|| process.env.VUE_APP_CONFIG_PATH
|
||||
|| '/conf.yml';
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Button,
|
||||
@@ -248,8 +254,12 @@ a.hyperlink-wrapper {
|
||||
p.app-version, p.language, p.config-location {
|
||||
margin: 0.5rem auto;
|
||||
font-size: 1rem;
|
||||
color: var(--transparent-white-50);
|
||||
color: var(--config-settings-color);
|
||||
cursor: default;
|
||||
opacity: var(--dimming-factor);
|
||||
a {
|
||||
color: var(--config-settings-color);
|
||||
}
|
||||
}
|
||||
|
||||
div.code-container {
|
||||
|
||||
@@ -115,7 +115,10 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.jsonData = this.config;
|
||||
const jsonData = { ...this.config };
|
||||
jsonData.sections = (jsonData.sections || []).map(({ filteredItems, ...section }) => section);
|
||||
if (!jsonData.pageInfo) jsonData.pageInfo = { title: 'Dashy' };
|
||||
this.jsonData = jsonData;
|
||||
if (!this.allowWriteToDisk) this.saveMode = 'local';
|
||||
},
|
||||
methods: {
|
||||
@@ -141,7 +144,11 @@ export default {
|
||||
this.$modal.hide(modalNames.CONF_EDITOR);
|
||||
},
|
||||
writeToDisk() {
|
||||
this.writeConfigToDisk(this.config);
|
||||
const newData = this.jsonData;
|
||||
this.writeConfigToDisk(newData);
|
||||
// this.$store.commit(StoreKeys.SET_APP_CONFIG, newData.appConfig);
|
||||
this.$store.commit(StoreKeys.SET_PAGE_INFO, newData.pageInfo);
|
||||
this.$store.commit(StoreKeys.SET_SECTIONS, newData.sections);
|
||||
},
|
||||
saveLocally() {
|
||||
const msg = this.$t('interactive-editor.menu.save-locally-warning');
|
||||
|
||||
@@ -94,6 +94,7 @@ export default {
|
||||
const raw = rawAppConfig;
|
||||
const isEmptyObject = (obj) => (typeof obj === 'object' && Object.keys(obj).length === 0);
|
||||
const isEmpty = (value) => (value === undefined || isEmptyObject(value));
|
||||
|
||||
// Delete empty values
|
||||
Object.keys(raw).forEach(key => {
|
||||
if (isEmpty(raw[key])) delete raw[key];
|
||||
|
||||
@@ -22,6 +22,14 @@
|
||||
<DownloadConfigIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<!-- Show path to which config file is being used -->
|
||||
<div class="config-path-info">
|
||||
<h3>Config Location</h3>
|
||||
<p>
|
||||
The base config file you are currently using is
|
||||
<a :href="configPath">{{ configPath }}</a>
|
||||
</p>
|
||||
</div>
|
||||
<!-- View Config in Tree Mode Section -->
|
||||
<h3>{{ $t('interactive-editor.export.view-title') }}</h3>
|
||||
<tree-view :data="config" class="config-tree-view" />
|
||||
@@ -61,6 +69,11 @@ export default {
|
||||
allowViewConfig() {
|
||||
return this.$store.getters.permissions.allowViewConfig;
|
||||
},
|
||||
configPath() {
|
||||
return this.$store.state.currentConfigInfo?.confPath
|
||||
|| process.env.VUE_APP_CONFIG_PATH
|
||||
|| '/conf.yml';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
convertJsonToYaml() {
|
||||
@@ -121,6 +134,13 @@ export default {
|
||||
border-bottom: 1px dashed var(--interactive-editor-color);
|
||||
button { margin: 0 1rem; }
|
||||
}
|
||||
.config-path-info {
|
||||
p, a {
|
||||
color: var(--interactive-editor-color);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
border-bottom: 1px dashed var(--interactive-editor-color);
|
||||
}
|
||||
.config-tree-view {
|
||||
padding: 0.5rem;
|
||||
font-family: var(--font-monospace);
|
||||
|
||||
@@ -64,7 +64,6 @@ export default {
|
||||
return this.$store.state.editMode;
|
||||
},
|
||||
sectionKey() {
|
||||
if (this.isEditMode) return undefined;
|
||||
return `collapsible-${this.uniqueKey}`;
|
||||
},
|
||||
collapseClass() {
|
||||
@@ -104,12 +103,23 @@ export default {
|
||||
watch: {
|
||||
checkboxState(newState) {
|
||||
this.isExpanded = newState;
|
||||
this.updateLocalStorage(); // Save every change immediately
|
||||
},
|
||||
uniqueKey() {
|
||||
this.checkboxState = this.isExpanded;
|
||||
uniqueKey(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.refreshCollapseState(); // Refresh state when key changes
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
refreshCollapseState() {
|
||||
this.checkboxState = this.isExpanded;
|
||||
},
|
||||
updateLocalStorage() {
|
||||
const collapseState = this.locallyStoredCollapseStates();
|
||||
collapseState[this.uniqueKey] = this.checkboxState;
|
||||
localStorage.setItem(localStorageKeys.COLLAPSE_STATE, JSON.stringify(collapseState));
|
||||
},
|
||||
/* Either expand or collapse section, based on it's current state */
|
||||
toggle() {
|
||||
this.checkboxState = !this.checkboxState;
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
:target="anchorTarget"
|
||||
:class="`item ${makeClassList}`"
|
||||
v-tooltip="getTooltipOptions()"
|
||||
rel="noopener noreferrer" tabindex="0"
|
||||
:rel="`${item.rel || 'noopener noreferrer'}`"
|
||||
tabindex="0"
|
||||
:id="`link-${item.id}`"
|
||||
:style="customStyle"
|
||||
>
|
||||
@@ -255,8 +256,12 @@ export default {
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
word-break: keep-all;
|
||||
overflow: hidden;
|
||||
span.text {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,6 +390,7 @@ p.description {
|
||||
font-size: .9em;
|
||||
line-height: 1rem;
|
||||
height: 2rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,8 +74,8 @@
|
||||
</div>
|
||||
<!-- Modal for opening in modal view -->
|
||||
<IframeModal
|
||||
:ref="`iframeModal`"
|
||||
:name="`iframeModal`"
|
||||
:ref="`iframeModal-${groupId}`"
|
||||
:name="`iframeModal-${groupId}`"
|
||||
@closed="$emit('itemClicked')"
|
||||
/>
|
||||
<!-- Edit item menu -->
|
||||
@@ -213,7 +213,7 @@ export default {
|
||||
methods: {
|
||||
/* Opens the iframe modal */
|
||||
triggerModal(url) {
|
||||
this.$refs.iframeModal.show(url);
|
||||
this.$refs[`iframeModal-${this.groupId}`].show(url);
|
||||
},
|
||||
/* Sorts items alphabetically using the title attribute */
|
||||
sortAlphabetically(items) {
|
||||
|
||||
153
src/components/PageStrcture/CriticalError.vue
Normal file
153
src/components/PageStrcture/CriticalError.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div class="critical-error-wrap" v-if="shouldShow">
|
||||
<button class="close" title="Close Warning" @click="close">🗙</button>
|
||||
<h3>{{ $t('critical-error.title') }}</h3>
|
||||
<p>{{ $t('critical-error.subtitle') }}</p>
|
||||
<h4>{{ $t('critical-error.sub-ensure-that') }}</h4>
|
||||
<ul>
|
||||
<li>The configuration file can be found at the specified location</li>
|
||||
<li>There are no CORS rules preventing client-side access</li>
|
||||
<li>The YAML is valid, parsable and matches the schema</li>
|
||||
</ul>
|
||||
<h4>{{ $t('critical-error.sub-error-details') }}</h4>
|
||||
<pre>{{ this.$store.state.criticalError }}</pre>
|
||||
<h4>{{ $t('critical-error.sub-next-steps') }}</h4>
|
||||
<ul>
|
||||
<li>Check the browser console for more details
|
||||
(<a href="https://github.com/Lissy93/dashy/blob/master/docs/troubleshooting.md#how-to-open-browser-console">see how</a>)
|
||||
</li>
|
||||
<li>View the
|
||||
<a href="https://github.com/Lissy93/dashy/blob/master/docs/troubleshooting.md">Troubleshooting Guide</a>
|
||||
and <a href="https://dashy.to/docs/">Docs</a>
|
||||
</li>
|
||||
<li>
|
||||
If you've verified the config is present, accessible and valid, and cannot find the solution
|
||||
in the troubleshooting, docs or GitHub issues,
|
||||
then <a href="https://github.com/Lissy93/dashy/issues/new/choose">open a ticket on GitHub</a>
|
||||
</li>
|
||||
<li>Click 'Ignore Critical Errors' below to not show this warning again</li>
|
||||
</ul>
|
||||
<button class="user-doesnt-care" @click="ignoreWarning">
|
||||
{{ $t('critical-error.ignore-button') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { localStorageKeys } from '@/utils/defaults';
|
||||
import Keys from '@/utils/StoreMutations';
|
||||
|
||||
export default {
|
||||
name: 'CriticalError',
|
||||
computed: {
|
||||
/* Determines if we should show this component.
|
||||
* If error present AND user hasn't disabled */
|
||||
shouldShow() {
|
||||
return this.$store.state.criticalError
|
||||
&& !localStorage[localStorageKeys.DISABLE_CRITICAL_WARNING];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* Ignore all future errors, by putting a key in local storage */
|
||||
ignoreWarning() {
|
||||
localStorage.setItem(localStorageKeys.DISABLE_CRITICAL_WARNING, true);
|
||||
this.close();
|
||||
},
|
||||
/* Close this dialog, by removing this error from the local store */
|
||||
close() {
|
||||
this.$store.commit(Keys.CRITICAL_ERROR_MSG, null);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/media-queries.scss';
|
||||
.critical-error-wrap {
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 3;
|
||||
max-width: 50rem;
|
||||
background: var(--background-darker);
|
||||
padding: 1rem;
|
||||
border-radius: var(--curve-factor);
|
||||
color: var(--danger);
|
||||
border: 2px solid var(--danger);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
@include tablet-down {
|
||||
top: 50%;
|
||||
width: 85vw;
|
||||
}
|
||||
p, ul, h4, a {
|
||||
margin: 0;
|
||||
color: var(--white);
|
||||
}
|
||||
pre {
|
||||
color: var(--warning);
|
||||
font-size: 0.8rem;
|
||||
overflow: auto;
|
||||
background: var(--transparent-white-10);
|
||||
padding: 0.5rem;
|
||||
border-radius: var(--curve-factor);
|
||||
}
|
||||
h4 {
|
||||
margin: 0.5rem 0 0 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
h3 {
|
||||
font-size: 2.2rem;
|
||||
text-align: center;
|
||||
background: var(--danger);
|
||||
color: var(--white);
|
||||
margin: -1rem -1rem 1rem -1rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
ul {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
.user-doesnt-care {
|
||||
background: var(--background-darker);
|
||||
color: var(--white);
|
||||
border-radius: var(--curve-factor);
|
||||
border: none;
|
||||
text-decoration: underline;
|
||||
padding: 0.25rem 0.5rem;
|
||||
cursor: pointer;
|
||||
width: fit-content;
|
||||
margin: 0 auto;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background: var(--danger);
|
||||
color: var(--background-darker);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 1rem;
|
||||
background: var(--background);
|
||||
color: var(--primary);
|
||||
border: none;
|
||||
border-radius: var(--curve-factor);
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background: var(--primary);
|
||||
color: var(--background);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,16 +1,13 @@
|
||||
<template>
|
||||
<!-- User Footer -->
|
||||
<footer v-if="text && text !== '' && visible" v-html="text"></footer>
|
||||
<!-- Default Footer -->
|
||||
<footer v-else-if="visible">
|
||||
<span v-if="$store.state.currentConfigInfo" class="path-to-config">
|
||||
Using: {{ $store.state.currentConfigInfo.confPath }}
|
||||
</span>
|
||||
<span>
|
||||
{{ $t('footer.dev-by') }} <a :href="authorUrl">{{authorName}}</a>.
|
||||
{{ $t('footer.licensed-under') }} <a :href="licenseUrl">{{license}}</a>
|
||||
{{ showCopyright? '©': '' }} {{date}}.
|
||||
{{ $t('footer.get-the') }} <a :href="repoUrl">{{ $t('footer.source-code') }}</a>.
|
||||
<footer v-if="visible">
|
||||
<!-- User-defined footer -->
|
||||
<span v-if="text" v-html="text"></span>
|
||||
<!-- Default footer -->
|
||||
<span v-else>
|
||||
<a :href="defaultInfo.projectUrl">Dashy</a> is free & open source
|
||||
- licensed under <a :href="defaultInfo.licenseUrl">{{defaultInfo.license}}</a>,
|
||||
© <a :href="defaultInfo.authorUrl">{{defaultInfo.authorName}}</a> {{defaultInfo.date}}.
|
||||
Get support on GitHub, at <a :href="defaultInfo.repoUrl">{{defaultInfo.repoName}}</a>.
|
||||
</span>
|
||||
</footer>
|
||||
</template>
|
||||
@@ -23,13 +20,20 @@ export default {
|
||||
name: 'Footer',
|
||||
props: {
|
||||
text: String,
|
||||
authorName: { type: String, default: 'Alicia Sykes' },
|
||||
authorUrl: { type: String, default: 'https://aliciasykes.com' },
|
||||
license: { type: String, default: 'MIT' },
|
||||
licenseUrl: { type: String, default: 'https://gist.github.com/Lissy93/143d2ee01ccc5c052a17' },
|
||||
date: { type: String, default: `${new Date().getFullYear()}` },
|
||||
showCopyright: { type: Boolean, default: true },
|
||||
repoUrl: { type: String, default: 'https://github.com/lissy93/dashy' },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaultInfo: {
|
||||
authorName: 'Alicia Sykes',
|
||||
authorUrl: 'https://as93.net',
|
||||
license: 'MIT',
|
||||
licenseUrl: 'https://gist.github.com/Lissy93/143d2ee01ccc5c052a17',
|
||||
date: `${new Date().getFullYear()}`,
|
||||
repoUrl: 'https://github.com/lissy93/dashy',
|
||||
repoName: 'Lissy93/Dashy',
|
||||
projectUrl: 'https://dashy.to',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
visible() {
|
||||
@@ -56,7 +60,7 @@ footer {
|
||||
display: none;
|
||||
}
|
||||
span.path-to-config {
|
||||
float: right;
|
||||
float: left;
|
||||
font-size: 0.75rem;
|
||||
margin: 0.1rem 0.5rem 0 0;
|
||||
opacity: var(--dimming-factor);
|
||||
|
||||
@@ -66,7 +66,7 @@ export default {
|
||||
span.subtitle {
|
||||
color: var(--heading-text-color);
|
||||
font-style: italic;
|
||||
text-shadow: 1px 1px 2px #130f23;
|
||||
text-shadow: 1px 1px 2px #130f2347;
|
||||
opacity: var(--dimming-factor);
|
||||
}
|
||||
img.site-logo {
|
||||
|
||||
@@ -319,6 +319,10 @@ div.action-buttons {
|
||||
min-width: 6rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 1rem 0.5rem 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,8 +37,10 @@ export default {
|
||||
input: '',
|
||||
};
|
||||
},
|
||||
props: {
|
||||
iconSize: String,
|
||||
computed: {
|
||||
iconSize() {
|
||||
return this.$store.getters.iconSize;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
IconSmall,
|
||||
|
||||
@@ -5,19 +5,19 @@
|
||||
<IconDeafault
|
||||
@click="updateDisplayLayout('auto')"
|
||||
v-tooltip="tooltip($t('settings.layout-auto'))"
|
||||
:class="`layout-icon ${displayLayout === 'auto' ? 'selected' : ''}`"
|
||||
:class="`layout-icon ${layout === 'auto' ? 'selected' : ''}`"
|
||||
tabindex="-2"
|
||||
/>
|
||||
<IconHorizontal
|
||||
@click="updateDisplayLayout('horizontal')"
|
||||
v-tooltip="tooltip($t('settings.layout-horizontal'))"
|
||||
:class="`layout-icon ${displayLayout === 'horizontal' ? 'selected' : ''}`"
|
||||
:class="`layout-icon ${layout === 'horizontal' ? 'selected' : ''}`"
|
||||
tabindex="-2"
|
||||
/>
|
||||
<IconVertical
|
||||
@click="updateDisplayLayout('vertical')"
|
||||
v-tooltip="tooltip($t('settings.layout-vertical'))"
|
||||
:class="`layout-icon ${displayLayout === 'vertical' ? 'selected' : ''}`"
|
||||
:class="`layout-icon ${layout === 'vertical' ? 'selected' : ''}`"
|
||||
tabindex="-2"
|
||||
/>
|
||||
</div>
|
||||
@@ -40,6 +40,11 @@ export default {
|
||||
IconHorizontal,
|
||||
IconVertical,
|
||||
},
|
||||
computed: {
|
||||
layout() {
|
||||
return this.$store.getters.layout;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateDisplayLayout(layout) {
|
||||
this.$store.commit(StoreKeys.SET_ITEM_LAYOUT, layout);
|
||||
|
||||
@@ -1,36 +1,70 @@
|
||||
<template>
|
||||
<transition name="slide-fade">
|
||||
<div class="kb-sc-info" v-if="!shouldHide">
|
||||
<h5>There are keyboard shortcuts! ⌨️🙌</h5>
|
||||
<h5>{{ popupContent.title }}</h5>
|
||||
<div class="close" title="Hide forever [Esc]" @click="hideWelcomeHelper()">x</div>
|
||||
<p title="Press [Esc] to hide this tip forever. See there's even a shortcut for that! 🚀">
|
||||
Just start typing to filter. Then use the tab key to cycle through results,
|
||||
and press enter to launch the selected item, or alt + enter to open in a modal.
|
||||
You can hit Esc at anytime to clear the search. Easy 🥳
|
||||
</p>
|
||||
<p :title="popupContent.hoverText">{{ popupContent.message }}</p>
|
||||
<p :title="popupContent.hoverText">{{ popupContent.messageContinued }}</p>
|
||||
<div class="action-buttons">
|
||||
<button @click="exportConfig">Export Local Config</button>
|
||||
<button @click="saveConfig">Save Changes to Disk</button>
|
||||
<button @click="resetLocalConfig">Reset Local Changes</button>
|
||||
<button @click="hideWelcomeHelper">Dismiss this Notification</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { localStorageKeys } from '@/utils/defaults';
|
||||
import { localStorageKeys, modalNames } from '@/utils/defaults';
|
||||
import StoreKeys from '@/utils/StoreMutations';
|
||||
import configSavingMixin from '@/mixins/ConfigSaving';
|
||||
|
||||
export default {
|
||||
name: 'KeyboardShortcutInfo',
|
||||
mixins: [configSavingMixin],
|
||||
data() {
|
||||
return {
|
||||
shouldHide: true, // False = show/ true = hide. Intuitive, eh?
|
||||
timeDelay: 3000, // Short delay in ms before popup appears
|
||||
timeDelay: 2000, // Short delay in ms before popup appears
|
||||
popupContent: {
|
||||
title: '⚠️ You\'re using a local config',
|
||||
message: `This means that your settings are saved in this browser only,
|
||||
and won't persist across devices.`,
|
||||
messageContinued: `To ensure you don't loose your changes,
|
||||
it's recommended to download a copy of your config, so you can restore it later.`,
|
||||
hoverText: 'Press [Esc] to hide this warning',
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
exportConfig() {
|
||||
this.$modal.show(modalNames.EXPORT_CONFIG_MENU);
|
||||
this.shouldHide = true;
|
||||
},
|
||||
saveConfig() {
|
||||
const localConfig = this.$store.state.config;
|
||||
this.writeConfigToDisk(localConfig);
|
||||
this.shouldHide = true;
|
||||
},
|
||||
resetLocalConfig() {
|
||||
const msg = `${this.$t('config.reset-config-msg-l1')} `
|
||||
+ `${this.$t('config.reset-config-msg-l2')}\n\n${this.$t('config.reset-config-msg-l3')}`;
|
||||
const isTheUserSure = confirm(msg); // eslint-disable-line no-alert, no-restricted-globals
|
||||
if (isTheUserSure) {
|
||||
localStorage.clear();
|
||||
this.$toasted.show(this.$t('config.data-cleared-msg'));
|
||||
this.$store.dispatch(StoreKeys.INITIALIZE_CONFIG);
|
||||
this.shouldHide = true;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Returns true if the key exists in session storage, otherwise false
|
||||
* And the !! just converts 'false' to false, as strings resolve to true
|
||||
*/
|
||||
shouldHideWelcomeMessage() {
|
||||
return !!localStorage[localStorageKeys.HIDE_WELCOME_BANNER];
|
||||
return !!localStorage[localStorageKeys.HIDE_INFO_NOTIFICATION];
|
||||
},
|
||||
/**
|
||||
* Update session storage, so that it won't be shown again
|
||||
@@ -38,7 +72,7 @@ export default {
|
||||
*/
|
||||
hideWelcomeHelper() {
|
||||
this.shouldHide = true;
|
||||
localStorage.setItem(localStorageKeys.HIDE_WELCOME_BANNER, true);
|
||||
localStorage.setItem(localStorageKeys.HIDE_INFO_NOTIFICATION, true);
|
||||
window.removeEventListener('keyup', this.keyPressEvent);
|
||||
},
|
||||
/* Passed to window function, to add/ remove event listener */
|
||||
@@ -114,6 +148,23 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 1em;
|
||||
button {
|
||||
padding: 0.2rem;
|
||||
background: var(--welcome-popup-background);
|
||||
color: var(--welcome-popup-text-color);
|
||||
border: 1px solid var(--welcome-popup-text-color);
|
||||
border-radius: var(--curve-factor);
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background: var(--welcome-popup-text-color);
|
||||
color: var(--welcome-popup-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Animations, animations everywhere */
|
||||
.slide-fade-enter-active {
|
||||
transition: all 1s ease;
|
||||
@@ -95,7 +95,8 @@ export default {
|
||||
},
|
||||
/* If configured, launch specific app when hotkey pressed */
|
||||
handleHotKey(key) {
|
||||
const usersHotKeys = this.getCustomKeyShortcuts();
|
||||
const sections = this.$store.getters.sections || [];
|
||||
const usersHotKeys = this.getCustomKeyShortcuts(sections);
|
||||
usersHotKeys.forEach((hotkey) => {
|
||||
if (hotkey.hotkey === parseInt(key, 10)) {
|
||||
if (hotkey.url) window.open(hotkey.url, '_blank');
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="options-outer">
|
||||
<div :class="`options-container ${!settingsVisible ? 'hide' : ''}`">
|
||||
<ThemeSelector />
|
||||
<LayoutSelector :displayLayout="displayLayout" />
|
||||
<LayoutSelector :displayLayout="$store.getters.layout" />
|
||||
<ItemSizeSelector :iconSize="iconSize" />
|
||||
<ConfigLauncher />
|
||||
<AuthButtons v-if="userState !== 0" :userType="userState" />
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
:value="$store.getters.theme"
|
||||
class="theme-dropdown"
|
||||
:tabindex="-2"
|
||||
@input="themeChanged"
|
||||
@input="themeChangedInUI"
|
||||
/>
|
||||
</div>
|
||||
<IconPalette
|
||||
@@ -28,18 +28,13 @@
|
||||
<script>
|
||||
|
||||
import CustomThemeMaker from '@/components/Settings/CustomThemeMaker';
|
||||
import {
|
||||
LoadExternalTheme,
|
||||
ApplyLocalTheme,
|
||||
ApplyCustomVariables,
|
||||
} from '@/utils/ThemeHelper';
|
||||
import Defaults, { localStorageKeys } from '@/utils/defaults';
|
||||
import Keys from '@/utils/StoreMutations';
|
||||
import ErrorHandler from '@/utils/ErrorHandler';
|
||||
import IconPalette from '@/assets/interface-icons/config-color-palette.svg';
|
||||
import ThemingMixin from '@/mixins/ThemingMixin';
|
||||
|
||||
export default {
|
||||
name: 'ThemeSelector',
|
||||
mixins: [ThemingMixin],
|
||||
props: {
|
||||
hidePallete: Boolean,
|
||||
},
|
||||
@@ -47,101 +42,16 @@ export default {
|
||||
CustomThemeMaker,
|
||||
IconPalette,
|
||||
},
|
||||
watch: {
|
||||
/* When theme in VueX store changes, then update theme */
|
||||
themeFromStore(newTheme) {
|
||||
this.selectedTheme = newTheme;
|
||||
this.updateTheme(newTheme);
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTheme: '',
|
||||
themeConfiguratorOpen: false, // Control the opening of theme config popup
|
||||
themeHelper: new LoadExternalTheme(),
|
||||
ApplyLocalTheme,
|
||||
ApplyCustomVariables,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/* Get appConfig from store */
|
||||
appConfig() {
|
||||
return this.$store.getters.appConfig;
|
||||
},
|
||||
/* Get users theme from store */
|
||||
themeFromStore() {
|
||||
return this.$store.getters.theme;
|
||||
},
|
||||
/* Combines all theme names (builtin and user defined) together */
|
||||
themeNames: function themeNames() {
|
||||
const externalThemeNames = Object.keys(this.externalThemes);
|
||||
const specialThemes = ['custom'];
|
||||
return [...this.extraThemeNames, ...externalThemeNames,
|
||||
...Defaults.builtInThemes, ...specialThemes];
|
||||
},
|
||||
extraThemeNames() {
|
||||
const userThemes = this.appConfig.cssThemes || [];
|
||||
if (typeof userThemes === 'string') return [userThemes];
|
||||
return userThemes;
|
||||
},
|
||||
/* Returns an array of links to external CSS from the Config */
|
||||
externalThemes() {
|
||||
const availibleThemes = {};
|
||||
if (this.appConfig && this.appConfig.externalStyleSheet) {
|
||||
const externals = this.appConfig.externalStyleSheet;
|
||||
if (Array.isArray(externals)) {
|
||||
externals.forEach((ext, i) => {
|
||||
availibleThemes[`External Stylesheet ${i + 1}`] = ext;
|
||||
});
|
||||
} else if (typeof externals === 'string') {
|
||||
availibleThemes['External Stylesheet'] = this.appConfig.externalStyleSheet;
|
||||
} else {
|
||||
ErrorHandler('External stylesheets must be of type string or string[]');
|
||||
}
|
||||
}
|
||||
// availibleThemes.Default = '#';
|
||||
return availibleThemes;
|
||||
},
|
||||
},
|
||||
computed: {},
|
||||
mounted() {
|
||||
const initialTheme = this.getInitialTheme();
|
||||
this.selectedTheme = initialTheme;
|
||||
// Quicker loading, if the theme is local we can apply it immidiatley
|
||||
if (this.isThemeLocal(initialTheme)) {
|
||||
this.updateTheme(initialTheme);
|
||||
}
|
||||
|
||||
// If it's an external stylesheet, then wait for promise to resolve
|
||||
if (this.externalThemes && Object.entries(this.externalThemes).length > 0) {
|
||||
const added = Object.keys(this.externalThemes).map(
|
||||
name => this.themeHelper.add(name, this.externalThemes[name]),
|
||||
);
|
||||
// Once, added, then apply users initial theme
|
||||
Promise.all(added).then(() => {
|
||||
this.updateTheme(initialTheme);
|
||||
});
|
||||
}
|
||||
this.initializeTheme();
|
||||
},
|
||||
methods: {
|
||||
/* Called when dropdown changed
|
||||
* Updates store, which will in turn update theme through watcher
|
||||
*/
|
||||
themeChanged() {
|
||||
const pageId = this.$store.state.currentConfigInfo?.pageId || null;
|
||||
this.$store.commit(Keys.SET_THEME, { theme: this.selectedTheme, pageId });
|
||||
this.updateTheme(this.selectedTheme);
|
||||
},
|
||||
/* Returns the initial theme */
|
||||
getInitialTheme() {
|
||||
const localTheme = localStorage[localStorageKeys.THEME];
|
||||
if (localTheme && localTheme !== 'undefined') return localTheme;
|
||||
return this.appConfig.theme || Defaults.theme;
|
||||
},
|
||||
/* Determines if a given theme is local / not a custom user stylesheet */
|
||||
isThemeLocal(themeToCheck) {
|
||||
const localThemes = [...Defaults.builtInThemes, ...this.extraThemeNames];
|
||||
return localThemes.includes(themeToCheck);
|
||||
},
|
||||
/* Opens the theme color configurator popup */
|
||||
openThemeConfigurator() {
|
||||
this.$store.commit(Keys.SET_MODAL_OPEN, true);
|
||||
@@ -154,24 +64,6 @@ export default {
|
||||
this.themeConfiguratorOpen = false;
|
||||
}
|
||||
},
|
||||
/* Updates theme. Checks if the new theme is local or external,
|
||||
and calls appropirate updating function. Updates local storage */
|
||||
updateTheme(newTheme) {
|
||||
if (newTheme === 'Default') {
|
||||
this.resetToDefault();
|
||||
this.themeHelper.theme = 'Default';
|
||||
} else if (this.isThemeLocal(newTheme)) {
|
||||
this.ApplyLocalTheme(newTheme);
|
||||
} else {
|
||||
this.themeHelper.theme = newTheme;
|
||||
}
|
||||
this.ApplyCustomVariables(newTheme);
|
||||
// localStorage.setItem(localStorageKeys.THEME, newTheme);
|
||||
},
|
||||
/* Removes any applied themes */
|
||||
resetToDefault() {
|
||||
document.getElementsByTagName('html')[0].removeAttribute('data-theme');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -26,7 +26,7 @@ export default {
|
||||
/* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */
|
||||
hostname() {
|
||||
if (!this.options.hostname) this.error('You must specify the path to your AdGuard server');
|
||||
return this.options.hostname;
|
||||
return this.parseAsEnvVar(this.options.hostname);
|
||||
},
|
||||
showFullInfo() {
|
||||
return this.options.showFullInfo;
|
||||
@@ -39,7 +39,9 @@ export default {
|
||||
},
|
||||
authHeaders() {
|
||||
if (this.options.username && this.options.password) {
|
||||
const encoded = window.btoa(`${this.options.username}:${this.options.password}`);
|
||||
const password = this.parseAsEnvVar(this.options.password);
|
||||
const username = this.parseAsEnvVar(this.options.username);
|
||||
const encoded = window.btoa(`${username}:${password}`);
|
||||
return { Authorization: `Basic ${encoded}` };
|
||||
}
|
||||
return {};
|
||||
|
||||
@@ -38,7 +38,7 @@ export default {
|
||||
/* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */
|
||||
hostname() {
|
||||
if (!this.options.hostname) this.error('You must specify the path to your AdGuard server');
|
||||
return this.options.hostname;
|
||||
return this.parseAsEnvVar(this.options.hostname);
|
||||
},
|
||||
showOnOffStatusOnly() {
|
||||
return this.options.showOnOffStatusOnly;
|
||||
@@ -48,7 +48,9 @@ export default {
|
||||
},
|
||||
authHeaders() {
|
||||
if (this.options.username && this.options.password) {
|
||||
const encoded = window.btoa(`${this.options.username}:${this.options.password}`);
|
||||
const username = this.parseAsEnvVar(this.options.username);
|
||||
const password = this.parseAsEnvVar(this.options.password);
|
||||
const encoded = window.btoa(`${username}:${password}`);
|
||||
return { Authorization: `Basic ${encoded}` };
|
||||
}
|
||||
return {};
|
||||
|
||||
@@ -20,14 +20,16 @@ export default {
|
||||
/* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */
|
||||
hostname() {
|
||||
if (!this.options.hostname) this.error('You must specify the path to your AdGuard server');
|
||||
return this.options.hostname;
|
||||
return this.parseAsEnvVar(this.options.hostname);
|
||||
},
|
||||
endpoint() {
|
||||
return `${this.hostname}/control/stats`;
|
||||
},
|
||||
authHeaders() {
|
||||
if (this.options.username && this.options.password) {
|
||||
const encoded = window.btoa(`${this.options.username}:${this.options.password}`);
|
||||
const username = this.parseAsEnvVar(this.options.username);
|
||||
const password = this.parseAsEnvVar(this.options.password);
|
||||
const encoded = window.btoa(`${username}:${password}`);
|
||||
return { Authorization: `Basic ${encoded}` };
|
||||
}
|
||||
return {};
|
||||
|
||||
@@ -36,11 +36,13 @@ export default {
|
||||
/* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */
|
||||
hostname() {
|
||||
if (!this.options.hostname) this.error('You must specify the path to your AdGuard server');
|
||||
return this.options.hostname;
|
||||
return this.parseAsEnvVar(this.options.hostname);
|
||||
},
|
||||
authHeaders() {
|
||||
if (this.options.username && this.options.password) {
|
||||
const encoded = window.btoa(`${this.options.username}:${this.options.password}`);
|
||||
const username = this.parseAsEnvVar(this.options.username);
|
||||
const password = this.parseAsEnvVar(this.options.password);
|
||||
const encoded = window.btoa(`${username}:${password}`);
|
||||
return { Authorization: `Basic ${encoded}` };
|
||||
}
|
||||
return {};
|
||||
|
||||
@@ -113,7 +113,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
hostname() {
|
||||
return this.options.hostname || widgetApiEndpoints.anonAddy;
|
||||
return this.parseAsEnvVar(this.options.hostname) || widgetApiEndpoints.anonAddy;
|
||||
},
|
||||
apiVersion() {
|
||||
return this.options.apiVersion || 'v1';
|
||||
@@ -132,7 +132,7 @@ export default {
|
||||
},
|
||||
apiKey() {
|
||||
if (!this.options.apiKey) this.error('An apiKey is required');
|
||||
return this.options.apiKey;
|
||||
return this.parseAsEnvVar(this.options.apiKey);
|
||||
},
|
||||
hideMeta() {
|
||||
return this.options.hideMeta;
|
||||
|
||||
@@ -35,7 +35,7 @@ export default {
|
||||
},
|
||||
apiKey() {
|
||||
if (!this.options.apiKey) this.error('Missing API Key');
|
||||
return this.options.apiKey;
|
||||
return this.parseAsEnvVar(this.options.apiKey);
|
||||
},
|
||||
endpoint() {
|
||||
return `${widgetApiEndpoints.blacklistCheck}/${this.ipAddress}`;
|
||||
|
||||
@@ -38,12 +38,12 @@ export default {
|
||||
/* The username to fetch data from - REQUIRED */
|
||||
username() {
|
||||
if (!this.options.username) this.error('You must specify a username');
|
||||
return this.options.username;
|
||||
return this.parseAsEnvVar(this.options.username);
|
||||
},
|
||||
/* Optionally override hostname, if using a self-hosted instance */
|
||||
hostname() {
|
||||
if (this.options.hostname) return this.options.hostname;
|
||||
return widgetApiEndpoints.codeStats;
|
||||
return this.parseAsEnvVar(widgetApiEndpoints.codeStats);
|
||||
},
|
||||
hideMeta() {
|
||||
return this.options.hideMeta || false;
|
||||
|
||||
@@ -63,11 +63,11 @@ export default {
|
||||
computed: {
|
||||
apiKey() {
|
||||
if (!this.options.apiKey) this.error('Missing API Key');
|
||||
return this.options.apiKey;
|
||||
return this.parseAsEnvVar(this.options.apiKey);
|
||||
},
|
||||
domain() {
|
||||
if (!this.options.domain) this.error('Missing Domain Name Key');
|
||||
return this.options.domain;
|
||||
return this.parseAsEnvVar(this.options.domain);
|
||||
},
|
||||
endpoint() {
|
||||
return `${widgetApiEndpoints.domainMonitor}/?domain=${this.domain}&r=whois&apikey=${this.apiKey}`;
|
||||
|
||||
@@ -106,7 +106,7 @@ export default {
|
||||
if (!this.options.apiKey) {
|
||||
this.error('An API key is required, please see the docs for more info');
|
||||
}
|
||||
return this.options.apiKey;
|
||||
return this.parseAsEnvVar(this.options.apiKey);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -45,7 +45,7 @@ export default {
|
||||
computed: {
|
||||
/* The users API key for exchangerate-api.com */
|
||||
apiKey() {
|
||||
return this.options.apiKey;
|
||||
return this.parseAsEnvVar(this.options.apiKey);
|
||||
},
|
||||
/* The currency to convert results into */
|
||||
inputCurrency() {
|
||||
|
||||
@@ -71,7 +71,7 @@ export default {
|
||||
this.error('An API key must be supplied');
|
||||
return '';
|
||||
}
|
||||
return usersChoice;
|
||||
return this.parseAsEnvVar(usersChoice);
|
||||
},
|
||||
/* The direction of flights: Arrival, Departure or Both */
|
||||
direction() {
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
<template>
|
||||
<div class="readme-stats">
|
||||
<img class="stats-card" v-if="!hideProfileCard" :src="profileCard" alt="Profile Card" />
|
||||
<img class="stats-card" v-if="!hideLanguagesCard" :src="topLanguagesCard" alt="Languages" />
|
||||
<a v-if="!hideProfileCard" :href="profileCardLink" target="_blank">
|
||||
<img class="stats-card" :src="profileCard" alt="Profile Card" />
|
||||
</a>
|
||||
<a v-if="!hideLanguagesCard" :href="profileCardLink" target="_blank">
|
||||
<img class="stats-card" :src="topLanguagesCard" alt="Languages" />
|
||||
</a>
|
||||
<template v-if="repos">
|
||||
<img class="stats-card" v-for="(repo, i) in repoCards" :key="i" :src="repo" :alt="repo" />
|
||||
<a v-for="(repo, i) in repoCards" :key="i" :href="repo.cardHref" target="_blank">
|
||||
<img class="stats-card" :src="repo.cardSrc" :alt="repo" />
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -61,6 +67,9 @@ export default {
|
||||
profileCard() {
|
||||
return `${widgetApiEndpoints.readMeStats}?username=${this.username}${this.cardConfig}`;
|
||||
},
|
||||
profileCardLink() {
|
||||
return `https://github.com/${this.username}`;
|
||||
},
|
||||
topLanguagesCard() {
|
||||
return `${widgetApiEndpoints.readMeStats}/top-langs/?username=${this.username}`
|
||||
+ `${this.cardConfig}&langs_count=12`;
|
||||
@@ -70,8 +79,11 @@ export default {
|
||||
this.repos.forEach((repo) => {
|
||||
const username = repo.split('/')[0];
|
||||
const repoName = repo.split('/')[1];
|
||||
cards.push(`${widgetApiEndpoints.readMeStats}/pin/?username=${username}&repo=${repoName}`
|
||||
+ `${this.cardConfig}&show_owner=true`);
|
||||
cards.push({
|
||||
cardSrc: `${widgetApiEndpoints.readMeStats}/pin/?username=${username}`
|
||||
+ `&repo=${repoName}${this.cardConfig}&show_owner=true`,
|
||||
cardHref: `https://github.com/${username}/${repoName}`,
|
||||
});
|
||||
});
|
||||
return cards;
|
||||
},
|
||||
|
||||
@@ -112,6 +112,7 @@ export default {
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
width: fit-content;
|
||||
margin: 0.25rem auto;
|
||||
padding: 0.1rem 0.25rem;
|
||||
|
||||
150
src/components/Widgets/GlCpuSpeedometer.vue
Normal file
150
src/components/Widgets/GlCpuSpeedometer.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div class="glances-cpu-gauge-wrapper">
|
||||
<GaugeChart class="gl-speedometer" :value="gaugeValue"
|
||||
:baseColor="baseColor" :shadowColor="shadowColor" :gaugeColor="gaugeColor"
|
||||
:startAngle="startAngle" :endAngle="endAngle" :innerRadius="innerRadius"
|
||||
:separatorThickness="separatorThickness">
|
||||
<p class="percentage">{{ gaugeValue }}%</p>
|
||||
</GaugeChart>
|
||||
<p class="show-more-btn" @click="toggleMoreInfo">
|
||||
{{ showMoreInfo ? $t('widgets.general.show-less') : $t('widgets.general.cpu-details') }}
|
||||
</p>
|
||||
<div class="more-info" v-if="moreInfo && showMoreInfo">
|
||||
<div class="more-info-row" v-for="(info, key) in moreInfo" :key="key">
|
||||
<p class="label">{{ info.label }}</p>
|
||||
<p class="value">{{ info.value }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
import GlancesMixin from '@/mixins/GlancesMixin';
|
||||
import GaugeChart from '@/components/Charts/Gauge';
|
||||
import { capitalize } from '@/utils/MiscHelpers';
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin, GlancesMixin],
|
||||
components: {
|
||||
GaugeChart,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
gaugeValue: 0,
|
||||
baseColor: '#101010ED',
|
||||
shadowColor: '#00000000',
|
||||
gaugeColor: [
|
||||
{ offset: 0, color: '#20e253' },
|
||||
{ offset: 35, color: '#f6f000' },
|
||||
{ offset: 65, color: '#fca016' },
|
||||
{ offset: 90, color: '#f80363' },
|
||||
],
|
||||
showMoreInfo: false,
|
||||
moreInfo: null,
|
||||
startAngle: -135,
|
||||
endAngle: 135,
|
||||
innerRadius: 80,
|
||||
separatorThickness: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
endpoint() {
|
||||
return this.makeGlancesUrl('cpu');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
processData(cpuData) {
|
||||
this.gaugeValue = cpuData.total;
|
||||
const moreInfo = [];
|
||||
const ignore = ['total', 'cpucore', 'time_since_update',
|
||||
'interrupts', 'soft_interrupts', 'ctx_switches', 'syscalls'];
|
||||
Object.keys(cpuData).forEach((key) => {
|
||||
if (!ignore.includes(key) && cpuData[key]) {
|
||||
moreInfo.push({ label: capitalize(key), value: `${cpuData[key].toFixed(1)}%` });
|
||||
}
|
||||
});
|
||||
this.moreInfo = moreInfo;
|
||||
},
|
||||
toggleMoreInfo() {
|
||||
this.showMoreInfo = !this.showMoreInfo;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.overrideUpdateInterval = 2;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.glances-cpu-gauge-wrapper {
|
||||
max-width: 15rem;
|
||||
margin: 0rem auto;
|
||||
|
||||
p.percentage {
|
||||
color: var(--widget-text-color);
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
font-size: 1.3rem;
|
||||
margin: 3.5rem 0;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.more-info {
|
||||
background: var(--widget-accent-color);
|
||||
border-radius: var(--curve-factor);
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0.5rem auto;
|
||||
|
||||
.more-info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
p.label, p.value {
|
||||
color: var(--widget-text-color);
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
p.value {
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px dashed var(--widget-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.show-more-btn {
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
width: fit-content;
|
||||
margin: -1.1rem auto 0 auto;
|
||||
padding: 0.1rem 0.25rem;
|
||||
border: 1px solid transparent;
|
||||
color: var(--widget-text-color);
|
||||
opacity: var(--dimming-factor);
|
||||
border-radius: var(--curve-factor);
|
||||
|
||||
&:hover {
|
||||
border: 1px solid var(--widget-text-color);
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:active {
|
||||
background: var(--widget-text-color);
|
||||
color: var(--widget-background-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
/* global override for the Guage tick lines */
|
||||
.gl-speedometer svg line {
|
||||
stroke: var(--widget-text-color);
|
||||
opacity: .3;
|
||||
}
|
||||
</style>
|
||||
150
src/components/Widgets/GlMemSpeedometer.vue
Normal file
150
src/components/Widgets/GlMemSpeedometer.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div class="glances-cpu-gauge-wrapper">
|
||||
<GaugeChart class="gl-speedometer" :value="gaugeValue"
|
||||
:baseColor="baseColor" :shadowColor="shadowColor" :gaugeColor="gaugeColor"
|
||||
:startAngle="startAngle" :endAngle="endAngle" :innerRadius="innerRadius"
|
||||
:separatorThickness="separatorThickness">
|
||||
<p class="percentage">{{ gaugeValue }}%</p>
|
||||
</GaugeChart>
|
||||
<p class="show-more-btn" @click="toggleMoreInfo">
|
||||
{{ showMoreInfo ? $t('widgets.general.show-less') : $t('widgets.general.mem-details') }}
|
||||
</p>
|
||||
<div class="more-info" v-if="moreInfo && showMoreInfo">
|
||||
<div class="more-info-row" v-for="(info, key) in moreInfo" :key="key">
|
||||
<p class="label">{{ info.label }}</p>
|
||||
<p class="value">{{ info.value }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
import GlancesMixin from '@/mixins/GlancesMixin';
|
||||
import GaugeChart from '@/components/Charts/Gauge';
|
||||
import { capitalize, convertBytes } from '@/utils/MiscHelpers';
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin, GlancesMixin],
|
||||
components: {
|
||||
GaugeChart,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
gaugeValue: 0,
|
||||
baseColor: '#101010ED',
|
||||
shadowColor: '#00000000',
|
||||
gaugeColor: [
|
||||
{ offset: 0, color: '#20e253' },
|
||||
{ offset: 35, color: '#f6f000' },
|
||||
{ offset: 65, color: '#fca016' },
|
||||
{ offset: 90, color: '#f80363' },
|
||||
],
|
||||
showMoreInfo: false,
|
||||
moreInfo: null,
|
||||
startAngle: -135,
|
||||
endAngle: 135,
|
||||
innerRadius: 80,
|
||||
separatorThickness: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
endpoint() {
|
||||
return this.makeGlancesUrl('mem');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
processData(memData) {
|
||||
this.gaugeValue = memData.percent;
|
||||
const moreInfo = [];
|
||||
const ignore = ['percent'];
|
||||
Object.keys(memData).forEach((key) => {
|
||||
if (!ignore.includes(key) && memData[key]) {
|
||||
moreInfo.push({ label: capitalize(key), value: convertBytes(memData[key]) });
|
||||
}
|
||||
});
|
||||
this.moreInfo = moreInfo;
|
||||
},
|
||||
toggleMoreInfo() {
|
||||
this.showMoreInfo = !this.showMoreInfo;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.overrideUpdateInterval = 2;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.glances-cpu-gauge-wrapper {
|
||||
max-width: 15rem;
|
||||
margin: 0rem auto;
|
||||
|
||||
p.percentage {
|
||||
color: var(--widget-text-color);
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
font-size: 1.3rem;
|
||||
margin: 3.5rem 0;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.more-info {
|
||||
background: var(--widget-accent-color);
|
||||
border-radius: var(--curve-factor);
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0.5rem auto;
|
||||
|
||||
.more-info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
p.label,
|
||||
p.value {
|
||||
color: var(--widget-text-color);
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
p.value {
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px dashed var(--widget-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.show-more-btn {
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
width: fit-content;
|
||||
margin: -1.1rem auto 0 auto;
|
||||
padding: 0.1rem 0.25rem;
|
||||
border: 1px solid transparent;
|
||||
color: var(--widget-text-color);
|
||||
opacity: var(--dimming-factor);
|
||||
border-radius: var(--curve-factor);
|
||||
|
||||
&:hover {
|
||||
border: 1px solid var(--widget-text-color);
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:active {
|
||||
background: var(--widget-text-color);
|
||||
color: var(--widget-background-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
/* global override for the Guage tick lines */
|
||||
.gl-speedometer svg line {
|
||||
stroke: var(--widget-text-color);
|
||||
opacity: .3;
|
||||
}
|
||||
</style>
|
||||
@@ -58,7 +58,7 @@ export default {
|
||||
},
|
||||
hostname() {
|
||||
if (!this.options.hostname) this.error('`hostname` is required');
|
||||
return this.options.hostname;
|
||||
return this.parseAsEnvVar(this.options.hostname);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -56,7 +56,7 @@ export default {
|
||||
this.error('An API key is required, please see the docs for more info');
|
||||
}
|
||||
if (typeof this.options.apiKey === 'string') {
|
||||
return [this.options.apiKey];
|
||||
return [this.parseAsEnvVar(this.options.apiKey)];
|
||||
}
|
||||
return this.options.apiKey;
|
||||
},
|
||||
|
||||
@@ -74,6 +74,7 @@ export default {
|
||||
this.jokeLine2 = data.delivery;
|
||||
} else if (this.jokeType === 'single') {
|
||||
this.jokeLine1 = data.joke;
|
||||
this.jokeLine2 = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -30,11 +30,11 @@ export default {
|
||||
computed: {
|
||||
endpoint() {
|
||||
if (!this.options.host) this.error('linkgding Host is required');
|
||||
return `${this.options.host}/api/bookmarks`;
|
||||
return `${this.parseAsEnvVar(this.options.host)}/api/bookmarks`;
|
||||
},
|
||||
apiKey() {
|
||||
if (!this.options.apiKey) this.error('linkgding apiKey is required');
|
||||
return this.options.apiKey;
|
||||
return this.parseAsEnvVar(this.options.apiKey);
|
||||
},
|
||||
filtertags() {
|
||||
return this.options.tags;
|
||||
|
||||
@@ -29,7 +29,7 @@ export default {
|
||||
computed: {
|
||||
apiKey() {
|
||||
if (!this.options.apiKey) this.error('An API key is required, see docs for more info');
|
||||
return this.options.apiKey;
|
||||
return this.parseAsEnvVar(this.options.apiKey);
|
||||
},
|
||||
country() {
|
||||
return this.options.country ? `&country=${this.options.country}` : '';
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</span>
|
||||
<span v-if="canDeleteNotification('delete')">
|
||||
<a @click="deleteNotification(notification.notification_id)"
|
||||
class="action secondary">{{ tt('delete-notification') }}</a>
|
||||
class="action secondary">{{ tt('delete-notification') }}</a>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<em v-html="formatNumber(shares.num_shares)"></em>
|
||||
<strong>{{ tt('local') }}</strong> <small> {{ tt('and') }}</small>
|
||||
<em v-html="formatNumber(shares.num_fed_shares_sent
|
||||
+ shares.num_fed_shares_received)"></em>
|
||||
+ shares.num_fed_shares_received)"></em>
|
||||
<strong>
|
||||
{{ tt('federated-shares') }}
|
||||
</strong>
|
||||
|
||||
@@ -36,13 +36,14 @@ export default {
|
||||
computed: {
|
||||
/* Let user select which comic to display: random, latest or a specific number */
|
||||
hostname() {
|
||||
const usersChoice = this.options.hostname;
|
||||
const usersChoice = this.parseAsEnvVar(this.options.hostname);
|
||||
if (!usersChoice) this.error('You must specify the hostname for your Pi-Hole server');
|
||||
return usersChoice || 'http://pi.hole';
|
||||
},
|
||||
apiKey() {
|
||||
if (!this.options.apiKey) this.error('API Key is required, please see the docs');
|
||||
return this.options.apiKey;
|
||||
const usersChoice = this.parseAsEnvVar(this.options.apiKey);
|
||||
if (!usersChoice) this.error('API Key is required, please see the docs');
|
||||
return usersChoice;
|
||||
},
|
||||
endpoint() {
|
||||
return `${this.hostname}/admin/api.php?summary&auth=${this.apiKey}`;
|
||||
|
||||
@@ -34,22 +34,22 @@ export default {
|
||||
computed: {
|
||||
clusterUrl() {
|
||||
if (!this.options.cluster_url) this.error('The cluster URL is required.');
|
||||
return this.options.cluster_url || '';
|
||||
return this.parseAsEnvVar(this.options.cluster_url) || '';
|
||||
},
|
||||
userName() {
|
||||
if (!this.options.user_name) this.error('The user name is required.');
|
||||
return this.options.user_name || '';
|
||||
return this.parseAsEnvVar(this.options.user_name) || '';
|
||||
},
|
||||
tokenName() {
|
||||
if (!this.options.token_name) this.error('The token name is required.');
|
||||
return this.options.token_name || '';
|
||||
return this.parseAsEnvVar(this.options.token_name) || '';
|
||||
},
|
||||
tokenUuid() {
|
||||
if (!this.options.token_uuid) this.error('The token uuid is required.');
|
||||
return this.options.token_uuid || '';
|
||||
return this.parseAsEnvVar(this.options.token_uuid) || '';
|
||||
},
|
||||
node() {
|
||||
return this.options.node || '';
|
||||
return this.parseAsEnvVar(this.options.node) || '';
|
||||
},
|
||||
nodeData() {
|
||||
return this.options.node_data || false;
|
||||
@@ -94,7 +94,7 @@ export default {
|
||||
}
|
||||
},
|
||||
processData(data) {
|
||||
this.data = data.data.sort((a, b) => a.vmid > b.vmid);
|
||||
this.data = data.data.sort((a, b) => Number(a.vmid) > Number(b.vmid));
|
||||
if (this.hideTemplates) {
|
||||
this.data = this.data.filter(item => item.template !== 1);
|
||||
}
|
||||
|
||||
@@ -90,7 +90,8 @@ export default {
|
||||
const formatType = (ht) => capitalize(ht.replaceAll('_', ' '));
|
||||
holidays.forEach((holiday) => {
|
||||
results.push({
|
||||
name: holiday.name.filter(p => p.lang == this.options.lang)[0].text || holiday.name[0].text,
|
||||
name: holiday.name
|
||||
.filter(p => p.lang === this.options.lang)[0].text || holiday.name[0].text,
|
||||
date: makeDate(holiday.date),
|
||||
type: formatType(holiday.holidayType),
|
||||
observed: holiday.observedOn ? makeDate(holiday.observedOn) : '',
|
||||
|
||||
@@ -35,7 +35,7 @@ export default {
|
||||
},
|
||||
provider() {
|
||||
// Can be either `ip-api`, `ipapi.co` or `ipgeolocation`
|
||||
return this.options.provider || 'ipapi.co';
|
||||
return this.parseAsEnvVar(this.options.provider) || 'ipapi.co';
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -51,7 +51,7 @@ export default {
|
||||
return this.options.rssUrl || '';
|
||||
},
|
||||
apiKey() {
|
||||
return this.options.apiKey;
|
||||
return this.parseAsEnvVar(this.options.apiKey);
|
||||
},
|
||||
parseLocally() {
|
||||
return this.options.parseLocally;
|
||||
|
||||
@@ -93,7 +93,7 @@ export default {
|
||||
return this.options.leagueId;
|
||||
},
|
||||
apiKey() {
|
||||
return this.options.apiKey || '50130162';
|
||||
return this.parseAsEnvVar(this.options.apiKey) || '50130162';
|
||||
},
|
||||
limit() {
|
||||
return this.options.limit || 20;
|
||||
|
||||
@@ -29,7 +29,7 @@ export default {
|
||||
},
|
||||
/* The users API key for AlphaVantage */
|
||||
apiKey() {
|
||||
return this.options.apiKey;
|
||||
return this.parseAsEnvVar(this.options.apiKey);
|
||||
},
|
||||
/* The formatted GET request API endpoint to fetch stock data from */
|
||||
endpoint() {
|
||||
|
||||
@@ -45,15 +45,15 @@ export default {
|
||||
computed: {
|
||||
hostname() {
|
||||
if (!this.options.hostname) this.error('A hostname is required');
|
||||
return this.options.hostname;
|
||||
return this.parseAsEnvVar(this.options.hostname);
|
||||
},
|
||||
username() {
|
||||
if (!this.options.username) this.error('A username is required');
|
||||
return this.options.username;
|
||||
return this.parseAsEnvVar(this.options.username);
|
||||
},
|
||||
password() {
|
||||
if (!this.options.password) this.error('A password is required');
|
||||
return this.options.password;
|
||||
return this.parseAsEnvVar(this.options.password);
|
||||
},
|
||||
endpointLogin() {
|
||||
return `${this.hostname}/webapi/auth.cgi?api=SYNO.API.Auth&version=3&method=login&account=${this.username}&passwd=${this.password}&session=DownloadStation&format=sid`;
|
||||
|
||||
238
src/components/Widgets/UptimeKuma.vue
Normal file
238
src/components/Widgets/UptimeKuma.vue
Normal file
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="monitors">
|
||||
<div v-for="(monitor, index) in monitors" :key="index" class="item-wrapper">
|
||||
<div class="item monitor-row">
|
||||
<div class="title-title"><span class="text">{{ monitor.name }}</span></div>
|
||||
<div class="monitors-container">
|
||||
<div class="status-container">
|
||||
<span class="status-pill" :class="[monitor.statusClass]">{{ monitor.status }}</span>
|
||||
</div>
|
||||
<div class="status-container">
|
||||
<span class="response-time">{{ monitor.responseTime }}ms</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="errorMessage">
|
||||
<div class="error-message">
|
||||
<span class="text">{{ errorMessage }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* A simple example which you can use as a template for creating your own widget.
|
||||
* Takes two optional parameters (`text` and `count`), and fetches a list of images
|
||||
* from dummyapis.com, then renders the results to the UI.
|
||||
*/
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin],
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
monitors: null,
|
||||
errorMessage: null,
|
||||
errorMessageConstants: {
|
||||
missingApiKey: 'No API key set',
|
||||
missingUrl: 'No URL set',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchData();
|
||||
},
|
||||
computed: {
|
||||
/* Get API key for access to instance */
|
||||
apiKey() {
|
||||
return this.parseAsEnvVar(this.options.apiKey);
|
||||
},
|
||||
/* Get instance URL */
|
||||
url() {
|
||||
return this.parseAsEnvVar(this.options.url);
|
||||
},
|
||||
/* Create authorisation header for the instance from the apiKey */
|
||||
authHeaders() {
|
||||
if (!this.options.apiKey) {
|
||||
return {};
|
||||
}
|
||||
const encoded = window.btoa(`:${this.options.apiKey}`);
|
||||
return { Authorization: `Basic ${encoded}` };
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* The update() method extends mixin, used to update the data.
|
||||
* It's called by parent component, when the user presses update
|
||||
*/
|
||||
update() {
|
||||
this.startLoading();
|
||||
this.fetchData();
|
||||
},
|
||||
/* Make the data request to the computed API endpoint */
|
||||
fetchData() {
|
||||
const { authHeaders, url } = this;
|
||||
|
||||
if (!this.optionsValid({ authHeaders, url })) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.makeRequest(url, authHeaders)
|
||||
.then(this.processData);
|
||||
},
|
||||
/* Convert API response data into a format to be consumed by the UI */
|
||||
processData(response) {
|
||||
const monitorRows = this.getMonitorRows(response);
|
||||
|
||||
const monitors = new Map();
|
||||
|
||||
for (let index = 0; index < monitorRows.length; index += 1) {
|
||||
const row = monitorRows[index];
|
||||
this.processRow(row, monitors);
|
||||
}
|
||||
|
||||
this.monitors = Array.from(monitors.values());
|
||||
},
|
||||
getMonitorRows(response) {
|
||||
return response.split('\n').filter(row => row.startsWith('monitor_'));
|
||||
},
|
||||
processRow(row, monitors) {
|
||||
const dataType = this.getRowDataType(row);
|
||||
const monitorName = this.getRowMonitorName(row);
|
||||
|
||||
if (!monitors.has(monitorName)) {
|
||||
monitors.set(monitorName, { name: monitorName });
|
||||
}
|
||||
|
||||
const monitor = monitors.get(monitorName);
|
||||
const value = this.getRowValue(row);
|
||||
|
||||
const updated = this.setMonitorValue(dataType, monitor, value);
|
||||
|
||||
monitors.set(monitorName, updated);
|
||||
},
|
||||
setMonitorValue(key, monitor, value) {
|
||||
const copy = { ...monitor };
|
||||
switch (key) {
|
||||
case 'monitor_cert_days_remaining': {
|
||||
copy.certDaysRemaining = value;
|
||||
break;
|
||||
}
|
||||
case 'monitor_cert_is_valid': {
|
||||
copy.certValid = value;
|
||||
break;
|
||||
}
|
||||
case 'monitor_response_time': {
|
||||
copy.responseTime = value;
|
||||
break;
|
||||
}
|
||||
case 'monitor_status': {
|
||||
copy.status = value === '1' ? 'Up' : 'Down';
|
||||
copy.statusClass = copy.status.toLowerCase();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return copy;
|
||||
},
|
||||
getRowValue(row) {
|
||||
return this.getValueWithRegex(row, /\b\d+\b$/);
|
||||
},
|
||||
getRowMonitorName(row) {
|
||||
return this.getValueWithRegex(row, /monitor_name="([^"]+)"/);
|
||||
},
|
||||
getRowDataType(row) {
|
||||
return this.getValueWithRegex(row, /^(.*?)\{/);
|
||||
},
|
||||
getValueWithRegex(string, regex) {
|
||||
const result = string.match(regex);
|
||||
|
||||
const isArray = Array.isArray(result);
|
||||
|
||||
if (!isArray) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return result.length > 1 ? result[1] : result[0];
|
||||
},
|
||||
optionsValid({ url, authHeaders }) {
|
||||
const errors = [];
|
||||
if (url === undefined) {
|
||||
errors.push(this.errorMessageConstants.missingUrl);
|
||||
}
|
||||
|
||||
if (authHeaders === undefined) {
|
||||
errors.push(this.errorMessageConstants.missingApiKey);
|
||||
}
|
||||
|
||||
if (errors.length === 0) { return true; }
|
||||
|
||||
this.errorMessage = errors.join('\n');
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.status-pill {
|
||||
border-radius: 50em;
|
||||
box-sizing: border-box;
|
||||
font-size: 0.75em;
|
||||
display: inline-block;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
padding: .35em .65em;
|
||||
margin: 1em 0.5em;
|
||||
min-width: 64px;
|
||||
|
||||
&.up {
|
||||
background-color: rgb(92, 221, 139);
|
||||
color: black;
|
||||
}
|
||||
|
||||
&.down {
|
||||
background-color: rgb(220, 53, 69);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
div.item.monitor-row:hover {
|
||||
background-color: var(--item-background);
|
||||
color: var(--current-color);
|
||||
opacity: 1;
|
||||
|
||||
div.title-title>span.text {
|
||||
color: var(--current-color);
|
||||
}
|
||||
}
|
||||
|
||||
.monitors-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.monitor-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.35em 0.5em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -53,7 +53,7 @@ export default {
|
||||
},
|
||||
address() {
|
||||
if (!this.options.address) this.error('You must specify a public address');
|
||||
return this.options.address;
|
||||
return this.parseAsEnvVar(this.options.address);
|
||||
},
|
||||
network() {
|
||||
return this.options.network || 'main';
|
||||
|
||||
@@ -46,8 +46,12 @@ export default {
|
||||
return this.options.units || 'metric';
|
||||
},
|
||||
endpoint() {
|
||||
const { apiKey, city } = this.options;
|
||||
return `${widgetApiEndpoints.weather}?q=${city}&appid=${apiKey}&units=${this.units}`;
|
||||
const apiKey = this.parseAsEnvVar(this.options.apiKey);
|
||||
const { city, lat, lon } = this.options;
|
||||
const params = (lat && lon)
|
||||
? `lat=${lat}&lon=${lon}&appid=${apiKey}&units=${this.units}`
|
||||
: `q=${city}&appid=${apiKey}&units=${this.units}`;
|
||||
return `${widgetApiEndpoints.weather}?${params}`;
|
||||
},
|
||||
tempDisplayUnits() {
|
||||
switch (this.units) {
|
||||
@@ -106,7 +110,11 @@ export default {
|
||||
checkProps() {
|
||||
const ops = this.options;
|
||||
if (!ops.apiKey) this.error('Missing API key for OpenWeatherMap');
|
||||
if (!ops.city) this.error('A city name is required to fetch weather');
|
||||
|
||||
if ((!ops.lat || !ops.lon) && !ops.city) {
|
||||
this.error('A city name or lat + lon is required to fetch weather');
|
||||
}
|
||||
|
||||
if (ops.units && ops.units !== 'metric' && ops.units !== 'imperial') {
|
||||
this.error('Invalid units specified, must be either \'metric\' or \'imperial\'');
|
||||
}
|
||||
|
||||
@@ -67,12 +67,14 @@ const COMPAT = {
|
||||
'gl-alerts': 'GlAlerts',
|
||||
'gl-current-cores': 'GlCpuCores',
|
||||
'gl-current-cpu': 'GlCpuGauge',
|
||||
'gl-cpu-speedometer': 'GlCpuSpeedometer',
|
||||
'gl-cpu-history': 'GlCpuHistory',
|
||||
'gl-disk-io': 'GlDiskIo',
|
||||
'gl-disk-space': 'GlDiskSpace',
|
||||
'gl-ip-address': 'GlIpAddress',
|
||||
'gl-load-history': 'GlLoadHistory',
|
||||
'gl-current-mem': 'GlMemGauge',
|
||||
'gl-mem-speedometer': 'GlMemSpeedometer',
|
||||
'gl-mem-history': 'GlMemHistory',
|
||||
'gl-network-interfaces': 'GlNetworkInterfaces',
|
||||
'gl-network-traffic': 'GlNetworkTraffic',
|
||||
@@ -113,6 +115,7 @@ const COMPAT = {
|
||||
'synology-download': 'SynologyDownload',
|
||||
'system-info': 'SystemInfo',
|
||||
'tfl-status': 'TflStatus',
|
||||
'uptime-kuma': 'UptimeKuma',
|
||||
'wallet-balance': 'WalletBalance',
|
||||
weather: 'Weather',
|
||||
'weather-forecast': 'WeatherForecast',
|
||||
@@ -203,14 +206,16 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/media-queries.scss';
|
||||
@import "@/styles/media-queries.scss";
|
||||
|
||||
.widget-base {
|
||||
position: relative;
|
||||
padding: 0.75rem 0.5rem 0.5rem 0.5rem;
|
||||
background: var(--widget-base-background);
|
||||
box-shadow: var(--widget-base-shadow, none);
|
||||
|
||||
// Refresh and full-page action buttons
|
||||
button.action-btn {
|
||||
button.action-btn {
|
||||
height: 1rem;
|
||||
min-width: auto;
|
||||
width: 1.75rem;
|
||||
@@ -221,21 +226,26 @@ export default {
|
||||
border: none;
|
||||
opacity: var(--dimming-factor);
|
||||
color: var(--widget-text-color);
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: var(--widget-background-color);
|
||||
}
|
||||
|
||||
&.update-btn {
|
||||
right: -0.25rem;
|
||||
}
|
||||
|
||||
&.open-btn {
|
||||
right: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Optional widget label
|
||||
.widget-label {
|
||||
color: var(--widget-text-color);
|
||||
}
|
||||
|
||||
// Actual widget container
|
||||
.widget-wrap {
|
||||
&.has-error {
|
||||
@@ -243,9 +253,11 @@ export default {
|
||||
opacity: 0.5;
|
||||
border-radius: var(--curve-factor);
|
||||
background: #ffff0040;
|
||||
|
||||
&:hover { background: none; }
|
||||
}
|
||||
}
|
||||
|
||||
// Error message output
|
||||
.widget-error {
|
||||
p.error-msg {
|
||||
@@ -254,12 +266,14 @@ export default {
|
||||
font-size: 1rem;
|
||||
margin: 0 auto 0.5rem auto;
|
||||
}
|
||||
|
||||
p.error-output {
|
||||
font-family: var(--font-monospace);
|
||||
color: var(--widget-text-color);
|
||||
font-size: 0.85rem;
|
||||
margin: 0.5rem auto;
|
||||
}
|
||||
|
||||
p.retry-link {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
@@ -268,14 +282,17 @@ export default {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Loading spinner
|
||||
.loading {
|
||||
margin: 0.2rem auto;
|
||||
text-align: center;
|
||||
|
||||
svg.loader {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide widget contents while loading
|
||||
&.is-loading {
|
||||
.widget-wrap {
|
||||
@@ -283,5 +300,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
import { widgetApiEndpoints } from '@/utils/defaults';
|
||||
|
||||
@@ -41,11 +40,17 @@ export default {
|
||||
methods: {
|
||||
/* Make GET request to CoinGecko API endpoint */
|
||||
fetchData() {
|
||||
axios.get(this.endpoint)
|
||||
.then((response) => {
|
||||
this.processData(response.data);
|
||||
fetch(this.endpoint)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
this.error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.catch((dataFetchError) => {
|
||||
.then(data => {
|
||||
this.processData(data);
|
||||
})
|
||||
.catch(dataFetchError => {
|
||||
this.error('Unable to fetch data', dataFetchError);
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -71,7 +76,7 @@ export default {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.xkcd-wrapper {
|
||||
.xkcd-title {
|
||||
.xkcd-title {
|
||||
font-size: 1.2rem;
|
||||
margin: 0.25rem auto;
|
||||
color: var(--widget-text-color);
|
||||
|
||||
Reference in New Issue
Block a user