⚠️ Fixes merge conflicts

This commit is contained in:
Alicia Sykes
2021-07-02 19:25:41 +01:00
40 changed files with 2785 additions and 123 deletions

View 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

View File

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

View 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>

View File

@@ -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;

View File

@@ -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()"

View File

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

View File

@@ -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>

View File

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

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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');
}

View File

@@ -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
View 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);
},
};

View File

@@ -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"
}
}
}

View 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])/;

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -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>