Reliability improvements for icon fallbacks

This commit is contained in:
Alicia Sykes 2021-11-30 15:58:47 +00:00
parent c9f2483c3e
commit 57554ddcdf
3 changed files with 116 additions and 79 deletions

View File

@ -452,7 +452,7 @@ export default {
height: 2rem;
padding-top: 4px;
max-width: 14rem;
div img, div svg.missing-image {
div img {
width: 2rem;
}
.tile-title {
@ -473,7 +473,7 @@ export default {
flex-direction: column;
align-items: center;
height: auto;
div img, div svg.missing-image {
div img {
width: 2.5rem;
margin-bottom: 0.25rem;
}

View File

@ -1,13 +1,14 @@
<template>
<div :class="`item-icon wrapper-${size}`">
<div v-if="icon" :class="`item-icon wrapper-${size}`">
<!-- Font-Awesome Icon -->
<i v-if="iconType === 'font-awesome'" :class="`${icon} ${size}`" ></i>
<!-- Emoji Icon -->
<i v-else-if="iconType === 'emoji'" :class="`emoji-icon ${size}`" >{{getEmoji(iconPath)}}</i>
<!-- Material Design Icon -->
<span v-else-if="iconType === 'mdi'" :class=" `mdi ${icon} ${size}`"></span>
<span v-else-if="iconType === 'mdi'" :class=" `mdi ${icon} ${size}`"></span>
<!-- Simple-Icons -->
<svg v-else-if="iconType === 'si'" :class="`simple-icons ${size}`" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<svg v-else-if="iconType === 'si' && !broken" :class="`simple-icons ${size}`"
role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path :d="getSimpleIcon(icon)" />
</svg>
<!-- Standard image asset icon -->
@ -15,7 +16,7 @@
:class="`tile-icon ${size} ${broken ? 'broken' : ''}`"
/>
<!-- Icon could not load/ broken url -->
<BrokenImage v-if="broken" class="missing-image" />
<BrokenImage v-if="broken" :class="`missing-image ${size}`" />
</div>
</template>
@ -25,8 +26,8 @@ import BrokenImage from '@/assets/interface-icons/broken-icon.svg';
import ErrorHandler from '@/utils/ErrorHandler';
import EmojiUnicodeRegex from '@/utils/EmojiUnicodeRegex';
import emojiLookup from '@/utils/emojis.json';
import { faviconApi as defaultFaviconApi, faviconApiEndpoints, iconCdns } from '@/utils/defaults';
import { asciiHash } from '@/utils/MiscHelpers';
import { faviconApi as defaultFaviconApi, faviconApiEndpoints, iconCdns } from '@/utils/defaults';
export default {
name: 'Icon',
@ -36,7 +37,7 @@ export default {
size: String, // Either small, medium or large
},
components: {
BrokenImage,
BrokenImage, // Used when the desired image returns a 404
},
computed: {
/* Get appConfig from store */
@ -60,6 +61,39 @@ export default {
};
},
methods: {
/* Determine icon type, e.g. local or remote asset, SVG, favicon, font-awesome, etc */
determineImageType(img) {
let imgType = '';
if (!img) imgType = 'none';
else if (this.isUrl(img)) imgType = 'url';
else if (this.isImage(img)) imgType = 'img';
else if (img.includes('fa-')) imgType = 'font-awesome';
else if (img.includes('mdi-')) imgType = 'mdi';
else if (img.includes('si-')) imgType = 'si';
else if (img.includes('hl-')) imgType = 'home-lab-icons';
else if (img.includes('favicon-')) imgType = 'custom-favicon';
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;
},
/* Return the path to icon asset, depending on icon type */
getIconPath(img, url) {
switch (this.determineImageType(img)) {
case 'url': return img;
case 'img': return this.getLocalImagePath(img);
case 'favicon': return this.getFavicon(url);
case 'custom-favicon': return this.getCustomFavicon(url, img);
case 'generative': return this.getGenerativeIcon(url);
case 'mdi': return img; // Material design icons
case 'simple-icons': return this.getSimpleIcon(img);
case 'home-lab-icons': return this.getHomeLabIcon(img);
case 'svg': return img; // Local SVG icon
case 'emoji': return img; // Emoji/ unicode
default: return '';
}
},
/* Check if a string is in a URL format. Used to identify tile icon source */
isUrl(str) {
const pattern = new RegExp(/(http|https):\/\/(\w+:{0,1}\w*)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%!\-/]))?/);
@ -73,7 +107,7 @@ 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 */
/* Determines 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' };
@ -84,15 +118,27 @@ export default {
}
return { isEmoji: false, emojiType: '' };
},
/* Formats and gets emoji from unicode or shortcode */
/* Returns the corresponding emoji for a shortcode, or shows error if not found */
getShortCodeEmoji(emojiCode) {
if (emojiLookup[emojiCode]) {
return emojiLookup[emojiCode];
} else {
this.imageNotFound(`No emoji found with name '${emojiCode}'`);
return null;
}
},
/* Formats and gets emoji from either unicode, shortcode or glyph */
getEmoji(emojiCode) {
const { emojiType } = this.isEmoji(emojiCode);
if (emojiType === 'shortcode') {
if (emojiLookup[emojiCode]) return emojiLookup[emojiCode];
} else if (emojiType === 'unicode') {
if (emojiType === 'shortcode') { // Short code emoji
return this.getShortCodeEmoji(emojiCode);
} else if (emojiType === 'unicode') { // Unicode emoji
return String.fromCodePoint(parseInt(emojiCode.substr(2), 16));
} else if (emojiType === 'glyph') { // Emoji is a glyph
return emojiCode;
}
return emojiCode; // Emoji is a glyph already, just return
this.imageNotFound(`Unrecognized emoji: '${emojiCode}'`);
return null;
},
/* Get favicon URL, for items which use the favicon as their icon */
getFavicon(fullUrl, specificApi) {
@ -109,16 +155,17 @@ export default {
},
/* Get the URL for a favicon, but using the non-default favicon API */
getCustomFavicon(fullUrl, faviconIdentifier) {
let errorMsg = '';
const faviconApi = faviconIdentifier.split('favicon-')[1];
if (!faviconApi) {
ErrorHandler('Favicon API not specified');
errorMsg = 'Favicon API not specified';
} else if (!Object.keys(faviconApiEndpoints).includes(faviconApi)) {
ErrorHandler(`The specified favicon API, '${faviconApi}' cannot be found.`);
errorMsg = `The specified favicon API, '${faviconApi}' cannot be found.`;
} else {
return this.getFavicon(fullUrl, faviconApi);
}
// Error encountered, favicon service not found
this.broken = true;
this.imageNotFound(errorMsg);
return undefined;
},
/* If using favicon for icon, and if service is running locally (determined by local IP) */
@ -140,69 +187,53 @@ export default {
getSimpleIcon(img) {
const imageName = img.replace('si-', '');
const icon = simpleIcons.Get(imageName);
if (!icon) {
this.imageNotFound(`No icon was found for '${imageName}' in Simple Icons`);
return null;
}
return icon.path;
},
/* Gets home-lab icon from GitHub */
getHomeLabIcon(img) {
getHomeLabIcon(img, cdn) {
const imageName = img.replace('hl-', '').toLocaleLowerCase();
return iconCdns.homeLabIcons.replace('{icon}', imageName);
},
/* Checks if the icon is from a local image, remote URL, SVG or font-awesome */
getIconPath(img, url) {
switch (this.determineImageType(img)) {
case 'url': return img;
case 'img': return this.getLocalImagePath(img);
case 'favicon': return this.getFavicon(url);
case 'custom-favicon': return this.getCustomFavicon(url, img);
case 'generative': return this.getGenerativeIcon(url);
case 'mdi': return img; // Material design icons
case 'simple-icons': return this.getSimpleIcon(img);
case 'home-lab-icons': return this.getHomeLabIcon(img);
case 'svg': return img; // Local SVG icon
case 'emoji': return img; // Emoji/ unicode
default: return '';
}
},
/* Checks if the icon is from a local image, remote URL, SVG or font-awesome */
determineImageType(img) {
let imgType = '';
if (!img) imgType = 'none';
else if (this.isUrl(img)) imgType = 'url';
else if (this.isImage(img)) imgType = 'img';
else if (img.includes('fa-')) imgType = 'font-awesome';
else if (img.includes('mdi-')) imgType = 'mdi';
else if (img.includes('si-')) imgType = 'si';
else if (img.includes('hl-')) imgType = 'home-lab-icons';
else if (img.includes('favicon-')) imgType = 'custom-favicon';
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;
return (cdn || iconCdns.homeLabIcons).replace('{icon}', imageName);
},
/* For a given URL, return the hostname only. Used for favicon and generative icons */
getHostName(url) {
try { return new URL(url).hostname; } catch (e) { return url; }
try {
return new URL(url).hostname.split('.').slice(-2).join('.');
} catch (e) {
ErrorHandler('Unable to format URL');
return url;
}
},
/* Called when the path to the image cannot be resolved */
imageNotFound() {
imageNotFound(errorMsg) {
let outputMessage = '';
if (errorMsg && typeof errorMsg === 'string') {
outputMessage = errorMsg;
} else if (!this.icon) {
outputMessage = 'Icon not specified';
} else {
outputMessage = `The path to '${this.icon}' could not be resolved`;
}
ErrorHandler(outputMessage);
this.broken = true;
ErrorHandler(`The path to '${this.icon}' could not be resolved`);
},
/* Called when initial icon has resulted in 404. Attempts to find new icon */
getFallbackIcon() {
if (this.attemptedFallback) return undefined; // If this is second attempt, then give up
const { iconType } = this;
const markAsSttempted = () => {
this.broken = false;
this.attemptedFallback = true;
};
const markAsAttempted = () => { this.broken = false; this.attemptedFallback = true; };
if (iconType.includes('favicon')) { // Specify fallback for favicon-based icons
markAsSttempted();
markAsAttempted();
return this.getFavicon(this.url, 'local');
} else if (iconType === 'generative') {
markAsSttempted();
markAsAttempted();
return this.getGenerativeIcon(this.url, iconCdns.generativeFallback);
} else if (iconType === 'home-lab-icons') {
markAsAttempted();
return this.getHomeLabIcon(this.icon, iconCdns.homeLabIconsFallback);
}
return undefined;
},
@ -290,7 +321,13 @@ export default {
}
/* Icon Not Found */
.missing-image {
width: 3.5rem;
width: 2rem;
&.small {
width: 1.5rem !important;
}
&.large {
width: 2.5rem;
}
path {
fill: currentColor;
}

View File

@ -27,6 +27,8 @@ module.exports = {
faviconApi: 'allesedv',
/* The default sort order for sections */
sortOrder: 'default',
/* If no 'target' specified, this is the default opening method */
openingMethod: 'newtab',
/* The page paths for each route within the app for the router */
routePaths: {
home: '/home',
@ -74,6 +76,18 @@ module.exports = {
'high-contrast-dark',
'high-contrast-light',
],
/* Default color options for the theme configurator swatches */
swatches: [
['#eb5cad', '#985ceb', '#5346f3', '#5c90eb'],
['#5cdfeb', '#00CCB4', '#5ceb8d', '#afeb5c'],
['#eff961', '#ebb75c', '#eb615c', '#eb2d6c'],
['#060913', '#141b33', '#1c2645', '#263256'],
['#2b2d42', '#1a535c', '#372424', '#312437'],
['#f5f5f5', '#d9d9d9', '#bfbfbf', '#9a9a9a'],
['#636363', '#363636', '#313941', '#0d0d0d'],
],
/* Which CSS variables to show in the first view of theme configurator */
mainCssVars: ['primary', 'background', 'background-darker'],
/* Which structural components should be visible by default */
visibleComponents: {
splashScreen: false,
@ -88,8 +102,6 @@ module.exports = {
'minimal',
'login',
'download',
'landing-page-minimal',
// '404',
],
/* Key names for local storage identifiers */
localStorageKeys: {
@ -138,17 +150,14 @@ module.exports = {
PAGE_INFO: 'pageInfo',
APP_CONFIG: 'appConfig',
SECTIONS: 'sections',
WIDGETS: 'widgets',
},
/* Which CSS variables to show in the first view of theme configurator */
mainCssVars: ['primary', 'background', 'background-darker'],
/* Amount of time to show splash screen, when enabled, in milliseconds */
splashScreenTime: 1900,
splashScreenTime: 1000,
/* Page meta-data, rendered in the header of each view */
metaTagData: [
{ name: 'description', content: 'A simple static homepage for you\'re server' },
],
/* If no 'target' specified, this is the default opening method */
openingMethod: 'newtab',
/* Default option for Toast messages */
toastedOptions: {
position: 'bottom-center',
@ -192,6 +201,7 @@ module.exports = {
localPath: './item-icons',
faviconName: 'favicon.ico',
homeLabIcons: 'https://raw.githubusercontent.com/WalkxCode/dashboard-icons/master/png/{icon}.png',
homeLabIconsFallback: 'https://raw.githubusercontent.com/NX211/homer-icons/master/png/{icon}.png',
},
/* URLs for web search engines */
searchEngineUrls: {
@ -233,16 +243,6 @@ module.exports = {
'/so': 'stackoverflow',
'/wa': 'wolframalpha',
},
/* Available built-in colors for the theme builder */
swatches: [
['#eb5cad', '#985ceb', '#5346f3', '#5c90eb'],
['#5cdfeb', '#00CCB4', '#5ceb8d', '#afeb5c'],
['#eff961', '#ebb75c', '#eb615c', '#eb2d6c'],
['#060913', '#141b33', '#1c2645', '#263256'],
['#2b2d42', '#1a535c', '#372424', '#312437'],
['#f5f5f5', '#d9d9d9', '#bfbfbf', '#9a9a9a'],
['#636363', '#363636', '#313941', '#0d0d0d'],
],
/* Use your own self-hosted Sentry instance. Only used if error reporting is turned on */
sentryDsn: 'https://3138ea85f15a4fa883a5b27a4dc8ee28@o937511.ingest.sentry.io/5887934',
/* A JS enum for indicating the user state, when guest mode + authentication is enabled */