diff --git a/src/components/LinkItems/Item.vue b/src/components/LinkItems/Item.vue index e66712df..da51469a 100644 --- a/src/components/LinkItems/Item.vue +++ b/src/components/LinkItems/Item.vue @@ -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; } diff --git a/src/components/LinkItems/ItemIcon.vue b/src/components/LinkItems/ItemIcon.vue index 5e3a9667..41786b8e 100644 --- a/src/components/LinkItems/ItemIcon.vue +++ b/src/components/LinkItems/ItemIcon.vue @@ -1,13 +1,14 @@ @@ -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; } diff --git a/src/utils/defaults.js b/src/utils/defaults.js index 81d49b47..16418044 100644 --- a/src/utils/defaults.js +++ b/src/utils/defaults.js @@ -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 */