⚠️ Fixes merge conflicts
This commit is contained in:
20
src/assets/interface-icons/open-workspace.svg
Normal file
20
src/assets/interface-icons/open-workspace.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
data-prefix="far"
|
||||
data-icon="browser"
|
||||
class="svg-inline--fa fa-browser fa-w-16"
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path
|
||||
transform = "rotate(-90 250 250)"
|
||||
fill="currentColor"
|
||||
d="M464 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h416c26.5 0 48-21.5
|
||||
48-48V80c0-26.5-21.5-48-48-48zM48 92c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12
|
||||
12v24c0 6.6-5.4 12-12 12H60c-6.6 0-12-5.4-12-12V92zm416 334c0 3.3-2.7 6-6
|
||||
6H54c-3.3 0-6-2.7-6-6V168h416v258zm0-310c0 6.6-5.4 12-12 12H172c-6.6
|
||||
0-12-5.4-12-12V92c0-6.6 5.4-12 12-12h280c6.6 0 12 5.4 12 12v24z">
|
||||
</path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 697 B |
@@ -42,11 +42,6 @@ export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOpen: !this.collapsed,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
/* Check that row & column span is valid, and not over the max */
|
||||
checkSpanNum(span, classPrefix) {
|
||||
|
||||
116
src/components/LinkItems/ContextMenu.vue
Normal file
116
src/components/LinkItems/ContextMenu.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<transition name="slide">
|
||||
<div class="context-menu" v-if="show && menuEnabled"
|
||||
:style="posX && posY ? `top:${posY}px;left:${posX}px;` : ''">
|
||||
<ul>
|
||||
<li @click="launch('sametab')">
|
||||
<SameTabOpenIcon />
|
||||
<span>Open in Current Tab</span>
|
||||
</li>
|
||||
<li @click="launch('newtab')">
|
||||
<NewTabOpenIcon />
|
||||
<span>Open in New Tab</span>
|
||||
</li>
|
||||
<li @click="launch('modal')">
|
||||
<IframeOpenIcon />
|
||||
<span>Open in Pop-Up Modal</span>
|
||||
</li>
|
||||
<li @click="launch('workspace')">
|
||||
<WorkspaceOpenIcon />
|
||||
<span>Open in Workspace View</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Import icons for each element
|
||||
import SameTabOpenIcon from '@/assets/interface-icons/open-current-tab.svg';
|
||||
import NewTabOpenIcon from '@/assets/interface-icons/open-new-tab.svg';
|
||||
import IframeOpenIcon from '@/assets/interface-icons/open-iframe.svg';
|
||||
import WorkspaceOpenIcon from '@/assets/interface-icons/open-workspace.svg';
|
||||
|
||||
export default {
|
||||
name: 'ContextMenu',
|
||||
inject: ['config'],
|
||||
components: {
|
||||
SameTabOpenIcon,
|
||||
NewTabOpenIcon,
|
||||
IframeOpenIcon,
|
||||
WorkspaceOpenIcon,
|
||||
},
|
||||
props: {
|
||||
posX: Number, // The X coordinate for positioning
|
||||
posY: Number, // The Y coordinate for positioning
|
||||
show: Boolean, // Should show or hide the menu
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
menuEnabled: !this.isMenuDisabled(), // Specifies if the context menu should be used
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
/* Called on item click, emits an event up to Item */
|
||||
/* in order to launch the current app to a given target */
|
||||
launch(target) {
|
||||
this.$emit('contextItemClick', target);
|
||||
},
|
||||
/* Checks if the user as disabled context menu in config */
|
||||
isMenuDisabled() {
|
||||
if (this.config && this.config.appConfig) {
|
||||
return !!this.config.appConfig.disableContextMenu;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
div.context-menu {
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
z-index: 8;
|
||||
background: var(--context-menu-background);
|
||||
color: var(--context-menu-color);
|
||||
border: 1px solid var(--context-menu-secondary-color);
|
||||
border-radius: var(--curve-factor);
|
||||
box-shadow: var(--context-menu-shadow);
|
||||
opacity: 0.98;
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
li {
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 1rem;
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--context-menu-secondary-color);
|
||||
}
|
||||
&:hover {
|
||||
background: var(--context-menu-secondary-color);
|
||||
}
|
||||
svg {
|
||||
width: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
path { fill: currentColor; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define enter and leave transitions
|
||||
.slide-enter-active { animation: slide-in .1s; }
|
||||
.slide-leave-active { animation: slide-in .1s reverse; }
|
||||
@keyframes slide-in {
|
||||
0% { transform: scaleY(0.5) scaleX(0.8) translateY(-50px); }
|
||||
100% { transform: scaleY(1) translateY(0) translateY(0); }
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,9 @@
|
||||
<template ref="container">
|
||||
<div class="item-wrapper">
|
||||
<a @click="itemOpened"
|
||||
:href="target !== 'iframe' ? url : '#'"
|
||||
@mouseup.right="openContextMenu"
|
||||
@contextmenu.prevent
|
||||
:href="target !== 'modal' ? url : '#'"
|
||||
:target="target === 'newtab' ? '_blank' : ''"
|
||||
:class="`item ${!icon? 'short': ''} size-${itemSize}`"
|
||||
v-tooltip="getTooltipOptions()"
|
||||
@@ -19,6 +22,7 @@
|
||||
<!-- Small icon, showing opening method on hover -->
|
||||
<ItemOpenMethodIcon class="opening-method-icon" :isSmall="!icon" :openingMethod="target"
|
||||
:position="itemSize === 'medium'? 'bottom right' : 'top right'"/>
|
||||
<!-- Status indicator dot (if enabled) showing weather srevice is availible -->
|
||||
<StatusIndicator
|
||||
class="status-indicator"
|
||||
v-if="enableStatusCheck"
|
||||
@@ -26,13 +30,24 @@
|
||||
:statusText="statusResponse ? statusResponse.message : undefined"
|
||||
/>
|
||||
</a>
|
||||
<ContextMenu
|
||||
:show="contextMenuOpen"
|
||||
v-click-outside="closeContextMenu"
|
||||
:posX="contextPos.posX"
|
||||
:posY="contextPos.posY"
|
||||
:id="`context-menu-${id}`"
|
||||
@contextItemClick="contextItemClick"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import router from '@/router';
|
||||
import Icon from '@/components/LinkItems/ItemIcon.vue';
|
||||
import ItemOpenMethodIcon from '@/components/LinkItems/ItemOpenMethodIcon';
|
||||
import StatusIndicator from '@/components/LinkItems/StatusIndicator';
|
||||
import ContextMenu from '@/components/LinkItems/ContextMenu';
|
||||
|
||||
export default {
|
||||
name: 'Item',
|
||||
@@ -45,40 +60,63 @@ export default {
|
||||
color: String, // Optional text and icon color, specified in hex code
|
||||
backgroundColor: String, // Optional item background color
|
||||
url: String, // URL to the resource, optional but recommended
|
||||
target: { // Where resource will open, either 'newtab', 'sametab' or 'iframe'
|
||||
target: { // Where resource will open, either 'newtab', 'sametab' or 'modal'
|
||||
type: String,
|
||||
default: 'newtab',
|
||||
validator: (value) => ['newtab', 'sametab', 'iframe'].indexOf(value) !== -1,
|
||||
validator: (value) => ['newtab', 'sametab', 'modal', 'workspace'].indexOf(value) !== -1,
|
||||
},
|
||||
itemSize: String,
|
||||
enableStatusCheck: Boolean,
|
||||
statusCheckHeaders: Object,
|
||||
statusCheckUrl: String,
|
||||
statusCheckInterval: Number,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
contextMenuOpen: false,
|
||||
getId: this.id,
|
||||
customStyles: {
|
||||
color: this.color,
|
||||
background: this.backgroundColor,
|
||||
},
|
||||
statusResponse: undefined,
|
||||
contextPos: {
|
||||
posX: undefined,
|
||||
posY: undefined,
|
||||
},
|
||||
};
|
||||
},
|
||||
components: {
|
||||
Icon,
|
||||
ItemOpenMethodIcon,
|
||||
StatusIndicator,
|
||||
ContextMenu,
|
||||
},
|
||||
methods: {
|
||||
/* Called when an item is clicked, manages the opening of iframe & resets the search field */
|
||||
/* Called when an item is clicked, manages the opening of modal & resets the search field */
|
||||
itemOpened(e) {
|
||||
if (e.altKey || this.target === 'iframe') {
|
||||
if (e.altKey || this.target === 'modal') {
|
||||
e.preventDefault();
|
||||
this.$emit('triggerModal', this.url);
|
||||
} else {
|
||||
this.$emit('itemClicked');
|
||||
}
|
||||
},
|
||||
/* Open custom context menu, and set position */
|
||||
openContextMenu(e) {
|
||||
this.contextMenuOpen = !this.contextMenuOpen;
|
||||
if (e && window) {
|
||||
// Calculate placement based on cursor and scroll position
|
||||
this.contextPos = {
|
||||
posX: e.clientX + window.pageXOffset,
|
||||
posY: e.clientY + window.pageYOffset,
|
||||
};
|
||||
}
|
||||
},
|
||||
/* Closes the context menu, called when user clicks literally anywhere */
|
||||
closeContextMenu() {
|
||||
this.contextMenuOpen = false;
|
||||
},
|
||||
/* Returns configuration object for the tooltip */
|
||||
getTooltipOptions() {
|
||||
return {
|
||||
@@ -97,15 +135,18 @@ export default {
|
||||
switch (this.target) {
|
||||
case 'newtab': return '"\\f360"';
|
||||
case 'sametab': return '"\\f24d"';
|
||||
case 'iframe': return '"\\f2d0"';
|
||||
case 'modal': return '"\\f2d0"';
|
||||
default: return '"\\f054"';
|
||||
}
|
||||
},
|
||||
/* Checks if a given service is currently online */
|
||||
checkWebsiteStatus() {
|
||||
this.statusResponse = undefined;
|
||||
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
|
||||
const endpoint = `${baseUrl}/ping?url=${this.url}`;
|
||||
axios.get(endpoint)
|
||||
const urlToCheck = this.statusCheckUrl || this.url;
|
||||
const headers = this.statusCheckHeaders || {};
|
||||
const endpoint = `${baseUrl}/ping?url=${urlToCheck}`;
|
||||
axios.get(endpoint, { headers })
|
||||
.then((response) => {
|
||||
if (response.data) this.statusResponse = response.data;
|
||||
})
|
||||
@@ -116,9 +157,31 @@ export default {
|
||||
};
|
||||
});
|
||||
},
|
||||
/* Handle navigation options from the context menu */
|
||||
contextItemClick(method) {
|
||||
const { url } = this;
|
||||
this.contextMenuOpen = false;
|
||||
switch (method) {
|
||||
case 'newtab':
|
||||
window.open(url, '_blank');
|
||||
break;
|
||||
case 'sametab':
|
||||
window.open(url, '_self');
|
||||
break;
|
||||
case 'modal':
|
||||
this.$emit('triggerModal', url);
|
||||
break;
|
||||
case 'workspace':
|
||||
router.push({ name: 'workspace', query: { url } });
|
||||
break;
|
||||
default: window.open(url, '_blank');
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// If ststus checking is enabled, then check service status
|
||||
if (this.enableStatusCheck) this.checkWebsiteStatus();
|
||||
// If continious status checking is enabled, then start ever-lasting loop
|
||||
if (this.statusCheckInterval > 0) {
|
||||
setInterval(this.checkWebsiteStatus, this.statusCheckInterval * 1000);
|
||||
}
|
||||
@@ -128,6 +191,10 @@ export default {
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.item-wrapper {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.item {
|
||||
flex-grow: 1;
|
||||
color: var(--item-text-color);
|
||||
@@ -147,6 +214,7 @@ export default {
|
||||
&:hover {
|
||||
box-shadow: var(--item-hover-shadow);
|
||||
background: var(--item-background-hover);
|
||||
color: var(--item-text-color-hover);
|
||||
position: relative;
|
||||
.tile-title span.text {
|
||||
white-space: pre-wrap;
|
||||
@@ -211,24 +279,29 @@ export default {
|
||||
|
||||
/* Specify layout for alternate sized icons */
|
||||
.item {
|
||||
/* Small Tile Specific Themes */
|
||||
&.size-small {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
padding-top: 4px;
|
||||
div img, div svg.missing-image {
|
||||
width: 2rem;
|
||||
}
|
||||
.tile-title {
|
||||
height: fit-content;
|
||||
min-height: 1.2rem;
|
||||
text-align: left;
|
||||
max-width:140px;
|
||||
span.text {
|
||||
text-align: left;
|
||||
padding-left: 10%;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Medium Tile Specific Themes */
|
||||
&.size-medium {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -243,14 +316,42 @@ export default {
|
||||
max-width: 160px;
|
||||
}
|
||||
}
|
||||
/* Large Tile Specific Themes */
|
||||
&.size-large {
|
||||
height: 100px;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: flex-end;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
max-height: 6rem;
|
||||
margin: 0.2rem;
|
||||
padding: 0.5rem;
|
||||
img {
|
||||
padding: 0.1rem 0.25rem;
|
||||
}
|
||||
.tile-title {
|
||||
height: auto;
|
||||
padding: 0.1rem 0.25rem;
|
||||
span.text {
|
||||
position: relative;
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
width: 100%;
|
||||
}
|
||||
p.description {
|
||||
display: block;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
font-size: .9em;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.description {
|
||||
display: none;
|
||||
display: none; // By default, we don't show the description
|
||||
}
|
||||
&:before {
|
||||
&:before { // Certain themes (e.g. material) show css animated fas icon on hover
|
||||
display: none;
|
||||
font-family: FontAwesome;
|
||||
content: var(--open-icon, "\f054") !important;
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
:target="item.target"
|
||||
:color="item.color"
|
||||
:backgroundColor="item.backgroundColor"
|
||||
:statusCheckUrl="item.statusCheckUrl"
|
||||
:statusCheckHeaders="item.statusCheckHeaders"
|
||||
:itemSize="newItemSize"
|
||||
:enableStatusCheck="shouldEnableStatusCheck(item.statusCheck)"
|
||||
:statusCheckInterval="getStatusCheckInterval()"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="item-icon">
|
||||
<i v-if="iconType === 'font-awesome'" :class="`${icon} ${size}`" ></i>
|
||||
<i v-else-if="iconType === 'emoji'" :class="`emoji-icon ${size}`" >{{getEmoji(iconPath)}}</i>
|
||||
<img v-else-if="icon" :src="iconPath" @error="imageNotFound"
|
||||
:class="`tile-icon ${size} ${broken ? 'broken' : ''}`"
|
||||
/>
|
||||
@@ -12,6 +13,8 @@
|
||||
import BrokenImage from '@/assets/interface-icons/broken-icon.svg';
|
||||
import ErrorHandler from '@/utils/ErrorHandler';
|
||||
import { faviconApi as defaultFaviconApi, faviconApiEndpoints } from '@/utils/defaults';
|
||||
import EmojiUnicodeRegex from '@/utils/EmojiUnicodeRegex';
|
||||
import emojiLookup from '@/utils/emojis.json';
|
||||
|
||||
export default {
|
||||
name: 'Icon',
|
||||
@@ -52,6 +55,27 @@ export default {
|
||||
if (splitPath.length >= 1) return validImgExtensions.includes(splitPath[1]);
|
||||
return false;
|
||||
},
|
||||
/* Determins if a given string is an emoji, and if so what type it is */
|
||||
isEmoji(img) {
|
||||
if (EmojiUnicodeRegex.test(img) && img.match(/./gu).length) { // Is a unicode emoji
|
||||
return { isEmoji: true, emojiType: 'glyph' };
|
||||
} else if (new RegExp(/^:.*:$/).test(img)) { // Is a shortcode emoji
|
||||
return { isEmoji: true, emojiType: 'shortcode' };
|
||||
} else if (img.substring(0, 2) === 'U+' && img.length === 7) {
|
||||
return { isEmoji: true, emojiType: 'unicode' };
|
||||
}
|
||||
return { isEmoji: false, emojiType: '' };
|
||||
},
|
||||
/* Formats and gets emoji from unicode or shortcode */
|
||||
getEmoji(emojiCode) {
|
||||
const { emojiType } = this.isEmoji(emojiCode);
|
||||
if (emojiType === 'shortcode') {
|
||||
if (emojiLookup[emojiCode]) return emojiLookup[emojiCode];
|
||||
} else if (emojiType === 'unicode') {
|
||||
return String.fromCodePoint(parseInt(emojiCode.substr(2), 16));
|
||||
}
|
||||
return emojiCode; // Emoji is a glyph already, just return
|
||||
},
|
||||
/* Get favicon URL, for items which use the favicon as their icon */
|
||||
getFavicon(fullUrl) {
|
||||
if (this.shouldUseDefaultFavicon(fullUrl)) { // Check if we should use local icon
|
||||
@@ -85,6 +109,7 @@ export default {
|
||||
case 'favicon': return this.getFavicon(url);
|
||||
case 'generative': return this.getGenerativeIcon(url);
|
||||
case 'svg': return img;
|
||||
case 'emoji': return img;
|
||||
default: return '';
|
||||
}
|
||||
},
|
||||
@@ -98,6 +123,7 @@ export default {
|
||||
else if (img.includes('fa-')) imgType = 'font-awesome';
|
||||
else if (img === 'favicon') imgType = 'favicon';
|
||||
else if (img === 'generative') imgType = 'generative';
|
||||
else if (this.isEmoji(img).isEmoji) imgType = 'emoji';
|
||||
else imgType = 'none';
|
||||
return imgType;
|
||||
},
|
||||
@@ -144,7 +170,17 @@ export default {
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
i.emoji-icon {
|
||||
font-style: normal;
|
||||
font-size: 2rem;
|
||||
margin: 0.2rem;
|
||||
&.small {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
&.large {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
.missing-image {
|
||||
width: 3.5rem;
|
||||
path {
|
||||
|
||||
@@ -2,19 +2,24 @@
|
||||
<div :class="makeClass(position, isSmall, isTransparent)">
|
||||
<NewTabOpenIcon v-if="openingMethod === 'newtab'" />
|
||||
<SameTabOpenIcon v-else-if="openingMethod === 'sametab'" />
|
||||
<IframeOpenIcon v-else-if="openingMethod === 'iframe'" />
|
||||
<IframeOpenIcon v-else-if="openingMethod === 'modal'" />
|
||||
<WorkspaceOpenIcon v-else-if="openingMethod === 'workspace'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* This component displays a small icon, indicating opening method */
|
||||
|
||||
// Import Icons
|
||||
import NewTabOpenIcon from '@/assets/interface-icons/open-new-tab.svg';
|
||||
import SameTabOpenIcon from '@/assets/interface-icons/open-current-tab.svg';
|
||||
import IframeOpenIcon from '@/assets/interface-icons/open-iframe.svg';
|
||||
import WorkspaceOpenIcon from '@/assets/interface-icons/open-workspace.svg';
|
||||
|
||||
export default {
|
||||
name: 'ItemOpenMethodIcon',
|
||||
props: {
|
||||
openingMethod: String, // newtab | sametab | iframe
|
||||
openingMethod: String, // newtab | sametab | modal | workspace
|
||||
isSmall: Boolean, // If true, will apply small class
|
||||
position: String, // Position classes: top, bottom, left, right
|
||||
isTransparent: Boolean, // If true, will apply opacity
|
||||
@@ -32,6 +37,7 @@ export default {
|
||||
NewTabOpenIcon,
|
||||
SameTabOpenIcon,
|
||||
IframeOpenIcon,
|
||||
WorkspaceOpenIcon,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -36,7 +36,7 @@ footer {
|
||||
text-align: center;
|
||||
color: var(--medium-grey);
|
||||
opacity: var(--dimming-factor);
|
||||
background: var(--background-darker);
|
||||
background: var(--footer-background);
|
||||
margin-top: 1.5rem;
|
||||
border-top: 1px solid var(--outline-color);
|
||||
@include tablet-down {
|
||||
|
||||
@@ -83,7 +83,7 @@ export default {
|
||||
background: var(--search-container-background);
|
||||
label {
|
||||
display: inline;
|
||||
color: var(--settings-text-color);
|
||||
color: var(--search-label-color);
|
||||
margin: 0.5rem;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
10
src/main.js
10
src/main.js
@@ -7,16 +7,18 @@ import VSelect from 'vue-select'; // Select dropdown component
|
||||
import VTabs from 'vue-material-tabs'; // Tab view component, used on the config page
|
||||
import Toasted from 'vue-toasted'; // Toast component, used to show confirmation notifications
|
||||
|
||||
import { toastedOptions } from './utils/defaults';
|
||||
import Dashy from './App.vue';
|
||||
import router from './router';
|
||||
import registerServiceWorker from './registerServiceWorker';
|
||||
import { toastedOptions } from '@/utils/defaults';
|
||||
import Dashy from '@/App.vue';
|
||||
import router from '@/router';
|
||||
import registerServiceWorker from '@/registerServiceWorker';
|
||||
import clickOutside from '@/utils/ClickOutside';
|
||||
|
||||
Vue.use(VTooltip);
|
||||
Vue.use(VModal);
|
||||
Vue.use(VTabs);
|
||||
Vue.use(Toasted, toastedOptions);
|
||||
Vue.component('v-select', VSelect);
|
||||
Vue.directive('clickOutside', clickOutside);
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
--nav-link-border-color: transparent;
|
||||
--nav-link-border-color-hover: var(--primary);
|
||||
--item-text-color: var(--primary);
|
||||
--item-text-color-hover: var(--item-text-color);
|
||||
--item-group-outer-background: var(--primary);
|
||||
--item-group-heading-text-color: var(--item-group-background);
|
||||
--item-group-heading-text-color-hover: var(--background);
|
||||
@@ -49,8 +50,10 @@
|
||||
--settings-text-color: var(--primary);
|
||||
--search-container-background: var(--background-darker);
|
||||
--search-field-background: var(--background);
|
||||
--search-label-color: var(--settings-text-color);
|
||||
--footer-text-color: var(--medium-grey);
|
||||
--footer-text-color-link: var(--primary);
|
||||
--footer-background: var(--background-darker);
|
||||
--welcome-popup-background: var(--background-darker);
|
||||
--welcome-popup-text-color: var(--primary);
|
||||
--config-code-background: #fff;
|
||||
@@ -78,4 +81,8 @@
|
||||
--status-check-tooltip-color: var(--primary);
|
||||
--code-editor-color: var(--black);
|
||||
--code-editor-background: var(--white);
|
||||
|
||||
--context-menu-background: var(--background);
|
||||
--context-menu-color: var(--primary);
|
||||
--context-menu-secondary-color: var(--background-darker);
|
||||
}
|
||||
|
||||
@@ -88,9 +88,46 @@ html[data-theme='matrix'] {
|
||||
--font-body: 'Cutive Mono', monospace;
|
||||
--font-headings: 'VT323', monospace;
|
||||
--about-page-background: var(--background);
|
||||
--context-menu-secondary-color: var(--primary);
|
||||
.prism-editor-wrapper.my-editor {
|
||||
border: 1px solid var(--primary);
|
||||
}
|
||||
div.context-menu ul li:hover {
|
||||
color: var(--background);
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='blue-purple'] {
|
||||
--primary: #54dbf8;
|
||||
--background: #e5e8f5;
|
||||
--background-darker: #5346f3;
|
||||
--font-headings: 'Sniglet', cursive;
|
||||
|
||||
--dimming-factor: 0.8;
|
||||
--curve-factor: 6px;
|
||||
|
||||
--settings-text-color: var(--background-darker);
|
||||
--item-text-color: var(--background-darker);
|
||||
--item-background: var(--white);
|
||||
--item-background-hover: var(--primary);
|
||||
|
||||
--item-group-heading-text-color: var(--background-darker);
|
||||
--item-group-background: var(--background);
|
||||
--footer-text-color: var(--white);
|
||||
--context-menu-background: var(--white);
|
||||
--context-menu-color: var(--background-darker);
|
||||
--context-menu-secondary-color: var(--primary);
|
||||
|
||||
.item {
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--background-darker);
|
||||
}
|
||||
section.filter-container form label {
|
||||
color: var(--primary);
|
||||
}
|
||||
footer {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='hacker-girl'] {
|
||||
@@ -184,6 +221,12 @@ html[data-theme='material-original'] {
|
||||
--about-page-accent: #000;
|
||||
--about-page-color: var(--background-darker);
|
||||
--about-page-background: var(--background);
|
||||
--context-menu-background: var(--white);
|
||||
--context-menu-secondary-color: var(--white);
|
||||
div.context-menu ul li:hover {
|
||||
background: var(--primary);
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='material-dark-original'] {
|
||||
@@ -222,6 +265,13 @@ html[data-theme='material-dark-original'] {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-left: 1px solid #131a1f;
|
||||
}
|
||||
div.context-menu {
|
||||
border: none;
|
||||
background: #131a1f;
|
||||
ul li:hover {
|
||||
background: #333c43;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='colorful'] {
|
||||
@@ -234,14 +284,14 @@ html[data-theme='colorful'] {
|
||||
--item-group-outer-background: #05070e;
|
||||
--item-group-heading-text-color: #e8eae1;
|
||||
--item-group-heading-text-color-hover: #fff;
|
||||
.item:nth-child(1n) { color: #eb5cad; border: 1px solid #eb5cad; }
|
||||
.item:nth-child(2n) { color: #985ceb; border: 1px solid #985ceb; }
|
||||
.item:nth-child(3n) { color: #5c90eb; border: 1px solid #5c90eb; }
|
||||
.item:nth-child(4n) { color: #5cdfeb; border: 1px solid #5cdfeb; }
|
||||
.item:nth-child(5n) { color: #5ceb8d; border: 1px solid #5ceb8d; }
|
||||
.item:nth-child(6n) { color: #afeb5c; border: 1px solid #afeb5c; }
|
||||
.item:nth-child(7n) { color: #ebb75c; border: 1px solid #ebb75c; }
|
||||
.item:nth-child(8n) { color: #eb615c; border: 1px solid #eb615c; }
|
||||
.item-wrapper:nth-child(1n) { .item { color: #eb5cad; border: 1px solid #eb5cad; } }
|
||||
.item-wrapper:nth-child(2n) { .item { color: #985ceb; border: 1px solid #985ceb; } }
|
||||
.item-wrapper:nth-child(3n) { .item { color: #5c90eb; border: 1px solid #5c90eb; } }
|
||||
.item-wrapper:nth-child(4n) { .item { color: #5cdfeb; border: 1px solid #5cdfeb; } }
|
||||
.item-wrapper:nth-child(5n) { .item { color: #5ceb8d; border: 1px solid #5ceb8d; } }
|
||||
.item-wrapper:nth-child(6n) { .item { color: #afeb5c; border: 1px solid #afeb5c; } }
|
||||
.item-wrapper:nth-child(7n) { .item { color: #ebb75c; border: 1px solid #ebb75c; } }
|
||||
.item-wrapper:nth-child(8n) { .item { color: #eb615c; border: 1px solid #eb615c; } }
|
||||
.item:hover, .item:focus {
|
||||
opacity: 0.85;
|
||||
outline: none;
|
||||
@@ -253,12 +303,20 @@ html[data-theme='colorful'] {
|
||||
h1, h2, h3, h4 {
|
||||
font-weight: normal;
|
||||
}
|
||||
div.context-menu {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='minimal-light'], html[data-theme='minimal-dark'], html[data-theme='vaporware'] {
|
||||
--font-body: 'Courier New', monospace;
|
||||
--font-headings: 'Courier New', monospace;
|
||||
--footer-height: 94px;
|
||||
|
||||
.item.size-medium .tile-title {
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
label.lbl-toggle h3 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
@@ -449,6 +507,7 @@ html[data-theme='material'] {
|
||||
--welcome-popup-text-color: #f5f5f5;
|
||||
--footer-text-color: #f5f5f5cc;
|
||||
// --login-form-background-secondary: #f5f5f5cc;
|
||||
--context-menu-secondary-color: #f5f5f5;
|
||||
|
||||
header {
|
||||
background: #4285f4;
|
||||
@@ -467,6 +526,14 @@ html[data-theme='material'] {
|
||||
.prism-editor-wrapper {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.item:focus {
|
||||
outline-color: #4285f4cc;
|
||||
}
|
||||
div.context-menu {
|
||||
border: none;
|
||||
background: var(--white);
|
||||
ul li:hover { svg path { fill: var(--background-darker); }}
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='material-dark'] {
|
||||
@@ -521,6 +588,13 @@ html[data-theme='material-dark'] {
|
||||
background: #131a1f !important;
|
||||
}
|
||||
}
|
||||
div.context-menu {
|
||||
border: none;
|
||||
background: var(--background);
|
||||
ul li:hover {
|
||||
background: #131a1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='minimal-light'] {
|
||||
@@ -547,7 +621,8 @@ html[data-theme='minimal-light'] {
|
||||
--login-form-color: #101931;
|
||||
--about-page-background: var(--background);
|
||||
--about-page-color: var(--background-darker);
|
||||
|
||||
--context-menu-color: var(--background-darker);
|
||||
--context-menu-secondary-color: var(--primary);
|
||||
section.filter-container {
|
||||
background: #fff;
|
||||
border-bottom: 1px dashed #00000038;
|
||||
@@ -592,6 +667,10 @@ html[data-theme='minimal-dark'] {
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
}
|
||||
|
||||
div.context-menu {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='vaporware'] {
|
||||
@@ -613,7 +692,8 @@ html[data-theme='vaporware'] {
|
||||
--curve-factor: 2px;
|
||||
--curve-factor-navbar: 6px;
|
||||
--login-form-color: #09bfe6;
|
||||
|
||||
--config-settings-background: #100e2c;
|
||||
|
||||
.home {
|
||||
background: linear-gradient(180deg, rgba(16,14,44,1) 10%, rgba(27,24,79,1) 40%, rgba(16,14,44,1) 100%);
|
||||
}
|
||||
@@ -674,4 +754,30 @@ html[data-theme='vaporware'] {
|
||||
// background-size: cover;
|
||||
// div.home { background: none; }
|
||||
// }
|
||||
}
|
||||
|
||||
html[data-theme='cyberpunk'] {
|
||||
--pink: #ff2a6d;
|
||||
--pale: #d1f7ff;
|
||||
--aqua: #05d9e8;
|
||||
--teal: #005678;
|
||||
--blue: #01012b;
|
||||
--gold: #ebeb0f;
|
||||
|
||||
--primary: var(--gold);
|
||||
--background: var(--blue);
|
||||
--background-darker: var(--pink);
|
||||
--heading-text-color: var(--blue);
|
||||
--nav-link-background-color-hover: var(--blue);
|
||||
--nav-link-text-color-hover: var(--pink);
|
||||
--nav-link-border-color-hover: var(--blue);
|
||||
--config-settings-background: var(--blue);
|
||||
--config-settings-color: var(--pink);
|
||||
--search-label-color: var(--blue);
|
||||
--item-group-background: var(--blue);
|
||||
--item-text-color: var(--pale);
|
||||
--scroll-bar-color: var(--aqua);
|
||||
--scroll-bar-background: var(--teal);
|
||||
--footer-background: var(--aqua);
|
||||
--font-headings: 'Audiowide', cursive;
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
--item-icon-transform: drop-shadow(2px 4px 6px var(--transparent-50)) saturate(0.65);
|
||||
--item-icon-transform-hover: drop-shadow(4px 8px 3px var(--transparent-50)) saturate(2);
|
||||
--item-group-shadow: var(--item-shadow);
|
||||
--context-menu-shadow: var(--item-shadow);
|
||||
|
||||
/* Settings and config menu */
|
||||
--settings-container-shadow: none;
|
||||
|
||||
@@ -42,7 +42,6 @@ html {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
/* Optional fonts for specific themes */
|
||||
/* These fonts are loaded from ./public and therefore not bundled within the apps source */
|
||||
@font-face { // Used by Dracula. Credit to Matt McInerney
|
||||
@@ -73,3 +72,9 @@ html {
|
||||
font-family: 'VT323';
|
||||
src: url('/fonts/VT323-Regular.ttf');
|
||||
}
|
||||
|
||||
@font-face { // Used by cyberpunk theme. Credit to Astigmatic
|
||||
font-family: 'Audiowide';
|
||||
src: url('/fonts/Audiowide-Regular.ttf');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import sha256 from 'crypto-js/sha256';
|
||||
import { cookieKeys, localStorageKeys } from './defaults';
|
||||
|
||||
/**
|
||||
* Generates a 1-way hash, in order to be stored in local storage for authentication
|
||||
* @param {String} user The username of user
|
||||
* @returns {String} The hashed token
|
||||
*/
|
||||
const generateUserToken = (user) => sha256(user.toString()).toString().toLowerCase();
|
||||
|
||||
/**
|
||||
* Checks if the user is currently authenticated
|
||||
* @param {Array[Object]} users An array of user objects pulled from the config
|
||||
* @returns {Boolean} Will return true if the user is logged in, else false
|
||||
*/
|
||||
export const isLoggedIn = (users) => {
|
||||
const validTokens = users.map((user) => generateUserToken(user));
|
||||
let userAuthenticated = false;
|
||||
@@ -20,6 +30,15 @@ export const isLoggedIn = (users) => {
|
||||
return userAuthenticated;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks credentials entered by the user against those in the config
|
||||
* Returns an object containing a boolean indicating success/ failure
|
||||
* along with a message outlining what's not right
|
||||
* @param {String} username The username entered by the user
|
||||
* @param {String} pass The password entered by the user
|
||||
* @param {String[]} users An array of valid user objects
|
||||
* @returns {Object} An object containing a boolean result and a message
|
||||
*/
|
||||
export const checkCredentials = (username, pass, users) => {
|
||||
let response;
|
||||
if (!username) {
|
||||
@@ -40,12 +59,24 @@ export const checkCredentials = (username, pass, users) => {
|
||||
return response || { correct: false, msg: 'User not found' };
|
||||
};
|
||||
|
||||
export const login = (username, pass) => {
|
||||
/**
|
||||
* Sets the cookie value in order to login the user locally
|
||||
* @param {String} username - The users username
|
||||
* @param {String} pass - Password, not yet hashed
|
||||
* @param {Number} timeout - A desired timeout for the session, in ms
|
||||
*/
|
||||
export const login = (username, pass, timeout) => {
|
||||
const now = new Date();
|
||||
const expiry = new Date(now.setTime(now.getTime() + timeout)).toGMTString();
|
||||
const userObject = { user: username, hash: sha256(pass).toString().toLowerCase() };
|
||||
document.cookie = `authenticationToken=${generateUserToken(userObject)}; max-age=600`;
|
||||
document.cookie = `authenticationToken=${generateUserToken(userObject)};`
|
||||
+ `${timeout > 0 ? `expires=${expiry}` : ''}`;
|
||||
localStorage.setItem(localStorageKeys.USERNAME, username);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removed the browsers cookie, causing user to be logged out
|
||||
*/
|
||||
export const logout = () => {
|
||||
document.cookie = 'authenticationToken=null';
|
||||
localStorage.removeItem(localStorageKeys.USERNAME);
|
||||
@@ -57,8 +88,8 @@ export const logout = () => {
|
||||
* But if auth is configured, then will verify user is correctly
|
||||
* logged in and then check weather they are of type admin, and
|
||||
* return false if any conditions fail
|
||||
* @param users[] : Array of users
|
||||
* @returns Boolean : True if admin privileges
|
||||
* @param {String[]} - Array of users
|
||||
* @returns {Boolean} - True if admin privileges
|
||||
*/
|
||||
export const isUserAdmin = (users) => {
|
||||
if (!users || users.length === 0) return true; // Authentication not setup
|
||||
|
||||
37
src/utils/ClickOutside.js
Normal file
37
src/utils/ClickOutside.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* A simple Vue directive to trigger an event when the user
|
||||
* clicks anywhere other than the specified element.
|
||||
* Used to close context menu's popup menus and tips.
|
||||
*/
|
||||
|
||||
const instances = [];
|
||||
|
||||
function onDocumentClick(e, el, fn) {
|
||||
const { target } = e;
|
||||
if (el !== target && !el.contains(target)) {
|
||||
fn(e);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
bind(element, binding) {
|
||||
const el = element;
|
||||
el.dataset.outsideClickIndex = instances.length;
|
||||
|
||||
const fn = binding.value;
|
||||
const click = (e) => {
|
||||
onDocumentClick(e, el, fn);
|
||||
};
|
||||
|
||||
document.addEventListener('click', click);
|
||||
document.addEventListener('touchstart', click);
|
||||
instances.push(click);
|
||||
},
|
||||
unbind(el) {
|
||||
if (!el.dataset) return;
|
||||
const index = el.dataset.outsideClickIndex;
|
||||
const handler = instances[index];
|
||||
document.removeEventListener('click', handler);
|
||||
instances.splice(index, 1);
|
||||
},
|
||||
};
|
||||
@@ -56,8 +56,31 @@
|
||||
},
|
||||
"theme": {
|
||||
"type": "string",
|
||||
"default": "Callisto",
|
||||
"description": "A theme to be applied by default on first load"
|
||||
"default": "callisto",
|
||||
"description": "A theme to be applied by default on first load",
|
||||
"examples": [
|
||||
"callisto",
|
||||
"thebe",
|
||||
"dracula",
|
||||
"material",
|
||||
"material-dark",
|
||||
"colorful",
|
||||
"nord",
|
||||
"nord-frost",
|
||||
"minimal-dark",
|
||||
"minimal-light",
|
||||
"matrix",
|
||||
"matrix-red",
|
||||
"hacker-girl",
|
||||
"raspberry-jam",
|
||||
"bee",
|
||||
"tiger",
|
||||
"material-original",
|
||||
"material-dark-original",
|
||||
"vaporware",
|
||||
"high-contrast-dark",
|
||||
"high-contrast-light"
|
||||
]
|
||||
},
|
||||
"enableFontAwesome": {
|
||||
"type": "boolean",
|
||||
@@ -176,7 +199,12 @@
|
||||
"disableServiceWorker": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "If set to true, then service worker will not be used"
|
||||
"description": "If set to true, then service workers will not be used to cache page contents"
|
||||
},
|
||||
"disableContextMenu": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "If set to true, custom right-click context menu will be disabled"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -295,7 +323,8 @@
|
||||
"enum": [
|
||||
"newtab",
|
||||
"sametab",
|
||||
"iframe"
|
||||
"modal",
|
||||
"workspace"
|
||||
],
|
||||
"default": "newtab",
|
||||
"description": "Opening method, when item is clicked"
|
||||
@@ -312,6 +341,14 @@
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Whether or not to display online/ offline status for this service. Will override appConfig.statusCheck"
|
||||
},
|
||||
"statusCheckUrl": {
|
||||
"type": "string",
|
||||
"description": "If you've enabled statusCheck, and want to use a different URL to what is defined under the item, then specify it here"
|
||||
},
|
||||
"statusCheckHeaders": {
|
||||
"type": "object",
|
||||
"description": " If you're endpoint requires any specific headers for the status checking, then define them here"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
src/utils/EmojiUnicodeRegex.js
Normal file
1
src/utils/EmojiUnicodeRegex.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|[\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|[\ud83c[\ude32-\ude3a]|[\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/;
|
||||
@@ -31,8 +31,10 @@ module.exports = {
|
||||
'raspberry-jam',
|
||||
'bee',
|
||||
'tiger',
|
||||
'blue-purple',
|
||||
'material-original',
|
||||
'material-dark-original',
|
||||
'cyberpunk',
|
||||
'vaporware',
|
||||
'high-contrast-dark',
|
||||
'high-contrast-light',
|
||||
|
||||
1919
src/utils/emojis.json
Normal file
1919
src/utils/emojis.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -160,13 +160,17 @@ export default {
|
||||
},
|
||||
/* Checks if any of the icons are Font Awesome glyphs */
|
||||
checkIfFontAwesomeNeeded() {
|
||||
let isFound = false;
|
||||
let isNeeded = false;
|
||||
if (!this.sections) return false;
|
||||
this.sections.forEach((section) => {
|
||||
if (section.icon && section.icon.includes('fa-')) isNeeded = true;
|
||||
section.items.forEach((item) => {
|
||||
if (item.icon && item.icon.includes('fa-')) isFound = true;
|
||||
if (item.icon && item.icon.includes('fa-')) isNeeded = true;
|
||||
});
|
||||
});
|
||||
return isFound;
|
||||
const currentTheme = localStorage[localStorageKeys.THEME]; // Some themes require FA
|
||||
if (['material', 'material-dark'].includes(currentTheme)) isNeeded = true;
|
||||
return isNeeded;
|
||||
},
|
||||
/* Injects font-awesome's script tag, only if needed */
|
||||
initiateFontAwesome() {
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
<h2 class="login-title">Dashy</h2>
|
||||
<Input v-model="username" label="Username" class="login-field username" type="text" />
|
||||
<Input v-model="password" label="Password" class="login-field password" type="password" />
|
||||
<label>Remember me for</label>
|
||||
<v-select
|
||||
v-model="timeout"
|
||||
:options="dropDownMenu"
|
||||
label="label"
|
||||
:selectOnTab="true"
|
||||
class="login-time-dropdown"
|
||||
/>
|
||||
<Button class="login-button" :click="submitLogin">Login</Button>
|
||||
<transition name="bounce">
|
||||
<p :class="`login-error-message ${status}`" v-show="message">{{ message }}</p>
|
||||
@@ -30,6 +38,13 @@ export default {
|
||||
password: '',
|
||||
message: '',
|
||||
status: 'waiting', // wating, error, success
|
||||
timeout: { label: 'Never', time: 0 },
|
||||
dropDownMenu: [ // Data for timeout dropdown menu, label + value
|
||||
{ label: 'Never', time: 0 }, // Time is specified in ms
|
||||
{ label: '4 Hours', time: 14400 * 1000 },
|
||||
{ label: '1 Day', time: 86400 * 1000 },
|
||||
{ label: '1 Week', time: 604800 * 1000 },
|
||||
],
|
||||
};
|
||||
},
|
||||
components: {
|
||||
@@ -38,11 +53,12 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
submitLogin() {
|
||||
const timeout = this.timeout.time || 0;
|
||||
const response = checkCredentials(this.username, this.password, this.appConfig.auth || []);
|
||||
this.message = response.msg; // Show error or success message to the user
|
||||
this.status = response.correct ? 'success' : 'error';
|
||||
if (response.correct) { // Yay, credentials were correct :)
|
||||
login(this.username, this.password); // Login, to set the cookie
|
||||
login(this.username, this.password, timeout); // Login, to set the cookie
|
||||
setTimeout(() => { // Wait a short while, then redirect back home
|
||||
router.push({ path: '/' });
|
||||
}, 250);
|
||||
@@ -130,4 +146,30 @@ export default {
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.v-select.login-time-dropdown {
|
||||
margin: 0.5rem 0;
|
||||
.vs__dropdown-toggle {
|
||||
border-color: var(--login-form-color);
|
||||
background: var(--login-form-background);
|
||||
span.vs__selected {
|
||||
color: var(--login-form-color);
|
||||
}
|
||||
.vs__actions svg path { fill: var(--login-form-color); }
|
||||
}
|
||||
ul.vs__dropdown-menu {
|
||||
background: var(--login-form-background);
|
||||
border-color: var(--login-form-color);
|
||||
li {
|
||||
color: var(--login-form-color);
|
||||
&:hover {
|
||||
color: var(--login-form-background);
|
||||
background: var(--login-form-color);
|
||||
}
|
||||
&.vs__dropdown-option--highlight {
|
||||
color: var(--login-form-background) !important;
|
||||
background: var(--login-form-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user